diff --git a/core/api.txt b/core/api.txt index dafa0080ccf..d6ab0738408 100644 --- a/core/api.txt +++ b/core/api.txt @@ -2378,6 +2378,8 @@ ion-select-modal,prop,options,SelectModalOption[],[],false,false ion-select-option,shadow ion-select-option,prop,description,string | undefined,undefined,false,false ion-select-option,prop,disabled,boolean,false,false,false +ion-select-option,prop,justify,"end" | "space-between" | "start" | undefined,undefined,false,false +ion-select-option,prop,labelPlacement,"end" | "start" | undefined,undefined,false,false ion-select-option,prop,mode,"ios" | "md",undefined,false,false ion-select-option,prop,theme,"ios" | "md" | "ionic",undefined,false,false ion-select-option,prop,value,any,undefined,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index c47221f63cb..787caa378d4 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -3863,6 +3863,14 @@ export namespace Components { * @default false */ "disabled": boolean; + /** + * How to pack the label and the option's selection control within a line. `"start"`: The label and radio will appear on the left in LTR and on the right in RTL. `"end"`: The label and radio will appear on the right in LTR and on the left in RTL. `"space-between"`: The label and radio will appear on opposite ends of the line with space between the two elements. Applies to the `alert`, `popover`, and `modal` interfaces, but has no visible effect on radio options in `popover` or `modal` on the `md` and `ionic` themes (the radio control is hidden there). When unset, the interface picks a default based on theme and control type. + */ + "justify"?: 'start' | 'end' | 'space-between'; + /** + * Where the label is placed relative to the option's selection control (radio circle or checkbox box) when the option is rendered in an `alert`, `popover`, or `modal` interface. `"start"`: The label will appear to the left of the radio in LTR and to the right in RTL. `"end"`: The label will appear to the right of the radio in LTR and to the left in RTL. Applies to the `alert`, `popover`, and `modal` interfaces, but has no visible effect on radio options in `popover` or `modal` on the `md` and `ionic` themes (the radio control is hidden there). When unset, the interface picks a default based on theme and control type. + */ + "labelPlacement"?: 'start' | 'end'; /** * The mode determines the platform behaviors of the component. */ @@ -10002,6 +10010,14 @@ declare namespace LocalJSX { * @default false */ "disabled"?: boolean; + /** + * How to pack the label and the option's selection control within a line. `"start"`: The label and radio will appear on the left in LTR and on the right in RTL. `"end"`: The label and radio will appear on the right in LTR and on the left in RTL. `"space-between"`: The label and radio will appear on opposite ends of the line with space between the two elements. Applies to the `alert`, `popover`, and `modal` interfaces, but has no visible effect on radio options in `popover` or `modal` on the `md` and `ionic` themes (the radio control is hidden there). When unset, the interface picks a default based on theme and control type. + */ + "justify"?: 'start' | 'end' | 'space-between'; + /** + * Where the label is placed relative to the option's selection control (radio circle or checkbox box) when the option is rendered in an `alert`, `popover`, or `modal` interface. `"start"`: The label will appear to the left of the radio in LTR and to the right in RTL. `"end"`: The label will appear to the right of the radio in LTR and to the left in RTL. Applies to the `alert`, `popover`, and `modal` interfaces, but has no visible effect on radio options in `popover` or `modal` on the `md` and `ionic` themes (the radio control is hidden there). When unset, the interface picks a default based on theme and control type. + */ + "labelPlacement"?: 'start' | 'end'; /** * The mode determines the platform behaviors of the component. */ @@ -11382,6 +11398,8 @@ declare namespace LocalJSX { "disabled": boolean; "value": string; "description": string; + "labelPlacement": 'start' | 'end'; + "justify": 'start' | 'end' | 'space-between'; } interface IonSelectPopoverAttributes { "header": string; diff --git a/core/src/components/action-sheet/action-sheet.common.scss b/core/src/components/action-sheet/action-sheet.common.scss index 7e857346ac9..c475c9ac1fe 100644 --- a/core/src/components/action-sheet/action-sheet.common.scss +++ b/core/src/components/action-sheet/action-sheet.common.scss @@ -1,4 +1,5 @@ -@import "./action-sheet.vars"; +@use "../../themes/mixins" as mixins; +@use "../select-option/select-option.common.overlay"; // Action Sheet // -------------------------------------------------- @@ -41,25 +42,22 @@ --button-color-hover: var(--button-color); --button-color-selected: var(--button-color); --min-width: auto; - --width: #{$action-sheet-width}; - --max-width: #{$action-sheet-max-width}; + --width: 100%; + --max-width: 500px; --min-height: auto; --height: auto; --max-height: calc(100% - (var(--ion-safe-area-top) + var(--ion-safe-area-bottom))); - @include font-smoothing(); - @include position(0, 0, 0, 0); + @include mixins.font-smoothing(); + @include mixins.position(0, 0, 0, 0); display: block; position: fixed; outline: none; - font-family: $font-family-base; - touch-action: none; user-select: none; - z-index: $z-index-overlay; } :host(.overlay-hidden) { @@ -67,8 +65,8 @@ } .action-sheet-wrapper { - @include position(null, 0, 0, 0); - @include transform(translate3d(0, 100%, 0)); + @include mixins.position(null, 0, 0, 0); + @include mixins.transform(translate3d(0, 100%, 0)); display: block; position: absolute; @@ -81,7 +79,6 @@ min-height: var(--min-height); max-height: var(--max-height); - z-index: $z-index-overlay-wrapper; pointer-events: none; } @@ -109,6 +106,10 @@ opacity: 0.4; } +.action-sheet-button:disabled ion-icon { + color: currentColor; +} + .action-sheet-button-inner { display: flex; @@ -177,7 +178,7 @@ // -------------------------------------------------- .action-sheet-button::after { - @include button-state(); + @include mixins.button-state(); } // Action Sheet: Selected @@ -209,7 +210,7 @@ // Action Sheet: Focused // -------------------------------------------------- -.action-sheet-button.ion-focused { +.action-sheet-button.ion-focused:not(.ion-activated) { color: var(--button-color-focused); &::after { @@ -217,6 +218,12 @@ opacity: var(--button-background-focused-opacity); } + + &.action-sheet-selected::after { + background: var(--button-background-focused, var(--button-background-selected)); + + opacity: var(--button-background-focused-opacity, var(--button-background-selected-opacity)); + } } // Action Sheet: Hover @@ -233,20 +240,3 @@ } } } - -// Action Sheet: Select Option -// -------------------------------------------------- - -.action-sheet-button-label { - display: flex; - - align-items: center; -} - -.select-option-content { - flex: 1; -} - -.select-option-description { - display: block; -} diff --git a/core/src/components/action-sheet/action-sheet.ionic.scss b/core/src/components/action-sheet/action-sheet.ionic.scss index b2c749d4e0a..14f12768d9e 100644 --- a/core/src/components/action-sheet/action-sheet.ionic.scss +++ b/core/src/components/action-sheet/action-sheet.ionic.scss @@ -1,22 +1,110 @@ @use "../../themes/ionic/ionic.globals.scss" as globals; +@use "../../themes/mixins" as mixins; +@use "../select-option/select-option.ionic.overlay"; @use "./action-sheet.common"; -@use "./action-sheet.md" as action-sheet-md; // Ionic Action Sheet // -------------------------------------------------- -// Action Sheet: Select Option -// -------------------------------------------------- +:host { + --background: #{globals.$ion-bg-surface-default}; + --backdrop-opacity: 0.7; + --button-background: transparent; + --button-background-selected: #{globals.$ion-bg-primary-subtle-default}; + --button-background-selected-opacity: 1; + --button-background-activated: #{globals.$ion-bg-neutral-subtlest-press}; + --button-background-activated-opacity: 1; + --button-background-hover: #{globals.$ion-bg-neutral-subtlest-press}; + --button-background-hover-opacity: 1; + --button-color: #{globals.$ion-text-default}; + --button-color-disabled: #{globals.$ion-text-disabled}; + --color: #{globals.$ion-text-default}; + + z-index: 1001; +} + +.action-sheet-wrapper { + z-index: 10; +} + +.action-sheet-button.ion-focused::after { + @include globals.focused-state( + globals.$ion-border-size-050, + globals.$ion-border-style-solid, + globals.$ion-border-focus-default + ); + + outline-offset: calc(globals.$ion-border-size-050 * -1); +} + +// Action Sheet Wrapper +// ----------------------------------------- -.action-sheet-button-label { - gap: globals.$ion-space-300; +.action-sheet-wrapper { + @include mixins.margin(var(--ion-safe-area-top, 0), auto, 0, auto); } -.select-option-description { +.action-sheet-title { + @include mixins.padding(globals.$ion-space-400); + @include globals.typography(globals.$ion-heading-h6-medium); + + color: var(--color); + + text-align: start; +} + +.action-sheet-sub-title { @include globals.typography(globals.$ion-body-md-regular); - @include globals.padding(0); - color: globals.$ion-text-subtle; + color: globals.$ion-text-subtlest; +} + +// Action Sheet Group +// ----------------------------------------- + +.action-sheet-group:first-child { + @include mixins.padding(globals.$ion-space-400, null, null, null); +} + +.action-sheet-group:last-child { + @include mixins.padding(null, null, globals.$ion-space-400, null); +} + +// Action Sheet Buttons +// ----------------------------------------- + +.action-sheet-button { + @include mixins.padding( + globals.$ion-space-200, + globals.$ion-space-400, + globals.$ion-space-200, + globals.$ion-space-400 + ); + @include globals.typography(globals.$ion-body-md-regular); + + position: relative; + + min-height: 52px; + + text-align: start; + + contain: content; + overflow: hidden; +} + +.action-sheet-icon { + @include mixins.margin(globals.$ion-space-0, globals.$ion-space-600, globals.$ion-space-0, globals.$ion-space-0); + + width: globals.$ion-scale-600; + height: globals.$ion-scale-600; + + color: var(--color, globals.$ion-text-default); +} + +.action-sheet-button-inner { + justify-content: flex-start; +} - font-size: globals.$ion-font-size-350; +.action-sheet-selected { + font-weight: bold; } diff --git a/core/src/components/action-sheet/action-sheet.ios.scss b/core/src/components/action-sheet/action-sheet.ios.scss index 94b98447981..94d5682f29a 100644 --- a/core/src/components/action-sheet/action-sheet.ios.scss +++ b/core/src/components/action-sheet/action-sheet.ios.scss @@ -1,3 +1,4 @@ +@use "../select-option/select-option.ios.overlay"; @import "./action-sheet.native"; @import "./action-sheet.ios.vars"; diff --git a/core/src/components/action-sheet/action-sheet.md.scss b/core/src/components/action-sheet/action-sheet.md.scss index e46f06085b3..567cfb4448f 100644 --- a/core/src/components/action-sheet/action-sheet.md.scss +++ b/core/src/components/action-sheet/action-sheet.md.scss @@ -1,7 +1,8 @@ +@use "../select-option/select-option.md.overlay"; @import "./action-sheet.native"; @import "./action-sheet.md.vars"; -// Material Design Action Sheet Title +// Material Design Action Sheet // ----------------------------------------- :host { diff --git a/core/src/components/action-sheet/action-sheet.native.scss b/core/src/components/action-sheet/action-sheet.native.scss index affa6aeb126..a4975e5620e 100644 --- a/core/src/components/action-sheet/action-sheet.native.scss +++ b/core/src/components/action-sheet/action-sheet.native.scss @@ -1,19 +1,15 @@ -@use "../../themes/native/native.theme.default" as native; -@use "../../themes/mixins" as mixins; -@use "../../themes/functions.font" as font; +@use "../../themes/native/native.globals" as native; @use "./action-sheet.common"; // Action Sheet: Native // -------------------------------------------------- -.action-sheet-button-label { - gap: 12px; -} - -.select-option-description { - @include mixins.padding(5px, 0, 0, 0); +:host { + font-family: native.$font-family-base; - color: native.$text-color-step-300; + z-index: native.$z-index-overlay; +} - font-size: font.dynamic-font(12px); +.action-sheet-wrapper { + z-index: native.$z-index-overlay-wrapper; } diff --git a/core/src/components/action-sheet/action-sheet.vars.scss b/core/src/components/action-sheet/action-sheet.vars.scss deleted file mode 100644 index d81812ae23e..00000000000 --- a/core/src/components/action-sheet/action-sheet.vars.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import "../../themes/native/native.globals"; - -// Action Sheet -// -------------------------------------------------- - -/// @prop - Width of the action sheet -$action-sheet-width: 100%; - -/// @prop - Maximum width of the action sheet -$action-sheet-max-width: 500px; diff --git a/core/src/components/action-sheet/test/basic/index.html b/core/src/components/action-sheet/test/basic/index.html index b95d43b42c7..e7ec4e819ba 100644 --- a/core/src/components/action-sheet/test/basic/index.html +++ b/core/src/components/action-sheet/test/basic/index.html @@ -46,6 +46,8 @@ .my-color-class { --background: #292929; --button-background-selected: #222222; + --button-background-activated: #393838; + --button-background-activated-opacity: 1; --color: #dfdfdf; --button-color: #dfdfdf; diff --git a/core/src/components/action-sheet/test/states/action-sheet.e2e.ts b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts new file mode 100644 index 00000000000..6929878f12b --- /dev/null +++ b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts @@ -0,0 +1,40 @@ +import { configs, test } from '@utils/test/playwright'; + +import { ActionSheetFixture } from '../basic/fixture'; + +/** + * This behavior does not vary across directions. + */ +configs({ directions: ['ltr'], modes: ['ios', 'md', 'ionic-md'] }).forEach(({ config, screenshot, title }) => { + test.describe(title('action sheet: states'), () => { + /** + * `(any-hover: hover)` evaluates to "none" in all three mobile-emulated + * projects, suppressing the hover rules: + * + * - Chromium and WebKit suppress it because of hasTouch and isMobile. + * - Headless Firefox doesn't detect input devices and reports no hover + * capability regardless of context options, so override it via + * launchOptions.firefoxUserPrefs. Bit values: 4 = FINE (mouse), + * 8 = HOVER, 12 = FINE | HOVER. + * + * Viewport, userAgent, and scale factor remain mobile-sized. + */ + test.use({ + hasTouch: false, + isMobile: false, + }); + + test('should render all button states', async ({ page }) => { + await page.goto(`/src/components/action-sheet/test/states`, config); + + const actionSheetFixture = new ActionSheetFixture(page, screenshot); + + await actionSheetFixture.open('#basic'); + + const defaultButton = page.locator('ion-action-sheet button.action-sheet-button').first(); + await defaultButton.hover(); + + await actionSheetFixture.screenshot('states'); + }); + }); +}); diff --git a/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..c9604b1aadd Binary files /dev/null and b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..4901c2e1b00 Binary files /dev/null and b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..c20a2716105 Binary files /dev/null and b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..5a3e78894ef Binary files /dev/null and b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..5ae3278c1b2 Binary files /dev/null and b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ios-ltr-Mobile-Safari-linux.png b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..d03447bdf25 Binary files /dev/null and b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-md-ltr-Mobile-Chrome-linux.png b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..414268d7a30 Binary files /dev/null and b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-md-ltr-Mobile-Firefox-linux.png b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..0b2cd5cb038 Binary files /dev/null and b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-md-ltr-Mobile-Safari-linux.png b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..1d3bc166290 Binary files /dev/null and b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/action-sheet/test/states/index.html b/core/src/components/action-sheet/test/states/index.html new file mode 100644 index 00000000000..5d5339d5d1b --- /dev/null +++ b/core/src/components/action-sheet/test/states/index.html @@ -0,0 +1,97 @@ + + + + + Action Sheet - States + + + + + + + + + + + + + + Action Sheet - States + + + + + + + + + + + + diff --git a/core/src/components/alert/alert.common.scss b/core/src/components/alert/alert.common.scss index 84e35eca5c3..b6860acb579 100644 --- a/core/src/components/alert/alert.common.scss +++ b/core/src/components/alert/alert.common.scss @@ -1,4 +1,6 @@ -@import "./alert.vars"; +@use "../../themes/functions.font" as font; +@use "../../themes/mixins" as mixins; +@use "../select-option/select-option.common.overlay"; // Alert // -------------------------------------------------- @@ -17,14 +19,14 @@ * * @prop --backdrop-opacity: Opacity of the backdrop */ - --min-width: #{$alert-min-width}; + --min-width: 250px; --width: auto; --min-height: auto; --height: auto; - --max-height: #{$alert-max-height}; + --max-height: 90%; - @include font-smoothing(); - @include position(0, 0, 0, 0); + @include mixins.font-smoothing(); + @include mixins.position(0, 0, 0, 0); display: flex; position: absolute; @@ -34,12 +36,9 @@ outline: none; - font-family: $font-family-base; - contain: strict; touch-action: none; user-select: none; - z-index: $z-index-overlay; } :host(.overlay-hidden) { @@ -47,7 +46,7 @@ } :host(.alert-top) { - @include padding(50px, null, null, null); + @include mixins.padding(50px, null, null, null); align-items: flex-start; } @@ -69,17 +68,16 @@ contain: content; opacity: 0; - z-index: $z-index-overlay-wrapper; } .alert-title { - @include margin(0); - @include padding(0); + @include mixins.margin(0); + @include mixins.padding(0); } .alert-sub-title { - @include margin(5px, 0, 0); - @include padding(0); + @include mixins.margin(5px, 0, 0); + @include mixins.padding(0); font-weight: normal; } @@ -140,7 +138,7 @@ } .alert-input { - @include padding(10px, 0); + @include mixins.padding(10px, 0); width: 100%; @@ -166,24 +164,19 @@ } .alert-button { - @include margin(0); + @include mixins.margin(0); display: block; border: 0; - font-size: $alert-button-font-size; + font-size: font.dynamic-font(14px); - line-height: $alert-button-line-height; + line-height: font.dynamic-font(20px); z-index: 0; } -.alert-button.ion-focused, -.alert-tappable.ion-focused { - background: $background-color-step-100; -} - .alert-button-inner { display: flex; @@ -198,6 +191,45 @@ min-height: inherit; } +// Alert Option: Label Placement +// -------------------------------------------------- + +/** + * Label is on the right of the radio in LTR and + * on the left in RTL. + */ +.radio-label-placement-start, +.checkbox-label-placement-start { + flex-direction: row-reverse; +} + +/** + * Label is on the left of the radio in LTR and + * on the right in RTL. + */ +.radio-label-placement-end, +.checkbox-label-placement-end { + flex-direction: row; +} + +// Alert Option: Justify +// -------------------------------------------------- + +.radio-justify-start, +.checkbox-justify-start { + justify-content: start; +} + +.radio-justify-end, +.checkbox-justify-end { + justify-content: end; +} + +.radio-justify-space-between, +.checkbox-justify-space-between { + justify-content: space-between; +} + // Alert Button: Disabled // -------------------------------------------------- .alert-input-disabled, @@ -209,8 +241,8 @@ } .alert-tappable { - @include margin(0); - @include padding(0); + @include mixins.margin(0); + @include mixins.padding(0); display: flex; @@ -244,24 +276,6 @@ } textarea.alert-input { - min-height: $alert-input-min-height; + min-height: 37px; resize: none; } - -// Alert Button: Select Option -// -------------------------------------------------- - -.alert-radio-label, -.alert-checkbox-label { - display: flex; - - align-items: center; -} - -.select-option-content { - flex: 1; -} - -.select-option-description { - display: block; -} diff --git a/core/src/components/alert/alert.ionic.scss b/core/src/components/alert/alert.ionic.scss index 3c54136b477..3b41ed3928b 100644 --- a/core/src/components/alert/alert.ionic.scss +++ b/core/src/components/alert/alert.ionic.scss @@ -1,23 +1,380 @@ @use "../../themes/ionic/ionic.globals.scss" as globals; +@use "../../themes/mixins" as mixins; +@use "../select-option/select-option.ionic.overlay"; @use "./alert.common"; -@use "./alert.md" as alert-md; // Ionic Alert // -------------------------------------------------- -// Alert: Select Option +:host { + --background: #{globals.$ion-bg-surface-default}; + --max-width: #{globals.$ion-scale-7400}; + --backdrop-opacity: 0.7; + + z-index: 1001; +} + +.alert-wrapper { + @include globals.border-radius(globals.$ion-border-radius-200); + + box-shadow: globals.$ion-elevation-4; + + z-index: 10; +} + +.alert-button.ion-focused { + @include globals.focused-state( + globals.$ion-border-size-050, + globals.$ion-border-style-solid, + globals.$ion-border-focus-default + ); +} + +.alert-tappable.ion-focused { + @include globals.focused-state( + globals.$ion-border-size-050, + globals.$ion-border-style-solid, + globals.$ion-border-focus-default + ); + + outline-offset: calc(globals.$ion-border-size-050 * -1); +} + +.alert-tappable.ion-activated, +.alert-tappable:not(:disabled):hover { + background: globals.$ion-bg-neutral-subtlest-press; +} + +.alert-tappable[aria-checked="true"] { + background: globals.$ion-bg-primary-subtle-default; +} + +// Ionic Alert Header // -------------------------------------------------- -.alert-radio-label, -.alert-checkbox-label { - gap: globals.$ion-space-300; +.alert-head { + @include mixins.padding(globals.$ion-space-400); + + text-align: start; } -.select-option-description { +.alert-title { + @include globals.typography(globals.$ion-heading-h6-medium); + + color: globals.$ion-text-default; +} + +.alert-sub-title { + @include globals.typography(globals.$ion-body-md-regular); + + color: globals.$ion-text-subtlest; +} + +// Ionic Alert Message +// -------------------------------------------------- + +.alert-message, +.alert-input-group { @include globals.typography(globals.$ion-body-md-regular); - @include globals.padding(0); + @include mixins.padding(globals.$ion-space-400); + + color: globals.$ion-text-default; +} + +/** + * Ionic Alerts on tablets can expand vertically up to + * a total maximum height. We only want to set a max-height + * on mobile phones. + */ +@include globals.mobile-viewport() { + .alert-message { + max-height: globals.$ion-scale-6200; + } +} + +.alert-message:empty { + @include mixins.padding(globals.$ion-space-0); +} + +.alert-head + .alert-message { + padding-top: globals.$ion-space-0; +} + +// Ionic Alert Input +// -------------------------------------------------- + +.alert-input { + @include mixins.margin(globals.$ion-space-150, 0); + + border-bottom: globals.$ion-border-size-025 globals.$ion-border-style-solid globals.$ion-border-input-default; + + color: globals.$ion-text-default; + + &::placeholder { + color: globals.$ion-text-subtlest; + + font-family: inherit; + font-weight: inherit; + } + + &::-ms-clear { + display: none; + } +} + +.alert-input:focus { + @include mixins.margin(null, null, globals.$ion-scale-100, null); + + border-bottom: globals.$ion-border-size-050 globals.$ion-border-style-solid globals.$ion-border-focus-default; +} + +// Ionic Alert Radio/Checkbox Group +// -------------------------------------------------- + +.alert-radio-group, +.alert-checkbox-group { + position: relative; + + border-top: globals.$ion-border-size-025 globals.$ion-border-style-solid globals.$ion-border-default; + border-bottom: globals.$ion-border-size-025 globals.$ion-border-style-solid globals.$ion-border-default; + + overflow: auto; +} + +/** + * Ionic Alerts on tablets can expand vertically up to + * a total maximum height. We only want to set a max-height + * on mobile phones. + */ +@include globals.mobile-viewport() { + .alert-radio-group, + .alert-checkbox-group { + max-height: globals.$ion-scale-6200; + } +} + +.alert-tappable { + position: relative; + + min-height: globals.$ion-scale-1200; +} + +// Ionic Alert Radio +// -------------------------------------------------- + +.alert-radio-label { + @include globals.typography(globals.$ion-body-md-regular); + @include mixins.padding(globals.$ion-space-200, null, globals.$ion-space-200, null); + + // Required for the radio icon to stay on the screen without + // being squished when the font size scales up. + max-width: calc(100% - globals.$ion-space-400); + + color: globals.$ion-text-default; +} + +.radio-label-placement-start .alert-radio-label { + /** + * The label's inline padding clears space for the icon on the + * icon's side. When the label is placed at the start, the icon + * is at the end, so swap the inline paddings to mirror that geometry. + */ + @include mixins.padding-horizontal(globals.$ion-space-400, globals.$ion-space-800); +} + +.radio-label-placement-end .alert-radio-label { + /** + * The label's inline padding clears space for the icon on the + * icon's side. When the label is placed at the end, the icon + * is at the start, so swap the inline paddings to mirror that geometry. + */ + @include mixins.padding-horizontal(globals.$ion-space-800, globals.$ion-space-400); +} + +// Ionic Alert Radio Outer Circle: Unchecked +// --------------------------------------------------- + +.alert-radio-icon { + @include globals.position(globals.$ion-space-0, null, null, null); + @include globals.border-radius(globals.$ion-border-radius-full); - color: globals.$ion-text-subtle; + display: flex; + position: relative; + + flex-shrink: 0; + + align-items: center; + justify-content: center; + + width: globals.$ion-scale-600; + height: globals.$ion-scale-600; + + border-width: globals.$ion-border-size-025; + border-style: globals.$ion-border-style-solid; + border-color: globals.$ion-border-input-default; + + background-color: globals.$ion-bg-input-default; + + box-sizing: border-box; +} + +// The icon's inline offset gives it a gap from whichever edge the icon +// sits on, which depends on label placement +.radio-label-placement-start .alert-radio-icon { + @include globals.position-horizontal(null, globals.$ion-space-400); +} + +.radio-label-placement-end .alert-radio-icon { + @include globals.position-horizontal(globals.$ion-space-400, null); +} + +// Ionic Alert Radio Inner Dot +// --------------------------------------------------- + +.alert-radio-inner { + @include globals.border-radius(50%); + + width: calc(32% + globals.$ion-border-size-025); + height: calc(32% + globals.$ion-border-size-025); + + background-color: globals.$ion-bg-surface-inverse; + + box-sizing: border-box; +} + +// Ionic Alert Radio Outer Circle: Checked +// --------------------------------------------------- + +[aria-checked="true"] .alert-radio-icon { + border-color: globals.$ion-bg-primary-base-default; + + background-color: globals.$ion-bg-primary-base-default; +} + +// Ionic Alert Checkbox Label +// -------------------------------------------------- + +.alert-checkbox-label { + @include globals.typography(globals.$ion-body-md-regular); + @include mixins.padding( + globals.$ion-space-200, + globals.$ion-space-400, + globals.$ion-space-200, + globals.$ion-space-800 + ); + + // Required for the checkbox icon to stay on the screen without + // being squished when the font size scales up. + max-width: calc(100% - globals.$ion-space-400); + + color: globals.$ion-text-default; +} + +.checkbox-label-placement-start .alert-checkbox-label { + /** + * The label's inline padding clears space for the icon on the + * icon's side. When the label is placed at the start, the icon + * is at the end, so swap the inline paddings to mirror that geometry. + */ + @include mixins.padding-horizontal(globals.$ion-space-400, globals.$ion-space-800); +} + +// Ionic Alert Checkbox Outline: Unchecked +// -------------------------------------------------- + +.alert-checkbox-icon { + @include globals.position(globals.$ion-space-0, null, null, null); + @include globals.border-radius(globals.$ion-border-radius-100); + + display: flex; + position: relative; + + flex-shrink: 0; + + align-items: center; + justify-content: center; + + width: globals.$ion-scale-600; + height: globals.$ion-scale-600; + + border-width: globals.$ion-border-size-025; + border-style: globals.$ion-border-style-solid; + border-color: globals.$ion-primitives-neutral-800; + + background-color: globals.$ion-bg-input-default; + + box-sizing: border-box; +} + +// The icon's inline offset gives it a gap from whichever edge the icon +// sits on, which depends on label placement +.checkbox-label-placement-end .alert-checkbox-icon { + @include globals.position-horizontal(globals.$ion-space-400, null); +} + +.checkbox-label-placement-start .alert-checkbox-icon { + @include globals.position-horizontal(null, globals.$ion-space-400); +} + +.alert-checkbox-inner { + width: globals.$ion-scale-400; + height: globals.$ion-scale-400; +} + +.alert-checkbox-inner path { + fill: globals.$ion-bg-surface-default; +} + +// Ionic Alert Checkbox Checkmark: Checked +// -------------------------------------------------- + +[aria-checked="true"] .alert-checkbox-icon { + border-color: globals.$ion-semantics-primary-base; + + background-color: globals.$ion-bg-primary-base-default; +} + +// Ionic Alert Button +// -------------------------------------------------- + +.alert-button-group { + @include mixins.padding(8px); + + box-sizing: border-box; + + flex-wrap: wrap-reverse; + justify-content: flex-end; +} + +.alert-button { + @include globals.border-radius(globals.$ion-border-size-050); + @include mixins.margin(globals.$ion-space-0, globals.$ion-space-200, globals.$ion-space-0, globals.$ion-space-0); + @include mixins.padding(globals.$ion-space-250); + + // necessary for ripple to work properly + position: relative; + + background-color: transparent; + color: globals.ion-color(primary, base); + + font-weight: globals.$ion-font-weight-medium; + + text-align: end; + + overflow: hidden; +} + +.alert-button .alert-button-inner { + justify-content: flex-end; +} - font-size: globals.$ion-font-size-350; +/** + * Ionic alerts should scale up to 560px x 560px + * on tablet dimensions. + */ +@include globals.tablet-viewport() { + :host { + --max-width: #{min(calc(100vw - 96px), 560px)}; + --max-height: #{min(calc(100vh - 96px), 560px)}; + } } diff --git a/core/src/components/alert/alert.ios.scss b/core/src/components/alert/alert.ios.scss index 2671dc0940b..d81625ec55e 100644 --- a/core/src/components/alert/alert.ios.scss +++ b/core/src/components/alert/alert.ios.scss @@ -1,3 +1,4 @@ +@use "../select-option/select-option.ios.overlay"; @import "./alert.native"; @import "./alert.ios.vars"; @@ -166,9 +167,6 @@ $alert-ios-radio-label-padding-start ); - flex: 1; - order: 0; - color: $alert-ios-radio-label-text-color; } @@ -185,7 +183,7 @@ .alert-radio-icon { position: relative; - order: 1; + flex-shrink: 0; min-width: $alert-ios-radio-min-width; } @@ -194,7 +192,7 @@ // ----------------------------------------- [aria-checked="true"] .alert-radio-inner { - @include position($alert-ios-radio-icon-top, null, null, $alert-ios-radio-icon-start); + @include position($alert-ios-radio-icon-top, null, null, null); position: absolute; @@ -210,6 +208,16 @@ border-color: $alert-ios-radio-icon-border-color; } +// The icon's inline offset gives it a gap from whichever edge the icon +// sits on, which depends on label placement +[aria-checked="true"] .radio-label-placement-end .alert-radio-inner { + @include position-horizontal(null, $alert-ios-radio-icon-start); +} + +[aria-checked="true"] .radio-label-placement-start .alert-radio-inner { + @include position-horizontal($alert-ios-radio-icon-start, null); +} + // iOS Alert Checkbox Label // -------------------------------------------------- @@ -221,8 +229,6 @@ $alert-ios-checkbox-label-padding-start ); - flex: 1; - color: $alert-ios-checkbox-label-text-color; } @@ -231,15 +237,12 @@ .alert-checkbox-icon { @include border-radius($alert-ios-checkbox-border-radius); - @include margin( - $alert-ios-checkbox-margin-top, - $alert-ios-checkbox-margin-end, - $alert-ios-checkbox-margin-bottom, - $alert-ios-checkbox-margin-start - ); + @include margin($alert-ios-checkbox-margin-top, null, $alert-ios-checkbox-margin-bottom, null); position: relative; + flex-shrink: 0; + width: $alert-ios-checkbox-size; height: $alert-ios-checkbox-size; @@ -252,6 +255,17 @@ contain: strict; } +// The icon's inline margins are asymmetric (larger gap from the row +// edge, smaller gap toward the label), so they swap with label +// placement. +.checkbox-label-placement-end .alert-checkbox-icon { + @include margin-horizontal($alert-ios-checkbox-margin-start, $alert-ios-checkbox-margin-end); +} + +.checkbox-label-placement-start .alert-checkbox-icon { + @include margin-horizontal($alert-ios-checkbox-margin-end, $alert-ios-checkbox-margin-start); +} + // iOS Alert Checkbox Outer Circle: Checked // ----------------------------------------- @@ -265,7 +279,7 @@ // ----------------------------------------- [aria-checked="true"] .alert-checkbox-inner { - @include position($alert-ios-checkbox-icon-top, null, null, $alert-ios-checkbox-icon-start); + @include position($alert-ios-checkbox-icon-top, null, null, null); position: absolute; @@ -281,6 +295,16 @@ border-color: $alert-ios-checkbox-icon-border-color; } +// The icon's inline offset gives it a gap from whichever edge the icon +// sits on, which depends on label placement +[aria-checked="true"] .checkbox-label-placement-end .alert-checkbox-inner { + @include position-horizontal($alert-ios-checkbox-icon-start, null); +} + +[aria-checked="true"] .checkbox-label-placement-start .alert-checkbox-inner { + @include position-horizontal(null, $alert-ios-checkbox-icon-start); +} + // iOS Alert Button // -------------------------------------------------- @@ -352,7 +376,7 @@ background-color: $alert-ios-button-background-color-activated; } -// iOS Action Sheet Button: Destructive +// iOS Alert Button: Destructive // --------------------------------------------------- .alert-button-role-destructive, diff --git a/core/src/components/alert/alert.md.scss b/core/src/components/alert/alert.md.scss index 2fbd0fd8775..efaf9c49f91 100644 --- a/core/src/components/alert/alert.md.scss +++ b/core/src/components/alert/alert.md.scss @@ -1,3 +1,4 @@ +@use "../select-option/select-option.md.overlay"; @import "./alert.native"; @import "./alert.md.vars"; @@ -157,30 +158,47 @@ // -------------------------------------------------- .alert-radio-label { - @include padding( - $alert-md-radio-label-padding-top, - $alert-md-radio-label-padding-end, - $alert-md-radio-label-padding-bottom, - $alert-md-radio-label-padding-start - ); + @include padding($alert-md-radio-label-padding-top, null, $alert-md-radio-label-padding-bottom, null); - flex: 1; + // Required for the radio icon to stay on the screen without + // being squished when the font size scales up. + max-width: calc(100% - $alert-md-radio-width); color: $alert-md-radio-label-text-color; font-size: $alert-md-radio-label-font-size; } +.radio-label-placement-end .alert-radio-label { + /** + * The label's inline padding clears space for the icon on the icon's side. + * When the label is placed at the end, the icon is at the start, so the + * larger padding clears it on the start side. + */ + @include padding-horizontal($alert-md-radio-label-padding-start, $alert-md-radio-label-padding-end); +} + +.radio-label-placement-start .alert-radio-label { + /** + * The label's inline padding clears space for the icon on the icon's side. + * When the label is placed at the start, the icon is at the end, so the + * larger padding clears it on the end side. + */ + @include padding-horizontal($alert-md-radio-label-padding-end, $alert-md-radio-label-padding-start); +} + // Material Design Alert Radio Unchecked Circle // --------------------------------------------------- .alert-radio-icon { - @include position($alert-md-radio-top, null, null, $alert-md-radio-left); + @include position($alert-md-radio-top, null, null, null); @include border-radius($alert-md-radio-border-radius); display: block; position: relative; + flex-shrink: 0; + width: $alert-md-radio-width; height: $alert-md-radio-height; @@ -189,6 +207,16 @@ border-color: $alert-md-radio-border-color-off; } +// The icon's inline offset gives it a gap from whichever edge the icon +// sits on, which depends on label placement +.radio-label-placement-end .alert-radio-icon { + @include position-horizontal($alert-md-radio-left, null); +} + +.radio-label-placement-start .alert-radio-icon { + @include position-horizontal(null, $alert-md-radio-left); +} + // Material Design Alert Radio Checked Dot // --------------------------------------------------- @@ -227,33 +255,46 @@ // -------------------------------------------------- .alert-checkbox-label { - @include padding( - $alert-md-checkbox-label-padding-top, - $alert-md-checkbox-label-padding-end, - $alert-md-checkbox-label-padding-bottom, - $alert-md-checkbox-label-padding-start - ); - - flex: 1; + @include padding($alert-md-checkbox-label-padding-top, null, $alert-md-checkbox-label-padding-bottom, null); // Required for the checkbox icon to stay on the screen without // being squished when the font size scales up. - width: calc(100% - $alert-md-checkbox-label-padding-start); + max-width: calc(100% - $alert-md-checkbox-width); color: $alert-md-checkbox-label-text-color; font-size: $alert-md-checkbox-label-font-size; } +.checkbox-label-placement-end .alert-checkbox-label { + /** + * The label's inline padding clears space for the icon on the icon's side. + * When the label is placed at the end, the icon is at the start, so the + * larger padding clears it on the start side. + */ + @include padding-horizontal($alert-md-checkbox-label-padding-start, $alert-md-checkbox-label-padding-end); +} + +.checkbox-label-placement-start .alert-checkbox-label { + /** + * The label's inline padding clears space for the icon on the icon's side. + * When the label is placed at the start, the icon is at the end, so the + * larger padding clears it on the end side. + */ + @include padding-horizontal($alert-md-checkbox-label-padding-end, $alert-md-checkbox-label-padding-start); +} + // Material Design Alert Checkbox Outline: Unchecked // -------------------------------------------------- .alert-checkbox-icon { - @include position($alert-md-checkbox-top, null, null, $alert-md-checkbox-left); + @include position($alert-md-checkbox-top, null, null, null); @include border-radius($alert-md-checkbox-border-radius); position: relative; + flex-shrink: 0; + width: $alert-md-checkbox-width; height: $alert-md-checkbox-height; @@ -264,6 +305,16 @@ contain: strict; } +// The icon's inline offset gives it a gap from whichever edge the icon +// sits on, which depends on label placement +.checkbox-label-placement-end .alert-checkbox-icon { + @include position-horizontal($alert-md-checkbox-left, null); +} + +.checkbox-label-placement-start .alert-checkbox-icon { + @include position-horizontal(null, $alert-md-checkbox-left); +} + // Material Design Alert Checkbox Checkmark: Checked // -------------------------------------------------- @@ -331,7 +382,7 @@ overflow: hidden; } -.alert-button-inner { +.alert-button-group .alert-button-inner { justify-content: $alert-md-button-group-justify-content; } diff --git a/core/src/components/alert/alert.md.vars.scss b/core/src/components/alert/alert.md.vars.scss index ce1fb7304dd..43458d416dd 100644 --- a/core/src/components/alert/alert.md.vars.scss +++ b/core/src/components/alert/alert.md.vars.scss @@ -34,11 +34,14 @@ $alert-md-background-color: $overlay-md-background-color; $alert-md-box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2), 0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12); +/// @prop - Padding end of the alert +$alert-md-padding-end: 24px; + /// @prop - Padding top of the alert head $alert-md-head-padding-top: 20px; /// @prop - Padding end of the alert head -$alert-md-head-padding-end: 23px; +$alert-md-head-padding-end: $alert-md-padding-end; /// @prop - Padding bottom of the alert head $alert-md-head-padding-bottom: 15px; @@ -68,7 +71,7 @@ $alert-md-sub-title-text-color: $text-color; $alert-md-message-padding-top: 20px; /// @prop - Padding end of the alert message -$alert-md-message-padding-end: 24px; +$alert-md-message-padding-end: $alert-md-padding-end; /// @prop - Padding bottom of the alert message $alert-md-message-padding-bottom: $alert-md-message-padding-top; @@ -187,11 +190,14 @@ $alert-md-list-border-top: 1px solid $alert-md-input-border-color; /// @prop - Border bottom of the alert list $alert-md-list-border-bottom: $alert-md-list-border-top; +/// @prop - Spacing between control and label +$alert-md-control-label-spacing: 32px; + /// @prop - Top of the alert radio $alert-md-radio-top: 0; /// @prop - Left of the alert radio -$alert-md-radio-left: 26px; +$alert-md-radio-left: $alert-md-padding-end; /// @prop - Width of the alert radio $alert-md-radio-width: 20px; @@ -242,13 +248,13 @@ $alert-md-radio-icon-transition: transform 280ms cubic-bezier(0.4, 0, 0.2, 1); $alert-md-radio-label-padding-top: 13px; /// @prop - Padding end on the label for the radio alert -$alert-md-radio-label-padding-end: 26px; +$alert-md-radio-label-padding-end: $alert-md-padding-end; /// @prop - Padding bottom on the label for the radio alert $alert-md-radio-label-padding-bottom: $alert-md-radio-label-padding-top; /// @prop - Padding start on the label for the radio alert -$alert-md-radio-label-padding-start: $alert-md-radio-label-padding-end + 26px; +$alert-md-radio-label-padding-start: $alert-md-radio-left + $alert-md-control-label-spacing; /// @prop - Font size of the label for the radio alert $alert-md-radio-label-font-size: dynamic-font(16px); @@ -263,7 +269,7 @@ $alert-md-radio-label-text-color-checked: $alert-md-radio-label-text-color; $alert-md-checkbox-top: 0; /// @prop - Left of the checkbox in the alert -$alert-md-checkbox-left: 26px; +$alert-md-checkbox-left: $alert-md-padding-end; /// @prop - Width of the checkbox in the alert $alert-md-checkbox-width: 16px; @@ -314,13 +320,13 @@ $alert-md-checkbox-icon-transform: rotate(45deg); $alert-md-checkbox-label-padding-top: 13px; /// @prop - Padding end of the label for the checkbox in the alert -$alert-md-checkbox-label-padding-end: $alert-md-checkbox-left; +$alert-md-checkbox-label-padding-end: $alert-md-padding-end; /// @prop - Padding bottom of the label for the checkbox in the alert $alert-md-checkbox-label-padding-bottom: $alert-md-checkbox-label-padding-top; /// @prop - Padding start of the label for the checkbox in the alert -$alert-md-checkbox-label-padding-start: $alert-md-checkbox-label-padding-end + 27px; +$alert-md-checkbox-label-padding-start: $alert-md-checkbox-left + $alert-md-control-label-spacing; /// @prop - Text color of the label for the checkbox in the alert $alert-md-checkbox-label-text-color: $text-color-step-150; diff --git a/core/src/components/alert/alert.native.scss b/core/src/components/alert/alert.native.scss index e2d5a87b8a5..b6a5ca8ef1a 100644 --- a/core/src/components/alert/alert.native.scss +++ b/core/src/components/alert/alert.native.scss @@ -1,20 +1,23 @@ -@use "../../themes/native/native.theme.default" as native; +@use "../../themes/native/native.globals" as native; @use "../../themes/mixins" as mixins; @use "../../themes/functions.font" as font; +@use "../select-option/select-option.native.overlay"; @use "./alert.common"; // Alert: Native // -------------------------------------------------- -.alert-radio-label, -.alert-checkbox-label { - gap: 12px; -} +:host { + font-family: native.$font-family-base; -.select-option-description { - @include mixins.padding(5px, 0, 0, 0); + z-index: native.$z-index-overlay; +} - color: native.$text-color-step-300; +.alert-wrapper { + z-index: native.$z-index-overlay-wrapper; +} - font-size: font.dynamic-font(12px); +.alert-button.ion-focused, +.alert-tappable.ion-focused { + background: native.$background-color-step-100; } diff --git a/core/src/components/alert/alert.tsx b/core/src/components/alert/alert.tsx index e44077d0f86..44adc065aa9 100644 --- a/core/src/components/alert/alert.tsx +++ b/core/src/components/alert/alert.tsx @@ -6,6 +6,7 @@ import { createButtonActiveGesture } from '@utils/gesture/button-active'; import { raf } from '@utils/helpers'; import { createLockController } from '@utils/lock-controller'; import { printIonWarning } from '@utils/logging'; +import { getOverlayLabelJustify, getOverlayLabelPlacement } from '@utils/overlay-control-label'; import { createDelegateController, createTriggerController, @@ -603,6 +604,8 @@ export class Alert implements ComponentInterface, OverlayInterface { endContent: richInput.endContent, description: richInput.description, }; + const defaultLabelPlacement = getOverlayLabelPlacement(theme, 'checkbox'); + const defaultJustify = getOverlayLabelJustify(theme, 'checkbox'); return ( + + + + + + + + + diff --git a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Chrome-linux.png index 5f17e5fabe5..7b5e4f5ea4d 100644 Binary files a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Firefox-linux.png index 84a81e8c938..769000f8882 100644 Binary files a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Safari-linux.png index effe36becf6..684064147ad 100644 Binary files a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Safari-linux.png and b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Chrome-linux.png index dd305e62607..ef35d402577 100644 Binary files a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Firefox-linux.png index ee5173b6a18..65e59390b54 100644 Binary files a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Safari-linux.png index 8811a5009a2..664339ba322 100644 Binary files a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Safari-linux.png and b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts b/core/src/components/alert/test/states/alert.e2e.ts new file mode 100644 index 00000000000..95f51290a51 --- /dev/null +++ b/core/src/components/alert/test/states/alert.e2e.ts @@ -0,0 +1,60 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +/** + * This behavior does not vary across directions. + */ +configs({ directions: ['ltr'], modes: ['ios', 'md', 'ionic-md'] }).forEach(({ config, screenshot, title }) => { + test.describe(title('alert: input states'), () => { + /** + * `(any-hover: hover)` evaluates to "none" in all three mobile-emulated + * projects, suppressing the hover rules: + * + * - Chromium and WebKit suppress it because of hasTouch and isMobile. + * - Headless Firefox doesn't detect input devices and reports no hover + * capability regardless of context options, so override it via + * launchOptions.firefoxUserPrefs. Bit values: 4 = FINE (mouse), + * 8 = HOVER, 12 = FINE | HOVER. + * + * Viewport, userAgent, and scale factor remain mobile-sized. + */ + test.use({ + hasTouch: false, + isMobile: false, + }); + + test.beforeEach(async ({ page }) => { + await page.goto(`/src/components/alert/test/states`, config); + }); + + test('should render all radio states', async ({ page }) => { + const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent'); + await page.locator('#radio').click(); + await ionAlertDidPresent.next(); + + const alert = page.locator('ion-alert'); + await expect(alert).toBeVisible(); + + const defaultRadio = alert.locator('button.alert-radio-button').first(); + await defaultRadio.hover(); + + await expect(alert).toHaveScreenshot(screenshot('alert-radio-states')); + }); + + test('should render all checkbox states', async ({ page }) => { + const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent'); + await page.locator('#checkbox').click(); + await ionAlertDidPresent.next(); + + const alert = page.locator('ion-alert'); + await expect(alert).toBeVisible(); + + const defaultCheckbox = alert.locator('button.alert-checkbox-button').first(); + await defaultCheckbox.hover(); + + await page.waitForChanges(); + + await expect(alert).toHaveScreenshot(screenshot('alert-checkbox-states')); + }); + }); +}); diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..dbc003fe008 Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..698434e695b Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..7d2dbd4a116 Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..d1c9a799e3d Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..cd1503d5798 Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ios-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..061da6fa38b Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..0068646af36 Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..24a998b57a0 Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..3a4180d8cb6 Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..30fc35b9cfc Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..6b9660d24b9 Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..080fd8605ad Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..1b509e947d5 Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..e6ebfbdee08 Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..f3cadc60546 Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..276ea72ed09 Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..1cdcf98e3b1 Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..3ff1ea6eeb1 Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/states/index.html b/core/src/components/alert/test/states/index.html new file mode 100644 index 00000000000..4591962127a --- /dev/null +++ b/core/src/components/alert/test/states/index.html @@ -0,0 +1,159 @@ + + + + + Alert - States + + + + + + + + + + + + + + Alert - States + + + + + + + + + + + + diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png index 5769b2e860e..09c93ab3ca7 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ionic-md-ltr-light-Mobile-Firefox-linux.png index 9a1dbee2541..ae012f8623d 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ionic-md-ltr-light-Mobile-Firefox-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ionic-md-ltr-light-Mobile-Safari-linux.png index 176bcb51146..aea4148d201 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ionic-md-ltr-light-Mobile-Safari-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/item/item.ionic.scss b/core/src/components/item/item.ionic.scss index 665f5346c69..107480559a0 100644 --- a/core/src/components/item/item.ionic.scss +++ b/core/src/components/item/item.ionic.scss @@ -6,7 +6,8 @@ :host { --background: #{globals.$ion-bg-surface-default}; - --background-activated: #{globals.$ion-bg-select-default}; + --background-activated: #{globals.$ion-bg-neutral-subtlest-press}; + --background-activated-opacity: 1; --border-color: #{globals.$ion-primitives-neutral-300}; --border-style: #{globals.$ion-border-style-solid}; --border-width: #{0 0 globals.$ion-border-size-025 0}; @@ -62,24 +63,20 @@ slot[name="end"]::slotted(*) { @include globals.disabled-state(); } -// Item: Activated -// -------------------------------------------------- - -:host(.ion-activated) .item-native { - background: var(--background-activated); -} - // Item: Focused // -------------------------------------------------- :host(.ion-focused) .item-native::after { @include globals.border-radius(inherit); @include globals.position(0, 0, 0, 0); + @include globals.focused-state( + globals.$ion-border-size-050, + globals.$ion-border-style-solid, + globals.$ion-border-focus-default + ); position: absolute; - border-width: globals.$ion-border-size-050; - border-style: globals.$ion-border-style-solid; - border-color: globals.$ion-border-focus-default; + outline-offset: calc(globals.$ion-border-size-050 * -1); content: ""; } @@ -111,14 +108,3 @@ slot[name="end"]::slotted(*) { :host(.item-lines-none) { --inner-border-width: #{globals.$ion-border-size-0}; } - -// Item in Select Modal -// -------------------------------------------------- -:host(.in-select-modal) { - --background-focused: #{globals.$ion-bg-neutral-subtlest-press}; - --background-focused-opacity: 0; -} - -:host(.in-select-modal.ion-focused) .item-native { - --border-radius: #{globals.$ion-border-radius-400}; -} diff --git a/core/src/components/item/item.tsx b/core/src/components/item/item.tsx index 65d5d949260..3af534b4d50 100644 --- a/core/src/components/item/item.tsx +++ b/core/src/components/item/item.tsx @@ -415,7 +415,6 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac 'item-control-needs-pointer-cursor': firstInteractiveNeedsPointerCursor, 'item-disabled': disabled, 'in-list': inList, - 'in-select-modal': hostContext('ion-select-modal', this.el), 'item-multiple-inputs': this.multipleInputs, 'ion-activatable': canActivate, 'ion-focusable': this.focusable, diff --git a/core/src/components/select-modal/select-modal.common.scss b/core/src/components/select-modal/select-modal.common.scss index 3bbb48b557d..1cd115ba602 100644 --- a/core/src/components/select-modal/select-modal.common.scss +++ b/core/src/components/select-modal/select-modal.common.scss @@ -1,19 +1,8 @@ +@use "../select-option/select-option.common.overlay"; + // Select Modal // -------------------------------------------------- :host { height: 100%; } - -// Select Modal: Select Option -// -------------------------------------------------- - -.select-option-label { - display: flex; - - align-items: center; -} - -.select-option-description { - display: block; -} diff --git a/core/src/components/select-modal/select-modal.ionic.scss b/core/src/components/select-modal/select-modal.ionic.scss index ca137a075d3..51b215ae88a 100644 --- a/core/src/components/select-modal/select-modal.ionic.scss +++ b/core/src/components/select-modal/select-modal.ionic.scss @@ -1,4 +1,5 @@ @use "../../themes/ionic/ionic.globals.scss" as globals; +@use "../select-option/select-option.ionic.overlay"; @use "./select-modal.common"; // Ionic Select Modal @@ -12,12 +13,26 @@ // ---------------------------------------------------------------- ion-item { - --border-width: 0; + --border-width: #{globals.$ion-border-size-0}; + --background-focused: transparent; + --background-focused-opacity: 1; + --background-hover: #{globals.$ion-bg-neutral-subtlest-press}; + --background-hover-opacity: 1; + --background-activated: #{globals.$ion-bg-neutral-subtlest-press}; + --background-activated-opacity: 1; + --border-radius: #{globals.$ion-border-radius-400}; } +// TODO(): Remove this when the focus styles are added back to the interface ion-item.ion-focused::part(native)::after { // Your styles for the ::after pseudo element when ion-item is focused - border: none; + outline: none; +} + +ion-item.ion-focused.item-checkbox-checked, +ion-item.ion-focused.item-radio-checked { + --background-focused: #{globals.$ion-bg-primary-subtle-default}; + --background-focused-opacity: 1; } // Toolbar @@ -35,7 +50,7 @@ ion-list ion-radio::part(container) { } ion-list ion-radio::part(label) { - @include globals.margin(0); + @include globals.margin(globals.$ion-space-0); } // Radio and Checkbox: Label @@ -51,8 +66,7 @@ ion-list ion-checkbox::part(label) { .item-radio-checked, .item-checkbox-checked { - --background: #{globals.$ion-semantics-primary-100}; - --border-radius: #{globals.$ion-border-radius-400}; + --background: #{globals.$ion-bg-primary-subtle-default}; } // Content @@ -71,24 +85,4 @@ ion-content { --padding-end: #{globals.$ion-space-400} !important; /* stylelint-disable-next-line declaration-no-important */ --padding-bottom: #{globals.$ion-space-1200} !important; - - // Set the background to the focused element within a radio group only when there is a checked radio - &:has(.radio-checked) .ion-focused:not(.item-radio-checked) { - --background-focused-opacity: 1; - } -} - -// Select Modal: Select Option -// -------------------------------------------------- - -.select-option-label { - gap: globals.$ion-space-300; -} - -.select-option-description { - @include globals.typography(globals.$ion-body-md-regular); - - color: globals.$ion-text-subtle; - - font-size: globals.$ion-font-size-350; } diff --git a/core/src/components/select-modal/select-modal.ios.scss b/core/src/components/select-modal/select-modal.ios.scss index abac9c8220b..07becc9d2e3 100644 --- a/core/src/components/select-modal/select-modal.ios.scss +++ b/core/src/components/select-modal/select-modal.ios.scss @@ -1,3 +1,4 @@ +@use "../select-option/select-option.ios.overlay"; @import "./select-modal.native"; @import "../item/item.ios.vars"; @import "../radio/radio.ios.vars"; diff --git a/core/src/components/select-modal/select-modal.md.scss b/core/src/components/select-modal/select-modal.md.scss index 260f6aba5be..1cb08cca77c 100644 --- a/core/src/components/select-modal/select-modal.md.scss +++ b/core/src/components/select-modal/select-modal.md.scss @@ -1,3 +1,4 @@ +@use "../select-option/select-option.md.overlay"; @import "./select-modal.native"; @import "../../themes/mixins.scss"; @import "../item/item.md.vars"; diff --git a/core/src/components/select-modal/select-modal.native.scss b/core/src/components/select-modal/select-modal.native.scss index 29b81819fcf..1c030fcfc38 100644 --- a/core/src/components/select-modal/select-modal.native.scss +++ b/core/src/components/select-modal/select-modal.native.scss @@ -1,19 +1,8 @@ @use "../../themes/native/native.theme.default" as native; @use "../../themes/mixins" as mixins; @use "../../themes/functions.font" as font; +@use "../select-option/select-option.native.overlay"; @use "./select-modal.common"; // Select Modal: Native // -------------------------------------------------- - -.select-option-label { - gap: 12px; -} - -.select-option-description { - @include mixins.padding(5px, 0, 0, 0); - - color: native.$text-color-step-300; - - font-size: font.dynamic-font(12px); -} diff --git a/core/src/components/select-modal/select-modal.tsx b/core/src/components/select-modal/select-modal.tsx index ca21e73a1f0..561ba79c6cc 100644 --- a/core/src/components/select-modal/select-modal.tsx +++ b/core/src/components/select-modal/select-modal.tsx @@ -2,6 +2,7 @@ import { getIonMode, getIonTheme } from '@global/ionic-global'; import xRegular from '@phosphor-icons/core/assets/regular/x.svg'; import type { ComponentInterface } from '@stencil/core'; import { Component, Element, Host, Prop, forceUpdate, h } from '@stencil/core'; +import { getOverlayLabelJustify, getOverlayLabelPlacement } from '@utils/overlay-control-label'; import { safeCall } from '@utils/overlays'; import { renderOptionLabel } from '@utils/select-option-render'; import { getClassMap, hostContext } from '@utils/theme'; @@ -115,6 +116,7 @@ export class SelectModal implements ComponentInterface { } private renderRadioOptions() { + const theme = getIonTheme(this); const checked = this.options.filter((o) => o.checked).map((o) => o.value)[0]; return ( @@ -127,6 +129,7 @@ export class SelectModal implements ComponentInterface { * part of the public `SelectModalOption` interface. */ const richOption = option as SelectOverlayOption; + const hasRichContent = !!richOption.startContent || !!richOption.endContent || !!richOption.description; const optionLabelOptions = { id: `modal-option-${index}`, label: richOption.text, @@ -134,10 +137,14 @@ export class SelectModal implements ComponentInterface { endContent: richOption.endContent, description: richOption.description, }; + const defaultLabelPlacement = getOverlayLabelPlacement(theme, 'radio', 'modal'); + const defaultJustify = getOverlayLabelJustify(theme, 'radio', 'modal'); return ( this.closeModal()} onKeyDown={(ev) => { if (ev.key === 'Enter' && !ev.repeat) { @@ -178,6 +188,7 @@ export class SelectModal implements ComponentInterface { } private renderCheckboxOptions() { + const theme = getIonTheme(this); return this.options.map((option, index) => { /** * Cast to `SelectOverlayOption` to access rich content @@ -186,6 +197,7 @@ export class SelectModal implements ComponentInterface { * part of the public `SelectModalOption` interface. */ const richOption = option as SelectOverlayOption; + const hasRichContent = !!richOption.startContent || !!richOption.endContent || !!richOption.description; const optionLabelOptions = { id: `modal-option-${index}`, label: richOption.text, @@ -193,9 +205,13 @@ export class SelectModal implements ComponentInterface { endContent: richOption.endContent, description: richOption.description, }; + const defaultLabelPlacement = getOverlayLabelPlacement(theme, 'checkbox', 'modal'); + const defaultJustify = getOverlayLabelJustify(theme, 'checkbox', 'modal'); return ( { this.setChecked(ev); this.callOptionHandler(ev); diff --git a/core/src/components/select-modal/test/basic/index.html b/core/src/components/select-modal/test/basic/index.html index 3e0ac7eec55..52e13a10462 100644 --- a/core/src/components/select-modal/test/basic/index.html +++ b/core/src/components/select-modal/test/basic/index.html @@ -2,7 +2,7 @@ - Select - Modal + Select Modal - Basic - + Cancel Text (default) - + diff --git a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png index 743aa408579..232b34daf60 100644 Binary files a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png and b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png index c47f13cde4f..0422fbc34ed 100644 Binary files a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png and b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ionic-md-ltr-light-Mobile-Firefox-linux.png index 9627de08483..c0e99c6bb5e 100644 Binary files a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ionic-md-ltr-light-Mobile-Firefox-linux.png and b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ionic-md-ltr-light-Mobile-Safari-linux.png index 3e30a54b205..4172d823b97 100644 Binary files a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ionic-md-ltr-light-Mobile-Safari-linux.png and b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png index eafac1f4249..d355a57b2fc 100644 Binary files a/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png and b/core/src/components/select-modal/test/custom/select-modal.e2e.ts-snapshots/select-modal-cancel-icon-diff-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-modal/test/states/index.html b/core/src/components/select-modal/test/states/index.html new file mode 100644 index 00000000000..5bd4553a7c9 --- /dev/null +++ b/core/src/components/select-modal/test/states/index.html @@ -0,0 +1,106 @@ + + + + + Select Modal - States + + + + + + + + + + + + + Select Modal - States + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts b/core/src/components/select-modal/test/states/select-modal.e2e.ts new file mode 100644 index 00000000000..6a32f78172b --- /dev/null +++ b/core/src/components/select-modal/test/states/select-modal.e2e.ts @@ -0,0 +1,80 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +/** + * This behavior does not vary across directions. + */ +configs({ directions: ['ltr'], modes: ['ios', 'md', 'ionic-md'] }).forEach(({ config, screenshot, title }) => { + test.describe(title('select-modal: states'), () => { + /** + * `(any-hover: hover)` evaluates to "none" in all three mobile-emulated + * projects, suppressing the hover rules: + * + * - Chromium and WebKit suppress it because of hasTouch and isMobile. + * - Headless Firefox doesn't detect input devices and reports no hover + * capability regardless of context options, so override it via + * launchOptions.firefoxUserPrefs. Bit values: 4 = FINE (mouse), + * 8 = HOVER, 12 = FINE | HOVER. + * + * Viewport, userAgent, and scale factor remain mobile-sized. + */ + test.use({ + hasTouch: false, + isMobile: false, + }); + + test.beforeEach(async ({ page }) => { + await page.goto(`/src/components/select-modal/test/states`, config); + }); + + test('should render all radio states', async ({ page }) => { + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + await page.locator('#single').click(); + await ionModalDidPresent.next(); + + const modal = page.locator('#modal-single'); + const selectModal = modal.locator('ion-select-modal'); + await expect(selectModal).toBeVisible(); + + /** + * After clicking the trigger button, the cursor sits at the + * button's screen coordinates — which may coincide with the + * "Default" row once the modal opens, depending on mode/viewport. + * Without a transition, `mouseenter` doesn't fire and the JS-driven + * label swap never runs, causing inconsistent hover states in the + * screenshots. + */ + await page.mouse.move(0, 0); + + const defaultRow = selectModal.locator('ion-item').first(); + await defaultRow.hover(); + + await expect(selectModal).toHaveScreenshot(screenshot('select-modal-radio-states')); + }); + + test('should render all checkbox states', async ({ page }) => { + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + await page.locator('#multiple').click(); + await ionModalDidPresent.next(); + + const modal = page.locator('#modal-multiple'); + const selectModal = modal.locator('ion-select-modal'); + await expect(selectModal).toBeVisible(); + + /** + * After clicking the trigger button, the cursor sits at the + * button's screen coordinates — which may coincide with the + * "Default" row once the modal opens, depending on mode/viewport. + * Without a transition, `mouseenter` doesn't fire and the JS-driven + * label swap never runs, causing inconsistent hover states in the + * screenshots. + */ + await page.mouse.move(0, 0); + + const defaultRow = selectModal.locator('ion-item').first(); + await defaultRow.hover(); + + await expect(selectModal).toHaveScreenshot(screenshot('select-modal-checkbox-states')); + }); + }); +}); diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..2db82f13b83 Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..bc16961c5ff Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..a8089b419fa Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..fd1716eb2db Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..1a4744a1dc9 Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..7bbc80f2147 Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..e071c2f6b99 Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..4dd93626894 Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..0fec86f8133 Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..23d9800a4ec Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..38c83ed8738 Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..72a251813f5 Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..5905048cdd4 Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..9eec3e12894 Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..53fb60527bb Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..f68735e8dd6 Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..5e135f73220 Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..61a79657dd1 Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/select-option.common.overlay.scss b/core/src/components/select-option/select-option.common.overlay.scss new file mode 100644 index 00000000000..166da5884b9 --- /dev/null +++ b/core/src/components/select-option/select-option.common.overlay.scss @@ -0,0 +1,83 @@ +// Select Option - Overlay +// -------------------------------------------------- + +// Outer label container, which also spaces the start +// and end slots when a select option has rich content +.action-sheet-button-label-has-rich-content, +.alert-radio-label-has-rich-content, +.alert-checkbox-label-has-rich-content, +.select-option-label-has-rich-content { + display: flex; + + align-items: center; +} + +/** + * Outer label container has rich content + * (start, content, description, end) that needs the + * label to span the available row width. + */ +.action-sheet-button-label-has-rich-content, +.alert-radio-label-has-rich-content, +.alert-checkbox-label-has-rich-content, +.select-option-content { + flex: 1; +} + +// Inner label container of a select option when +// there is rich content within the default slot +.action-sheet-button-label-text, +.alert-checkbox-label-text, +.alert-radio-label-text, +.select-option-label-text { + display: flex; + + align-items: center; +} + +// Start and end slots +.select-option-start, +.select-option-end { + display: flex; + + align-items: center; +} + +.select-option-description { + display: block; +} + +// Select Option: Select Modal / Select Popover +// -------------------------------------------------- + +/** + * Non-rich labels are plain text and should ellipsize when they + * overflow the row. Rich-content labels switch to flex so the + * start / content / end pieces can lay out side-by-side and wrap. + */ +.select-option-label:not(.select-option-label-has-rich-content) { + text-overflow: ellipsis; + + white-space: nowrap; + + overflow: hidden; +} + +.select-option-label-has-rich-content { + display: flex; + + align-items: center; +} + +ion-radio.select-option-has-rich-content::part(label), +ion-checkbox.select-option-has-rich-content::part(label), +.select-option-content { + flex: 1; + + /** + * Let rich content wrap instead of inheriting the label part's + * single-line truncation, so arbitrary slotted elements render + * without clipping. + */ + white-space: normal; +} diff --git a/core/src/components/select-option/select-option.ionic.overlay.scss b/core/src/components/select-option/select-option.ionic.overlay.scss new file mode 100644 index 00000000000..746ec00594e --- /dev/null +++ b/core/src/components/select-option/select-option.ionic.overlay.scss @@ -0,0 +1,60 @@ +@use "../../themes/ionic/ionic.globals.scss" as globals; + +// Select Option - Interface +// -------------------------------------------------- + +// Outer label container, which also spaces the start +// and end slots when a select option has rich content +.action-sheet-button-label-has-rich-content, +.alert-radio-label-has-rich-content, +.alert-checkbox-label-has-rich-content, +.select-option-label-has-rich-content { + gap: globals.$ion-space-300; +} + +// Inner label container of a select option when +// there is rich content within the default slot +.action-sheet-button-label-text, +.alert-checkbox-label-text, +.alert-radio-label-text, +.select-option-label-text { + gap: globals.$ion-space-300; +} + +.select-option-description { + @include globals.typography(globals.$ion-body-md-regular); + @include globals.padding(globals.$ion-space-0); + + color: globals.$ion-text-subtle; + + font-size: globals.$ion-font-size-350; +} + +// Start and end slots +.select-option-start, +.select-option-end { + gap: globals.$ion-space-200; +} + +/** + * Cap slotted children so they can't stretch the option + * row out of proportion, keeping rows visually uniform + * regardless of the content. + */ + +// Avatar / Image / SVG / Thumbnail / Icon +.select-option-start > ion-avatar, +.select-option-start > ion-img, +.select-option-start > ion-thumbnail, +.select-option-start > ion-icon, +.select-option-start > img, +.select-option-start > svg, +.select-option-end > ion-avatar, +.select-option-end > ion-img, +.select-option-end > ion-thumbnail, +.select-option-end > ion-icon, +.select-option-end > img, +.select-option-end > svg { + width: globals.$ion-scale-1200; + height: globals.$ion-scale-1200; +} diff --git a/core/src/components/select-option/select-option.ios.overlay.scss b/core/src/components/select-option/select-option.ios.overlay.scss new file mode 100644 index 00000000000..da95dd05fc3 --- /dev/null +++ b/core/src/components/select-option/select-option.ios.overlay.scss @@ -0,0 +1,47 @@ +@use "./select-option.native.overlay"; +@use "../../themes/mixins" as mixins; +@use "../item/item.ios.vars" as item; + +// Select Option - Interface +// -------------------------------------------------- + +/** + * Cap slotted children so they can't stretch the option + * row out of proportion, keeping rows visually uniform + * regardless of the content. + */ + +// Avatar / Image / SVG / Thumbnail +.select-option-start > ion-avatar, +.select-option-start > ion-img, +.select-option-start > ion-thumbnail, +.select-option-start > img, +.select-option-start > svg, +.select-option-end > ion-avatar, +.select-option-end > ion-img, +.select-option-end > ion-thumbnail, +.select-option-end > img, +.select-option-end > svg { + width: 44px; + height: 44px; +} + +// Icon +.select-option-start > ion-icon, +.select-option-end > ion-icon { + width: 28px; + height: 28px; +} + +// Select Option: Action Sheet +// -------------------------------------------------- +.action-sheet-button-label-text { + justify-content: center; +} + +// Select Option: Select Modal +// -------------------------------------------------- + +.select-option-has-rich-content { + @include mixins.padding-horizontal(null, item.$item-ios-padding-end); +} diff --git a/core/src/components/select-option/select-option.md.overlay.scss b/core/src/components/select-option/select-option.md.overlay.scss new file mode 100644 index 00000000000..ba72e243f39 --- /dev/null +++ b/core/src/components/select-option/select-option.md.overlay.scss @@ -0,0 +1,44 @@ +@use "./select-option.native.overlay"; + +// Select Option - Interface +// -------------------------------------------------- + +/** + * Cap slotted children so they can't stretch the option + * row out of proportion, keeping rows visually uniform + * regardless of the content. + */ + +// Avatar +.select-option-start > ion-avatar, +.select-option-end > ion-avatar { + width: 40px; + height: 40px; +} + +// Icon +.select-option-start > ion-icon, +.select-option-end > ion-icon { + width: 24px; + height: 24px; +} + +// Image / SVG / Thumbnail +.select-option-start > ion-img, +.select-option-start > img, +.select-option-start > svg, +.select-option-start > ion-thumbnail, +.select-option-end > ion-img, +.select-option-end > img, +.select-option-end > svg, +.select-option-end > ion-thumbnail { + width: 56px; + height: 56px; +} + +// Video +.select-option-start > video, +.select-option-end > video { + width: 114px; + height: 56px; +} diff --git a/core/src/components/select-option/select-option.native.overlay.scss b/core/src/components/select-option/select-option.native.overlay.scss new file mode 100644 index 00000000000..e2311843644 --- /dev/null +++ b/core/src/components/select-option/select-option.native.overlay.scss @@ -0,0 +1,38 @@ +@use "../../themes/native/native.globals" as native; +@use "../../themes/mixins" as mixins; +@use "../../themes/functions.font" as font; + +// Select Option - Interface +// -------------------------------------------------- + +// Outer label container, which also spaces the start +// and end slots when a select option has rich content +.action-sheet-button-label-has-rich-content, +.alert-radio-label-has-rich-content, +.alert-checkbox-label-has-rich-content, +.select-option-label-has-rich-content { + gap: 16px; +} + +// Inner label container of a select option when +// there is rich content within the default slot +.action-sheet-button-label-text, +.alert-checkbox-label-text, +.alert-radio-label-text, +.select-option-label-text { + gap: 12px; +} + +// Start and end slots +.select-option-start, +.select-option-end { + gap: 8px; +} + +.select-option-description { + @include mixins.padding(5px, 0, 0, 0); + + color: native.$text-color-step-300; + + font-size: font.dynamic-font(12px); +} diff --git a/core/src/components/select-option/select-option.scss b/core/src/components/select-option/select-option.scss index cc36f78bfa0..b166e375c82 100644 --- a/core/src/components/select-option/select-option.scss +++ b/core/src/components/select-option/select-option.scss @@ -1,3 +1,6 @@ +// Select Option +// -------------------------------------------------- + :host { display: none; } diff --git a/core/src/components/select-option/select-option.tsx b/core/src/components/select-option/select-option.tsx index dba97e56c47..a00d98c3bce 100644 --- a/core/src/components/select-option/select-option.tsx +++ b/core/src/components/select-option/select-option.tsx @@ -36,6 +36,40 @@ export class SelectOption implements ComponentInterface { */ @Prop() description?: string; + /** + * Where the label is placed relative to the option's selection control + * (radio circle or checkbox box) when the option is rendered in an + * `alert`, `popover`, or `modal` interface. + * `"start"`: The label will appear to the left of the radio in LTR and to the right in RTL. + * `"end"`: The label will appear to the right of the radio in LTR and to the left in RTL. + * + * Applies to the `alert`, `popover`, and `modal` interfaces, but has no + * visible effect on radio options in `popover` or `modal` on the `md` + * and `ionic` themes (the radio control is hidden there). + * + * When unset, the interface picks a default based on theme and control + * type. + */ + @Prop() labelPlacement?: 'start' | 'end'; + + /** + * How to pack the label and the option's selection control within a line. + * `"start"`: The label and radio will appear on the left in LTR and + * on the right in RTL. + * `"end"`: The label and radio will appear on the right in LTR and + * on the left in RTL. + * `"space-between"`: The label and radio will appear on opposite + * ends of the line with space between the two elements. + * + * Applies to the `alert`, `popover`, and `modal` interfaces, but has no + * visible effect on radio options in `popover` or `modal` on the `md` + * and `ionic` themes (the radio control is hidden there). + * + * When unset, the interface picks a default based on theme and control + * type. + */ + @Prop() justify?: 'start' | 'end' | 'space-between'; + render() { const theme = getIonTheme(this); diff --git a/core/src/components/select-option/test/label-placement/index.html b/core/src/components/select-option/test/label-placement/index.html new file mode 100644 index 00000000000..fdd0d235b5b --- /dev/null +++ b/core/src/components/select-option/test/label-placement/index.html @@ -0,0 +1,68 @@ + + + + + Select Option - Label Placement + + + + + + + + + + + + + Select Option - Label Placement + + + + + + + + + + + diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts b/core/src/components/select-option/test/label-placement/select-option.e2e.ts new file mode 100644 index 00000000000..8bea9a13d07 --- /dev/null +++ b/core/src/components/select-option/test/label-placement/select-option.e2e.ts @@ -0,0 +1,79 @@ +import { expect } from '@playwright/test'; +import type { Page } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +/** + * iOS does not respect the viewport so styles must be updated instead. + */ +const ALERT_SIZE_OVERRIDES = ` + ion-alert { + --max-width: 560px !important; + --max-height: none !important; + } + ion-alert .alert-radio-group, + ion-alert .alert-checkbox-group { + max-height: none !important; + } +`; + +const INTERFACES = [ + { name: 'alert', presentEvent: 'ionAlertDidPresent', locator: 'ion-alert .alert-wrapper' }, + { name: 'popover', presentEvent: 'ionPopoverDidPresent', locator: 'ion-popover' }, + { name: 'modal', presentEvent: 'ionModalDidPresent', locator: 'ion-modal' }, +] as const; + +const JUSTIFY_VARIANTS = ['start', 'end', 'space-between'] as const; + +const LABEL_PLACEMENTS = ['start', 'end'] as const; + +const FIRST_OPTION_VALUE = `${JUSTIFY_VARIANTS[0]}-short`; + +const renderOptions = (labelPlacement: 'start' | 'end') => + JUSTIFY_VARIANTS.flatMap((justify) => { + const longLabel = `Justify ${justify} — ${'long label '.repeat(6).trim()}`; + return [ + `Justify ${justify}`, + `${longLabel}`, + ]; + }).join(''); + +const setContentForInterface = async ( + page: Page, + interfaceName: 'alert' | 'popover' | 'modal', + labelPlacement: 'start' | 'end', + config: object +) => { + await page.setContent( + ` + + ${renderOptions(labelPlacement)} + + `, + config + ); +}; + +configs({ modes: ['md', 'ios', 'ionic-md'] }).forEach(({ config, screenshot, title }) => { + test.describe(title('select-option: label placement'), () => { + for (const { name, presentEvent, locator } of INTERFACES) { + test.describe(`${name} interface`, () => { + for (const placement of LABEL_PLACEMENTS) { + test(`placement ${placement}`, async ({ page }) => { + await setContentForInterface(page, name, placement, config); + + if (name === 'alert') { + await page.addStyleTag({ content: ALERT_SIZE_OVERRIDES }); + } + + const didPresent = await page.spyOnEvent(presentEvent); + await page.locator('#select').click(); + await didPresent.next(); + + const overlay = page.locator(locator); + await expect(overlay).toHaveScreenshot(screenshot(`select-option-label-${name}-${placement}`)); + }); + } + }); + } + }); +}); diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..d07357be7ed Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..b69f2dad1cd Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..65c66097172 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-rtl-light-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-rtl-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..ebbd5fbba85 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-rtl-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-rtl-light-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-rtl-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..0ab39b75963 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-rtl-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-rtl-light-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-rtl-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..7864ab72cb3 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ionic-md-rtl-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..9317780e3a2 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..00c275ac24e Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..0580de0c5c1 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..6e1f40168c6 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..ffbab7c8919 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Safari-linux.png new file mode 100644 index 00000000000..d354dd4272c Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..46cfe39151f Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..412b6469f4f Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..42af1a6caf9 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..27763acc007 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..7dcfbd9826b Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Safari-linux.png new file mode 100644 index 00000000000..08b9f1a73e7 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..d6195184ad3 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..0070f2a4194 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..2d081db8ba0 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-rtl-light-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-rtl-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..1bb49038732 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-rtl-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-rtl-light-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-rtl-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..9f877d92266 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-rtl-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-rtl-light-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-rtl-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..9010cc2e1fb Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ionic-md-rtl-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..56c16bba9f2 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..a0b8449914a Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..ec69762dc4d Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..3bda3556f65 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..30e6a5de2a2 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Safari-linux.png new file mode 100644 index 00000000000..14248f7206f Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..6bcf0597a7b Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..5985dde3e94 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..065789037e3 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..28e4b16fc74 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..57392ec7794 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Safari-linux.png new file mode 100644 index 00000000000..20924f0ee21 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..ca74cba615c Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..c8f9e1d4cab Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..ed6aec886d1 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-rtl-light-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-rtl-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..2ef9ee8c860 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-rtl-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-rtl-light-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-rtl-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..c7c46faa4c5 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-rtl-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-rtl-light-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-rtl-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..b6759ef50cd Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ionic-md-rtl-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..821d991a870 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..6713610f1ef Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..baa2f3680d2 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..99ba9258b12 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..bad88595e54 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Safari-linux.png new file mode 100644 index 00000000000..e4488119066 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..b38d5b82126 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..7a8638bfd41 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..d5968bcfac1 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..aeb3b7cd841 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..a3b237c4410 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Safari-linux.png new file mode 100644 index 00000000000..c977a6dbb3b Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..ca74cba615c Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..c8f9e1d4cab Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..ed6aec886d1 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-rtl-light-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-rtl-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..2ef9ee8c860 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-rtl-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-rtl-light-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-rtl-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..c7c46faa4c5 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-rtl-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-rtl-light-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-rtl-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..b6759ef50cd Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ionic-md-rtl-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..84153c6f41a Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..f197c5c4514 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..db4030312a0 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..b22bdfdeaea Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..720116cf1cb Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Safari-linux.png new file mode 100644 index 00000000000..4a84e90bcdd Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..c36791eb1c2 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..540d563377c Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..722b846dc1e Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..e3fb8a2ed88 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..03d30774c67 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Safari-linux.png new file mode 100644 index 00000000000..ac996d14943 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..1abf12cee2a Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..4748b709611 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..aa8ab2242a1 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-rtl-light-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-rtl-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..3414ef156d3 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-rtl-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-rtl-light-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-rtl-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..3b58e6dd1f1 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-rtl-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-rtl-light-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-rtl-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..08bbb64977b Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ionic-md-rtl-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..f19ee15e627 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..57b91450e0d Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..cf8c1533aae Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..49f1ab849f4 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..496eea2a8bd Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Safari-linux.png new file mode 100644 index 00000000000..aa70f017281 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..4301e63f0e0 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..d132894239e Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..9c035e71043 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..bb6b8421da9 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..79bd12dd0c1 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Safari-linux.png new file mode 100644 index 00000000000..923a4ca22f8 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..e89d403830d Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..abda0ed0f19 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..5308e6a239a Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-rtl-light-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-rtl-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..5c7236fac19 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-rtl-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-rtl-light-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-rtl-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..ed6b8f13ddd Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-rtl-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-rtl-light-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-rtl-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..7a6e83efaa7 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ionic-md-rtl-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..510859f2a22 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..6c3b7c4f0e7 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..779044231ee Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..27c11d3a495 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..5f3d1540166 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Safari-linux.png new file mode 100644 index 00000000000..6bf85e0399e Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..d5190455b7c Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..ae282fc5ae6 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..855f16724b1 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..1b871d28990 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..425a6284131 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Safari-linux.png new file mode 100644 index 00000000000..cda4837fcc7 Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-popover/select-popover.common.scss b/core/src/components/select-popover/select-popover.common.scss index 095b6660f35..02f70aaf5e2 100644 --- a/core/src/components/select-popover/select-popover.common.scss +++ b/core/src/components/select-popover/select-popover.common.scss @@ -1,15 +1,16 @@ -@import "../../themes/native/native.globals"; +@use "../../themes/mixins" as mixins; +@use "../select-option/select-option.common.overlay"; // Select Popover // -------------------------------------------------- :host ion-list { - @include margin(0); + @include mixins.margin(0); } ion-list-header, ion-label { - @include margin(0); + @include mixins.margin(0); } /** @@ -21,22 +22,3 @@ ion-label { :host { overflow-y: auto; } - -// Select Popover: Select Option -// -------------------------------------------------- - -.select-option-label { - display: flex; - - align-items: center; -} - -.select-option-label-text { - display: flex; - - flex-wrap: wrap; -} - -.select-option-description { - display: block; -} diff --git a/core/src/components/select-popover/select-popover.ionic.scss b/core/src/components/select-popover/select-popover.ionic.scss index 1813794975d..e194c06a0d6 100644 --- a/core/src/components/select-popover/select-popover.ionic.scss +++ b/core/src/components/select-popover/select-popover.ionic.scss @@ -1,22 +1,50 @@ @use "../../themes/ionic/ionic.globals.scss" as globals; +@use "../../themes/mixins" as mixins; +@use "../select-option/select-option.ionic.overlay"; @use "./select-popover.common"; -@use "./select-popover.md" as select-popover-md; // Ionic Select Popover // -------------------------------------------------- -// Select Popover: Select Option -// -------------------------------------------------- +ion-item { + --border-width: #{globals.$ion-border-size-0}; + --background-focused: transparent; + --background-focused-opacity: 1; + --background-hover: #{globals.$ion-bg-neutral-subtlest-press}; + --background-hover-opacity: 1; + --background-activated: #{globals.$ion-bg-neutral-subtlest-press}; + --background-activated-opacity: 1; +} + +ion-item.ion-focused.item-checkbox-checked, +ion-item.ion-focused.item-radio-checked { + --background-focused: #{globals.$ion-bg-primary-subtle-default}; + --background-focused-opacity: 1; +} + +// Radio +// ---------------------------------------------------------------- -.select-option-label { - gap: globals.$ion-space-300; +ion-list ion-radio::part(container) { + display: none; } -.select-option-description { - @include globals.typography(globals.$ion-body-md-regular); - @include globals.padding(0); +ion-list ion-radio::part(label) { + @include mixins.margin(globals.$ion-space-0); +} + +// Radio and Checkbox: Label +// ---------------------------------------------------------------- + +ion-list ion-radio::part(label), +ion-list ion-checkbox::part(label) { + @include globals.typography(globals.$ion-body-lg-medium); +} - color: globals.$ion-text-subtle; +// Radio and Checkbox: Checked +// ---------------------------------------------------------------- - font-size: globals.$ion-font-size-350; +.item-radio-checked, +.item-checkbox-checked { + --background: #{globals.$ion-bg-primary-subtle-default}; } diff --git a/core/src/components/select-popover/select-popover.ios.scss b/core/src/components/select-popover/select-popover.ios.scss index de3cfea6135..ab68689b837 100644 --- a/core/src/components/select-popover/select-popover.ios.scss +++ b/core/src/components/select-popover/select-popover.ios.scss @@ -1,2 +1,5 @@ +@use "../select-option/select-option.ios.overlay"; @import "./select-popover.native"; -@import "./select-popover.ios.vars"; + +// Select Popover: Select Option +// -------------------------------------------------- diff --git a/core/src/components/select-popover/select-popover.ios.vars.scss b/core/src/components/select-popover/select-popover.ios.vars.scss deleted file mode 100644 index 188e3f5f97b..00000000000 --- a/core/src/components/select-popover/select-popover.ios.vars.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import "../../themes/native/native.globals.ios"; -@import "../item/item.ios.vars"; - -// iOS Select Popover -// -------------------------------------------------- diff --git a/core/src/components/select-popover/select-popover.md.scss b/core/src/components/select-popover/select-popover.md.scss index c7728bcaf04..1ea46b7c288 100644 --- a/core/src/components/select-popover/select-popover.md.scss +++ b/core/src/components/select-popover/select-popover.md.scss @@ -1,3 +1,4 @@ +@use "../select-option/select-option.md.overlay"; @import "./select-popover.native"; @import "./select-popover.md.vars"; diff --git a/core/src/components/select-popover/select-popover.native.scss b/core/src/components/select-popover/select-popover.native.scss index 0b52fafe932..79289a37ea9 100644 --- a/core/src/components/select-popover/select-popover.native.scss +++ b/core/src/components/select-popover/select-popover.native.scss @@ -1,19 +1,8 @@ @use "../../themes/native/native.theme.default" as native; @use "../../themes/mixins" as mixins; @use "../../themes/functions.font" as font; +@use "../select-option/select-option.native.overlay"; @use "./select-popover.common"; // Select Popover: Native // -------------------------------------------------- - -.select-option-label { - gap: 12px; -} - -.select-option-description { - @include mixins.padding(5px, 0, 0, 0); - - color: native.$text-color-step-300; - - font-size: font.dynamic-font(12px); -} diff --git a/core/src/components/select-popover/select-popover.tsx b/core/src/components/select-popover/select-popover.tsx index b7f0f9bb839..711b80bba1b 100644 --- a/core/src/components/select-popover/select-popover.tsx +++ b/core/src/components/select-popover/select-popover.tsx @@ -1,5 +1,6 @@ import type { ComponentInterface } from '@stencil/core'; import { Element, Component, Host, Prop, h, forceUpdate } from '@stencil/core'; +import { getOverlayLabelJustify, getOverlayLabelPlacement } from '@utils/overlay-control-label'; import { safeCall } from '@utils/overlays'; import { renderOptionLabel } from '@utils/select-option-render'; import { getClassMap } from '@utils/theme'; @@ -127,6 +128,7 @@ export class SelectPopover implements ComponentInterface { } renderCheckboxOptions(options: SelectPopoverOption[]) { + const theme = getIonTheme(this); return options.map((option, index) => { /** * Cast to `SelectOverlayOption` to access rich content @@ -135,6 +137,7 @@ export class SelectPopover implements ComponentInterface { * part of the public `SelectPopoverOption` interface. */ const richOption = option as SelectOverlayOption; + const hasRichContent = !!richOption.startContent || !!richOption.endContent || !!richOption.description; const optionLabelOptions = { id: `popover-option-${index}`, label: richOption.text, @@ -142,9 +145,13 @@ export class SelectPopover implements ComponentInterface { endContent: richOption.endContent, description: richOption.description, }; + const defaultLabelPlacement = getOverlayLabelPlacement(theme, 'checkbox'); + const defaultJustify = getOverlayLabelJustify(theme, 'checkbox'); return ( { this.setChecked(ev); this.callOptionHandler(ev); @@ -172,6 +182,7 @@ export class SelectPopover implements ComponentInterface { } renderRadioOptions(options: SelectPopoverOption[]) { + const theme = getIonTheme(this); const checked = options.filter((o) => o.checked).map((o) => o.value)[0]; return ( @@ -184,6 +195,7 @@ export class SelectPopover implements ComponentInterface { * part of the public `SelectPopoverOption` interface. */ const richOption = option as SelectOverlayOption; + const hasRichContent = !!richOption.startContent || !!richOption.endContent || !!richOption.description; const optionLabelOptions = { id: `popover-option-${index}`, label: richOption.text, @@ -194,6 +206,8 @@ export class SelectPopover implements ComponentInterface { return ( this.dismissParentPopover()} onKeyDown={(ev) => { if (ev.key === 'Enter' && !ev.repeat) { diff --git a/core/src/components/select-popover/test/basic/index.html b/core/src/components/select-popover/test/basic/index.html index 69b0e78ceba..679ec678d2c 100644 --- a/core/src/components/select-popover/test/basic/index.html +++ b/core/src/components/select-popover/test/basic/index.html @@ -2,7 +2,7 @@ - Select - Popover + Select Popover - Basic + + + + Select Popover - States + + + + + + + + + + + + + Select Popover - States + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts b/core/src/components/select-popover/test/states/select-popover.e2e.ts new file mode 100644 index 00000000000..6963342d39c --- /dev/null +++ b/core/src/components/select-popover/test/states/select-popover.e2e.ts @@ -0,0 +1,60 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +/** + * This behavior does not vary across directions. + */ +configs({ directions: ['ltr'], modes: ['ios', 'md', 'ionic-md'] }).forEach(({ config, screenshot, title }) => { + test.describe(title('select-popover: states'), () => { + /** + * `(any-hover: hover)` evaluates to "none" in all three mobile-emulated + * projects, suppressing the hover rules: + * + * - Chromium and WebKit suppress it because of hasTouch and isMobile. + * - Headless Firefox doesn't detect input devices and reports no hover + * capability regardless of context options, so override it via + * launchOptions.firefoxUserPrefs. Bit values: 4 = FINE (mouse), + * 8 = HOVER, 12 = FINE | HOVER. + * + * Viewport, userAgent, and scale factor remain mobile-sized. + */ + test.use({ + hasTouch: false, + isMobile: false, + }); + + test.beforeEach(async ({ page }) => { + await page.goto(`/src/components/select-popover/test/states`, config); + }); + + test('should render all radio states', async ({ page }) => { + const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent'); + await page.locator('#single').click(); + await ionPopoverDidPresent.next(); + + const popover = page.locator('#popover-single'); + const selectPopover = popover.locator('ion-select-popover'); + await expect(selectPopover).toBeVisible(); + + const defaultRow = selectPopover.locator('ion-item').first(); + await defaultRow.hover(); + + await expect(selectPopover).toHaveScreenshot(screenshot('select-popover-radio-states')); + }); + + test('should render all checkbox states', async ({ page }) => { + const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent'); + await page.locator('#multiple').click(); + await ionPopoverDidPresent.next(); + + const popover = page.locator('#popover-multiple'); + const selectPopover = popover.locator('ion-select-popover'); + await expect(selectPopover).toBeVisible(); + + const defaultRow = selectPopover.locator('ion-item').first(); + await defaultRow.hover(); + + await expect(selectPopover).toHaveScreenshot(screenshot('select-popover-checkbox-states')); + }); + }); +}); diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..78b8d52a9bc Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..3cfc3bcee8b Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..dcdd1d85ae2 Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..e2aaa6296c6 Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..647c77b193c Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..76753d37ccc Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..bb05fb3d9e8 Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..1933f822460 Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..0c3f5a3f507 Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..9cf11e2df24 Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..dc20aa1555e Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..7dc06288c15 Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..4814033cdfb Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..122e0a19fbc Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..7896d663311 Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..a0448e47d3f Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..94559c1724d Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..2af5fa8df3f Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/select-interface.ts b/core/src/components/select/select-interface.ts index 7bca3390a15..af1a397c9e1 100644 --- a/core/src/components/select/select-interface.ts +++ b/core/src/components/select/select-interface.ts @@ -23,11 +23,19 @@ export interface SelectActionSheetButton extends Omit export interface SelectAlertInput extends Omit, RichContentOption { /** The main label for the option as a string or an HTMLElement. */ label?: string | HTMLElement; + /** Where the label sits relative to the option's selection control. */ + labelPlacement?: 'start' | 'end'; + /** How to pack the label and the option's selection control within a line. */ + justify?: 'start' | 'end' | 'space-between'; } export interface SelectOverlayOption extends Omit, RichContentOption { /** The main text for the option as a string or an HTMLElement. */ text?: string | HTMLElement; + /** Where the label sits relative to the option's selection control. */ + labelPlacement?: 'start' | 'end'; + /** How to pack the label and the option's selection control within a line. */ + justify?: 'start' | 'end' | 'space-between'; } export interface RichContentOption { diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index cde4a52850f..61f037bb10a 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -10,7 +10,7 @@ import { printIonWarning } from '@utils/logging'; import { actionSheetController, alertController, popoverController, modalController } from '@utils/overlays'; import type { OverlaySelect } from '@utils/overlays-interface'; import { isRTL } from '@utils/rtl'; -import { sanitizeDOMString } from '@utils/sanitization'; +import { reflectPropertiesToAttributes, sanitizeDOMTree } from '@utils/sanitization'; import { createColorClasses, hostContext } from '@utils/theme'; import { watchForOptions } from '@utils/watch-options'; import { caretDownSharp, chevronExpand } from 'ionicons/icons'; @@ -591,16 +591,13 @@ export class Select implements ComponentInterface { .join(' '); const optClass = `${OPTION_CLASS} ${copyClasses}`; const isSelected = isOptionSelected(selectValue, value, this.compareWith); - const text = this.customHTMLEnabled ? getOptionContent(option) : getDefaultSlotPlainText(option); - const startContent = this.customHTMLEnabled - ? (getOptionContent(option, 'start') as HTMLElement | null) - : undefined; - const endContent = this.customHTMLEnabled ? (getOptionContent(option, 'end') as HTMLElement | null) : undefined; + const { content, startContent, endContent } = extractOptionContent(option, this.customHTMLEnabled); return { role: isSelected ? 'selected' : '', - text: text ?? '', + text: content ?? '', cssClass: optClass, + disabled: option.disabled, handler: () => { this.setValue(value); }, @@ -608,8 +605,8 @@ export class Select implements ComponentInterface { 'aria-checked': isSelected ? 'true' : 'false', role: 'radio', }, - startContent: startContent ?? undefined, - endContent: endContent ?? undefined, + startContent, + endContent, description: option.description, } as SelectActionSheetButton; }); @@ -639,22 +636,20 @@ export class Select implements ComponentInterface { .filter((cls) => cls !== 'hydrated') .join(' '); const optClass = `${OPTION_CLASS} ${copyClasses}`; - const label = this.customHTMLEnabled ? getOptionContent(option) : getDefaultSlotPlainText(option); - const startContent = this.customHTMLEnabled - ? (getOptionContent(option, 'start') as HTMLElement | null) - : undefined; - const endContent = this.customHTMLEnabled ? (getOptionContent(option, 'end') as HTMLElement | null) : undefined; + const { content, startContent, endContent } = extractOptionContent(option, this.customHTMLEnabled); return { type: inputType, cssClass: optClass, - label: label ?? '', + label: content ?? '', value, checked: isOptionSelected(selectValue, value, this.compareWith), disabled: option.disabled, - startContent: startContent ?? undefined, - endContent: endContent ?? undefined, + startContent, + endContent, description: option.description, + labelPlacement: option.labelPlacement, + justify: option.justify, }; }); @@ -670,14 +665,10 @@ export class Select implements ComponentInterface { .filter((cls) => cls !== 'hydrated') .join(' '); const optClass = `${OPTION_CLASS} ${copyClasses}`; - const text = this.customHTMLEnabled ? getOptionContent(option) : getDefaultSlotPlainText(option); - const startContent = this.customHTMLEnabled - ? (getOptionContent(option, 'start') as HTMLElement | null) - : undefined; - const endContent = this.customHTMLEnabled ? (getOptionContent(option, 'end') as HTMLElement | null) : undefined; + const { content, startContent, endContent } = extractOptionContent(option, this.customHTMLEnabled); return { - text: text ?? '', + text: content ?? '', cssClass: optClass, value, checked: isOptionSelected(selectValue, value, this.compareWith), @@ -688,9 +679,11 @@ export class Select implements ComponentInterface { this.close(); } }, - startContent: startContent ?? undefined, - endContent: endContent ?? undefined, + startContent, + endContent, description: option.description, + labelPlacement: option.labelPlacement, + justify: option.justify, }; }); @@ -1641,8 +1634,15 @@ const getOptionContent = ( // Default slot: get nodes without a slot attribute const defaultSlot = getOptionDefaultSlot(option) || []; nodes = defaultSlot.filter((node) => { - // Exclude whitespace-only text nodes to prevent empty container returns - return node.textContent?.trim().length !== 0; + /** + * Exclude whitespace-only text nodes (newline noise between + * markup elements). Element nodes are always kept, even when + * their textContent is empty (e.g. , ). + */ + if (node.nodeType === Node.TEXT_NODE) { + return node.textContent?.trim().length !== 0; + } + return true; }); } @@ -1655,6 +1655,18 @@ const getOptionContent = ( return nodes.map((n) => n.textContent?.trim()).join(' ') || null; } + /** + * Mirror known custom-element properties (e.g. ion-icon's `icon`) + * onto attributes before cloning. Frameworks like Vue set these as + * DOM properties, which `cloneNode` doesn't copy, so without this + * step the cloned overlay copy renders without the prop's value. + */ + nodes.forEach((n) => { + if (n.nodeType === Node.ELEMENT_NODE) { + reflectPropertiesToAttributes(n as Element); + } + }); + // Clone each node into a temporary container const container = document.createElement('div'); nodes.forEach((n) => { @@ -1667,11 +1679,17 @@ const getOptionContent = ( container.appendChild(clone); }); + /** + * Sanitize the cloned DOM in place. Trusted attributes (size, color, + * shape, etc.) are preserved; event handlers, javascript: URLs, and + * blocked tags are stripped. + */ + sanitizeDOMTree(container); + if (useHTML) { - return sanitizeDOMString(container.innerHTML.trim()) || null; + return container.innerHTML.trim() || null; } - // Already sanitized through `renderOptionLabel` return container; }; @@ -1716,6 +1734,31 @@ const getDefaultSlotPlainText = (option: HTMLIonSelectOptionElement): string => return texts.join(' '); }; +/** + * Extracts the rich content from an `ion-select-option`. + * When `customHTMLEnabled` is `false`, only the plain text from the + * default slot is read and the start and end slots are skipped. + * + * @param option - The `ion-select-option` element to extract content from. + * @param customHTMLEnabled - Whether custom HTML rendering is enabled + * via the `innerHTMLTemplatesEnabled` config. + */ +const extractOptionContent = (option: HTMLIonSelectOptionElement, customHTMLEnabled: boolean) => { + if (!customHTMLEnabled) { + return { + content: getDefaultSlotPlainText(option), + startContent: undefined as HTMLElement | undefined, + endContent: undefined as HTMLElement | undefined, + }; + } + + return { + content: getOptionContent(option), + startContent: (getOptionContent(option, 'start') as HTMLElement | null) ?? undefined, + endContent: (getOptionContent(option, 'end') as HTMLElement | null) ?? undefined, + }; +}; + let selectIds = 0; const OPTION_CLASS = 'select-interface-option'; diff --git a/core/src/components/select/test/basic/index.html b/core/src/components/select/test/basic/index.html index 1048e1db3e6..e8bd88d3736 100644 --- a/core/src/components/select/test/basic/index.html +++ b/core/src/components/select/test/basic/index.html @@ -29,7 +29,7 @@ - + Apples Oranges Pears @@ -37,7 +37,7 @@ - + Apples Oranges Pears @@ -45,7 +45,7 @@ - + Apples Oranges Pears @@ -53,7 +53,7 @@ - + Apples Oranges Pears @@ -67,7 +67,12 @@ - + Apple Apricot Avocado @@ -105,12 +110,7 @@ - + Apple Apricot Avocado @@ -148,7 +148,7 @@ - + Apple Apricot Avocado @@ -186,7 +186,7 @@ - + Apple Apricot Avocado @@ -240,7 +240,7 @@ - + Bird Cat Dog @@ -249,7 +249,7 @@ - + Bird Cat Dog @@ -263,14 +263,12 @@ Custom Interface Options - + Pepperoni Bacon @@ -280,8 +278,15 @@ - - + + Pepperoni Bacon Extra Cheese @@ -290,13 +295,8 @@ - - + + Pepperoni Bacon Extra Cheese @@ -305,8 +305,8 @@ - - + + Pepperoni Bacon Extra Cheese @@ -318,30 +318,22 @@ diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png index b1a10395d7c..1b15e0e4c10 100644 Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png index b55a80fa526..26f83cbaf0f 100644 Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png index 4c235db6b42..7884f70bed7 100644 Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png index 8ce21e9a05e..cf94b7493e1 100644 Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png index 3394b979256..fd00e3047cb 100644 Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png index 40af8b073d5..d5869e74cbb 100644 Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Chrome-linux.png index 92064755809..16c11b07e63 100644 Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Firefox-linux.png index 09dbd660ee0..ed5197a95bc 100644 Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Safari-linux.png index ed6becd6e68..93e0f922f04 100644 Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Safari-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png index 058e9eb36b8..1e7861eda5d 100644 Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png index f6dda21ddea..e4274e14f01 100644 Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png index a9540533623..426f785d18b 100644 Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Chrome-linux.png index 48f5106e004..c27db8f1d50 100644 Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Firefox-linux.png index e13afdfc587..f30c52a9d56 100644 Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Safari-linux.png index 82b3f630513..7717021c56a 100644 Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Safari-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/disabled/select.e2e.ts b/core/src/components/select/test/disabled/select.e2e.ts index 621ed048d3e..e567e2ff82e 100644 --- a/core/src/components/select/test/disabled/select.e2e.ts +++ b/core/src/components/select/test/disabled/select.e2e.ts @@ -1,8 +1,83 @@ import { expect } from '@playwright/test'; +import type { E2ELocator } from '@utils/test/playwright'; import { configs, test } from '@utils/test/playwright'; +const DISABLED_OPTION_INTERFACES = [ + { + name: 'action-sheet', + overlayTag: 'ion-action-sheet', + didPresent: 'ionActionSheetDidPresent', + didDismiss: 'ionActionSheetDidDismiss', + // The option itself is the interactive button. + controlSuffix: '', + }, + { + name: 'alert', + overlayTag: 'ion-alert', + didPresent: 'ionAlertDidPresent', + didDismiss: 'ionAlertDidDismiss', + // The option itself is the interactive radio button. + controlSuffix: '', + }, + { + name: 'popover', + overlayTag: 'ion-popover', + didPresent: 'ionPopoverDidPresent', + didDismiss: 'ionPopoverDidDismiss', + // The interactive control is the nested ion-radio. + controlSuffix: ' ion-radio', + }, + { + name: 'modal', + overlayTag: 'ion-modal', + didPresent: 'ionModalDidPresent', + didDismiss: 'ionModalDidDismiss', + // The interactive control is the nested ion-radio. + controlSuffix: ' ion-radio', + }, +] as const; + configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { test.describe(title('select: disabled options'), () => { + for (const { name, overlayTag, didPresent, didDismiss, controlSuffix } of DISABLED_OPTION_INTERFACES) { + test(`${name}: clicking a disabled option should not change the value or dismiss the overlay`, async ({ + page, + }) => { + await page.setContent( + ` + + Oranges + + `, + config + ); + + const select = page.locator('ion-select') as E2ELocator; + + const ionChange = await select.spyOnEvent('ionChange'); + const ionDidPresent = await page.spyOnEvent(didPresent); + const ionDidDismiss = await page.spyOnEvent(didDismiss); + + await select.click(); + + await ionDidPresent.next(); + + const overlay = page.locator(overlayTag); + const disabledOption = overlay.locator(`.select-interface-option${controlSuffix}`); + + await disabledOption.click({ force: true }); + + await page.waitForChanges(); + + const value = await select.evaluate((el: HTMLIonSelectElement) => el.value); + expect(value).toBeUndefined(); + + expect(ionChange).toHaveReceivedEventTimes(0); + expect(ionDidDismiss).toHaveReceivedEventTimes(0); + await expect(overlay).toBeVisible(); + }); + } + test('should not focus a disabled option when no value is set', async ({ page, skip }) => { // TODO (ROU-5437) skip.browser('webkit', 'Safari 16 only allows text fields and pop-up menus to be focused.'); diff --git a/core/src/components/select/test/rich-content-option/index.html b/core/src/components/select/test/rich-content-option/index.html index 7bdf2881d3a..78d3f3987c9 100644 --- a/core/src/components/select/test/rich-content-option/index.html +++ b/core/src/components/select/test/rich-content-option/index.html @@ -54,14 +54,13 @@ - + NEW - + Full Content - This is a span element @@ -98,18 +97,60 @@ NEW + + + + + This is a very long option label that demonstrates how the start and end slots stay at their intrinsic + widths while the content area absorbs the remaining row width + NEW + + + + + + SVGs + + + + + + + + + + + Icons + + + + + Images + + + + + + + Thumbnails + + + + - + NEW Full Content - This is a span element @@ -146,18 +187,60 @@ NEW + + + + + This is a very long option label that demonstrates how the start and end slots stay at their intrinsic + widths while the content area absorbs the remaining row width + NEW + + + + + + SVGs + + + + + + + + + + + Icons + + + + + Images + + + + + + + Thumbnails + + + + - + NEW Full Content - This is a span element @@ -194,18 +277,60 @@ NEW + + + + + This is a very long option label that demonstrates how the start and end slots stay at their intrinsic + widths while the content area absorbs the remaining row width + NEW + + + + + + SVGs + + + + + + + + + + + Icons + + + + + Images + + + + + + + Thumbnails + + + + - + NEW Full Content - This is a span element @@ -242,6 +367,49 @@ NEW + + + + + This is a very long option label that demonstrates how the start and end slots stay at their intrinsic + widths while the content area absorbs the remaining row width + NEW + + + + + + SVGs + + + + + + + + + + + Icons + + + + + Images + + + + + + + Thumbnails + + + + @@ -259,7 +427,6 @@ Full Content - This is a span element @@ -296,18 +463,60 @@ NEW + + + + + This is a very long option label that demonstrates how the start and end slots stay at their intrinsic + widths while the content area absorbs the remaining row width + NEW + + + + + + SVGs + + + + + + + + + + + Icons + + + + + Images + + + + + + + Thumbnails + + + + - + NEW Full Content - This is a span element @@ -344,18 +553,61 @@ NEW + + + + + This is a very long option label that demonstrates how the start and end slots stay at their intrinsic + widths while the content area absorbs the remaining row width + NEW + + + + + + SVGs + + + + + + + SVG + + + + + Icons + + + + + Images + + + + + + + Thumbnails + + + + - + NEW Full Content - This is a span element @@ -392,6 +644,49 @@ NEW + + + + + This is a very long option label that demonstrates how the start and end slots stay at their intrinsic + widths while the content area absorbs the remaining row width + NEW + + + + + + SVGs + + + + + + + + + + + Icons + + + + + Images + + + + + + + Thumbnails + + + + diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts b/core/src/components/select/test/rich-content-option/select.e2e.ts index 78a2fa4e4a1..2929c00c790 100644 --- a/core/src/components/select/test/rich-content-option/select.e2e.ts +++ b/core/src/components/select/test/rich-content-option/select.e2e.ts @@ -2,28 +2,94 @@ import { expect } from '@playwright/test'; import { configs, test } from '@utils/test/playwright'; /** - * This behavior does not vary across modes/directions + * This behavior does not vary across directions */ -configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { +configs({ directions: ['ltr'], modes: ['ionic-md', 'md', 'ios'] }).forEach(({ title, screenshot, config }) => { test.describe(title('select: rich content options'), () => { test.beforeEach(async ({ page }) => { await page.goto('/src/components/select/test/rich-content-option', config); }); - test('it should render for alert interface and single selection', async ({ page }) => { + test('should not have visual regressions for the action sheet interface', async ({ page }) => { + const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent'); + + await page.locator('#action-sheet-select').click(); + await ionActionSheetDidPresent.next(); + + // Move mouse to away from the alert so hover styles don't interfere with screenshots + await page.mouse.move(0, 0); + + const firstOption = page.locator('ion-action-sheet .select-interface-option').first(); + + await expect(firstOption).toHaveScreenshot(screenshot(`select-rich-content-action-sheet`)); + }); + + test('should not have visual regressions for the alert interface', async ({ page }) => { const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent'); - const ionAlertDidDismiss = await page.spyOnEvent('ionAlertDidDismiss'); - const select = page.locator('#alert-select'); + await page.locator('#alert-select').click(); + await ionAlertDidPresent.next(); + + // Move mouse to away from the alert so hover styles don't interfere with screenshots + await page.mouse.move(0, 0); + + const firstOption = page.locator('ion-alert .select-interface-option').first(); + + await expect(firstOption).toHaveScreenshot(screenshot(`select-rich-content-alert`)); + }); + + test('should not have visual regressions for the modal interface', async ({ page }) => { + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + await page.locator('#modal-select').click(); + await ionModalDidPresent.next(); + + // Move mouse to away from the alert so hover styles don't interfere with screenshots + await page.mouse.move(0, 0); + + const firstOption = page.locator('ion-modal .select-interface-option').first(); + + await expect(firstOption).toHaveScreenshot(screenshot(`select-rich-content-modal`)); + }); + + test('should not have visual regressions for the popover interface', async ({ page }) => { + const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent'); + + await page.locator('#popover-select').click(); + await ionPopoverDidPresent.next(); + + // Move mouse to away from the alert so hover styles don't interfere with screenshots + await page.mouse.move(0, 0); + + const firstOption = page.locator('ion-popover .select-interface-option').first(); + + await expect(firstOption).toHaveScreenshot(screenshot(`select-rich-content-popover`)); + }); + }); +}); + +/** + * This behavior does not vary across modes/directions + */ +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('select: rich content option functionality'), () => { + test.beforeEach(async ({ page }) => { + await page.goto('/src/components/select/test/rich-content-option', config); + }); + + test('it should render for action sheet interface', async ({ page }) => { + const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent'); + const ionActionSheetDidDismiss = await page.spyOnEvent('ionActionSheetDidDismiss'); + + const select = page.locator('#action-sheet-select'); await select.click(); - await ionAlertDidPresent.next(); + await ionActionSheetDidPresent.next(); - const alert = page.locator('ion-alert'); - const firstOption = alert.locator('.alert-radio-label').first(); + const actionSheet = page.locator('ion-action-sheet'); + const firstOption = actionSheet.locator('.select-interface-option').first(); const avatar = firstOption.locator('ion-avatar'); - const spanText = await firstOption.locator('.span-style').textContent(); const firstOptionText = 'Full Content'; await expect(firstOption).toContainText(firstOptionText); @@ -32,17 +98,12 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { // Click on the first option await firstOption.click(); - // Confirm the selection - const confirmButton = alert.locator('.alert-button:not(.alert-button-role-cancel)'); - await confirmButton.click(); - - await ionAlertDidDismiss.next(); + await ionActionSheetDidDismiss.next(); // Verify that the select text includes the option text const selectText = await select.locator('.select-text').textContent(); expect(selectText).toContain(firstOptionText); - expect(selectText).toContain(spanText); // Verify that the select text does not include the avatar and badge const selectTextAvatar = select.locator('.select-text ion-avatar'); @@ -52,20 +113,19 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { await expect(selectTextBadge).toHaveCount(0); }); - test('it should render for alert interface and multiple selection', async ({ page }) => { + test('it should render for alert interface and single selection', async ({ page }) => { const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent'); const ionAlertDidDismiss = await page.spyOnEvent('ionAlertDidDismiss'); - const select = page.locator('#alert-select-multiple'); + const select = page.locator('#alert-select'); await select.click(); await ionAlertDidPresent.next(); const alert = page.locator('ion-alert'); - const firstOption = alert.locator('.alert-checkbox-label').first(); + const firstOption = alert.locator('.select-interface-option').first(); const avatar = firstOption.locator('ion-avatar'); - const spanText = await firstOption.locator('.span-style').textContent(); const firstOptionText = 'Full Content'; await expect(firstOption).toContainText(firstOptionText); @@ -84,7 +144,6 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { const selectText = await select.locator('.select-text').textContent(); expect(selectText).toContain(firstOptionText); - expect(selectText).toContain(spanText); // Verify that the select text does not include the avatar and badge const selectTextAvatar = select.locator('.select-text ion-avatar'); @@ -94,20 +153,19 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { await expect(selectTextBadge).toHaveCount(0); }); - test('it should render for action sheet interface', async ({ page }) => { - const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent'); - const ionActionSheetDidDismiss = await page.spyOnEvent('ionActionSheetDidDismiss'); + test('it should render for alert interface and multiple selection', async ({ page }) => { + const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent'); + const ionAlertDidDismiss = await page.spyOnEvent('ionAlertDidDismiss'); - const select = page.locator('#action-sheet-select'); + const select = page.locator('#alert-select-multiple'); await select.click(); - await ionActionSheetDidPresent.next(); + await ionAlertDidPresent.next(); - const actionSheet = page.locator('ion-action-sheet'); - const firstOption = actionSheet.locator('.action-sheet-button-label').first(); + const alert = page.locator('ion-alert'); + const firstOption = alert.locator('.select-interface-option').first(); const avatar = firstOption.locator('ion-avatar'); - const spanText = await firstOption.locator('.span-style').textContent(); const firstOptionText = 'Full Content'; await expect(firstOption).toContainText(firstOptionText); @@ -116,13 +174,16 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { // Click on the first option await firstOption.click(); - await ionActionSheetDidDismiss.next(); + // Confirm the selection + const confirmButton = alert.locator('.alert-button:not(.alert-button-role-cancel)'); + await confirmButton.click(); + + await ionAlertDidDismiss.next(); // Verify that the select text includes the option text const selectText = await select.locator('.select-text').textContent(); expect(selectText).toContain(firstOptionText); - expect(selectText).toContain(spanText); // Verify that the select text does not include the avatar and badge const selectTextAvatar = select.locator('.select-text ion-avatar'); @@ -132,20 +193,19 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { await expect(selectTextBadge).toHaveCount(0); }); - test('it should render for popover interface and single selection', async ({ page }) => { - const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent'); - const ionPopoverDidDismiss = await page.spyOnEvent('ionPopoverDidDismiss'); + test('it should render for modal interface and single selection', async ({ page }) => { + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss'); - const select = page.locator('#popover-select'); + const select = page.locator('#modal-select'); await select.click(); - await ionPopoverDidPresent.next(); + await ionModalDidPresent.next(); - const popover = page.locator('ion-popover'); - const firstOption = popover.locator('.select-option-label').first(); + const modal = page.locator('ion-modal'); + const firstOption = modal.locator('.select-interface-option').first(); const avatar = firstOption.locator('ion-avatar'); - const spanText = await firstOption.locator('.span-style').textContent(); const firstOptionText = 'Full Content'; await expect(firstOption).toContainText(firstOptionText); @@ -154,13 +214,12 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { // Click on the first option await firstOption.click(); - await ionPopoverDidDismiss.next(); + await ionModalDidDismiss.next(); // Verify that the select text includes the option text const selectText = await select.locator('.select-text').textContent(); expect(selectText).toContain(firstOptionText); - expect(selectText).toContain(spanText); // Verify that the select text does not include the avatar and badge const selectTextAvatar = select.locator('.select-text ion-avatar'); @@ -170,20 +229,19 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { await expect(selectTextBadge).toHaveCount(0); }); - test('it should render for popover interface and multiple selection', async ({ page }) => { - const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent'); - const ionPopoverDidDismiss = await page.spyOnEvent('ionPopoverDidDismiss'); + test('it should render for modal interface and multiple selection', async ({ page }) => { + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss'); - const select = page.locator('#popover-select-multiple'); + const select = page.locator('#modal-select-multiple'); await select.click(); - await ionPopoverDidPresent.next(); + await ionModalDidPresent.next(); - const popover = page.locator('ion-popover'); - const firstOption = popover.locator('.select-option-label').first(); + const modal = page.locator('ion-modal'); + const firstOption = modal.locator('.select-interface-option').first(); const avatar = firstOption.locator('ion-avatar'); - const spanText = await firstOption.locator('.span-style').textContent(); const firstOptionText = 'Full Content'; await expect(firstOption).toContainText(firstOptionText); @@ -193,16 +251,15 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { await firstOption.click(); // Confirm the selection - const backdrop = page.locator('ion-backdrop'); - await backdrop.click({ position: { x: 10, y: 10 } }); + const cancelButton = modal.getByRole('button', { name: 'Cancel' }); - await ionPopoverDidDismiss.next(); + await cancelButton.click(); + await ionModalDidDismiss.next(); // Verify that the select text includes the option text const selectText = await select.locator('.select-text').textContent(); expect(selectText).toContain(firstOptionText); - expect(selectText).toContain(spanText); // Verify that the select text does not include the avatar and badge const selectTextAvatar = select.locator('.select-text ion-avatar'); @@ -212,20 +269,19 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { await expect(selectTextBadge).toHaveCount(0); }); - test('it should render for modal interface and single selection', async ({ page }) => { - const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); - const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss'); + test('it should render for popover interface and single selection', async ({ page }) => { + const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent'); + const ionPopoverDidDismiss = await page.spyOnEvent('ionPopoverDidDismiss'); - const select = page.locator('#modal-select'); + const select = page.locator('#popover-select'); await select.click(); - await ionModalDidPresent.next(); + await ionPopoverDidPresent.next(); - const modal = page.locator('ion-modal'); - const firstOption = modal.locator('.select-option-label').first(); + const popover = page.locator('ion-popover'); + const firstOption = popover.locator('.select-interface-option').first(); const avatar = firstOption.locator('ion-avatar'); - const spanText = await firstOption.locator('.span-style').textContent(); const firstOptionText = 'Full Content'; await expect(firstOption).toContainText(firstOptionText); @@ -234,13 +290,12 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { // Click on the first option await firstOption.click(); - await ionModalDidDismiss.next(); + await ionPopoverDidDismiss.next(); // Verify that the select text includes the option text const selectText = await select.locator('.select-text').textContent(); expect(selectText).toContain(firstOptionText); - expect(selectText).toContain(spanText); // Verify that the select text does not include the avatar and badge const selectTextAvatar = select.locator('.select-text ion-avatar'); @@ -250,20 +305,19 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { await expect(selectTextBadge).toHaveCount(0); }); - test('it should render for modal interface and multiple selection', async ({ page }) => { - const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); - const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss'); + test('it should render for popover interface and multiple selection', async ({ page }) => { + const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent'); + const ionPopoverDidDismiss = await page.spyOnEvent('ionPopoverDidDismiss'); - const select = page.locator('#modal-select-multiple'); + const select = page.locator('#popover-select-multiple'); await select.click(); - await ionModalDidPresent.next(); + await ionPopoverDidPresent.next(); - const modal = page.locator('ion-modal'); - const firstOption = modal.locator('.select-option-label').first(); + const popover = page.locator('ion-popover'); + const firstOption = popover.locator('.select-interface-option').first(); const avatar = firstOption.locator('ion-avatar'); - const spanText = await firstOption.locator('.span-style').textContent(); const firstOptionText = 'Full Content'; await expect(firstOption).toContainText(firstOptionText); @@ -273,16 +327,15 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { await firstOption.click(); // Confirm the selection - const cancelButton = modal.getByRole('button', { name: 'Cancel' }); + const backdrop = page.locator('ion-backdrop'); + await backdrop.click({ position: { x: 10, y: 10 } }); - await cancelButton.click(); - await ionModalDidDismiss.next(); + await ionPopoverDidDismiss.next(); // Verify that the select text includes the option text const selectText = await select.locator('.select-text').textContent(); expect(selectText).toContain(firstOptionText); - expect(selectText).toContain(spanText); // Verify that the select text does not include the avatar and badge const selectTextAvatar = select.locator('.select-text ion-avatar'); @@ -302,13 +355,13 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { await ionAlertDidPresent.next(); + // The "no-text" option only has a for its label content, + // so its aria label should be the span's plain text. const alert = page.locator('ion-alert'); - const firstOption = alert.locator('.alert-radio-label').first(); + const spanOption = alert.locator('.alert-radio-button', { hasText: 'This is a span element' }); - // Click on the first option - await firstOption.click(); + await spanOption.click(); - // Confirm the selection const confirmButton = alert.locator('.alert-button:not(.alert-button-role-cancel)'); await confirmButton.click(); @@ -317,7 +370,7 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { const nativeButton = select.locator('button'); const ariaLabel = await nativeButton.getAttribute('aria-label'); - expect(ariaLabel).toContain('Full Content This is a span element'); + expect(ariaLabel).toContain('This is a span element'); }); }); }); @@ -341,7 +394,7 @@ configs({ modes: ['md'] }).forEach(({ title, config }) => { await ionAlertDidPresent.next(); const alert = page.locator('ion-alert'); - const firstOption = alert.locator('.alert-radio-label').first(); + const firstOption = alert.locator('.alert-radio-button').first(); const startContainer = firstOption.locator('.select-option-start'); const endContainer = firstOption.locator('.select-option-end'); @@ -404,7 +457,7 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { await ionAlertDidPresent.next(); const alert = page.locator('ion-alert'); - const firstOption = alert.locator('.alert-radio-label').first(); + const firstOption = alert.locator('.alert-radio-button').first(); const startContainer = firstOption.locator('.select-option-start'); const endContainer = firstOption.locator('.select-option-end'); const span = firstOption.locator('.span-style'); diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..75ff364fd3e Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..36c430cd23e Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..163e08b5dca Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..37f31b6384d Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..6c4b34e8983 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..b65bbfd9a09 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..76ccc2b5eb6 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..03c11eb98a9 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..ea751c3a109 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..e13b18f1370 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..f6e54e16005 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..c22ee9333d5 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..65b83d62d23 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..46de4f3e86e Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..0e48794f233 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..a741ba882d2 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..cc75b06c121 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..c64bbb9e6cc Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..1b316f262e4 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..1f4b4f3e867 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..efa0b81027a Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..886d1dd9f7d Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..d2d728a7c1c Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..063821f6cd7 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..7bd6d9b5eb2 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..6b94e4b327f Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..0500953c143 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..b3f4cddd661 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..7693ed60d0c Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..0486fbe4fd0 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..815c9a3e0da Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..a3567e18458 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..528b3b23802 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..c95a90b27dd Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..dfa6943cc37 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..ef1b48f49f0 Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/select.spec.tsx b/core/src/components/select/test/select.spec.tsx index f00c4c66884..ad7c0d3d050 100644 --- a/core/src/components/select/test/select.spec.tsx +++ b/core/src/components/select/test/select.spec.tsx @@ -1,6 +1,8 @@ import { h } from '@stencil/core'; import { newSpecPage } from '@stencil/core/testing'; +import { config } from '../../../global/config'; +import { SelectOption } from '../../select-option/select-option'; import { Select } from '../select'; describe('ion-select', () => { @@ -157,3 +159,56 @@ describe('ion-select: required', () => { expect(nativeButton.getAttribute('aria-required')).toBe('false'); }); }); + +describe('ion-select: option content property reflection', () => { + beforeEach(() => { + // Cloning rich option content into the select text only happens when + // custom HTML rendering is enabled. + config.reset({ innerHTMLTemplatesEnabled: true }); + }); + + afterEach(() => { + config.reset({}); + }); + + it('should reflect ion-icon DOM properties onto attributes so they survive cloning into the select text', async () => { + const page = await newSpecPage({ + components: [Select, SelectOption], + html: `Star`, + }); + + const select = page.body.querySelector('ion-select')!; + const sourceIcon = select.querySelector('ion-icon')!; + + /** + * Frameworks such as Vue set `icon` as a DOM property rather than an + * attribute. `cloneNode` only copies attributes, so without reflection + * the cloned copy in the select text would lose the icon value. + */ + (sourceIcon as any).icon = 'logo-ionic'; + + // Selecting the option rebuilds the displayed text from the option content. + select.value = 'star'; + await page.waitForChanges(); + + const renderedIcon = select.shadowRoot!.querySelector('.select-text ion-icon'); + expect(renderedIcon).not.toBeNull(); + expect(renderedIcon!.getAttribute('icon')).toBe('logo-ionic'); + }); + + it('should preserve an ion-icon attribute that is already set when cloning into the select text', async () => { + const page = await newSpecPage({ + components: [Select, SelectOption], + html: `Star`, + }); + + const select = page.body.querySelector('ion-select')!; + + select.value = 'star'; + await page.waitForChanges(); + + const renderedIcon = select.shadowRoot!.querySelector('.select-text ion-icon'); + expect(renderedIcon).not.toBeNull(); + expect(renderedIcon!.getAttribute('icon')).toBe('logo-ionic'); + }); +}); diff --git a/core/src/utils/overlay-control-label.ts b/core/src/utils/overlay-control-label.ts new file mode 100644 index 00000000000..f8dcd4a57cb --- /dev/null +++ b/core/src/utils/overlay-control-label.ts @@ -0,0 +1,58 @@ +// TODO(FW-6886, FW-6892, FW-6891): Remove this file in favor of the Modular Ionic component config. Each overlay will be able to select its own defaults for label placement and justify based on the interface and theme, so this utility will no longer be necessary. + +import type { Theme } from '../interface'; + +/** + * Returns the default `labelPlacement` for a radio or checkbox option + * rendered inside an overlay. Defaults follow each theme's established + * option-row layout: + * - `ionic`: always `"start"`. + * - `ios`: `"start"` for radio in `alert` and `popover`. The `modal` + * interface flips iOS radio back to `"end"`s. Checkbox is always + * `"end"` on iOS. + * - everything else (e.g. `md`): `"end"`. + * + * `interfaceType` is optional; only `"modal"` changes the result, so + * callers that aren't a modal can omit it. + *` + * Used by `alert`, `select-popover`, and `select-modal` as the fallback + * when an option doesn't explicitly set `labelPlacement`. + */ +export const getOverlayLabelPlacement = ( + theme: Theme, + control: 'radio' | 'checkbox', + interfaceType?: 'alert' | 'popover' | 'modal' +): 'start' | 'end' => { + if (theme === 'ionic' || (theme === 'ios' && control === 'radio' && interfaceType !== 'modal')) { + return 'start'; + } + + return 'end'; +}; + +/** + * Returns the default `justify` for a radio or checkbox option rendered + * inside an overlay. Defaults follow each theme's option-row layout: + * - `ionic`: always `"space-between"`. + * - `ios`: `"space-between"` for radio in `alert` and `popover`. The + * `modal` interface falls back to `"start"`. Checkbox is always `"start"` + * on iOS. + * - everything else (e.g. `md`): `"start"`. + * + * `interfaceType` is optional; only `"modal"` changes the result, so + * callers that aren't a modal can omit it. + * + * Used by `alert`, `select-popover`, and `select-modal` as the fallback when + * an option doesn't explicitly set `justify`. + */ +export const getOverlayLabelJustify = ( + theme: Theme, + control: 'radio' | 'checkbox', + interfaceType?: 'alert' | 'popover' | 'modal' +): 'start' | 'end' | 'space-between' => { + if (theme === 'ionic' || (theme === 'ios' && control === 'radio' && interfaceType !== 'modal')) { + return 'space-between'; + } + + return 'start'; +}; diff --git a/core/src/utils/sanitization/index.ts b/core/src/utils/sanitization/index.ts index acab505d828..c970f882191 100644 --- a/core/src/utils/sanitization/index.ts +++ b/core/src/utils/sanitization/index.ts @@ -1,8 +1,22 @@ import { printIonError } from '@utils/logging'; /** - * Does a simple sanitization of all elements - * in an untrusted string + * Sanitize an untrusted HTML string. + * + * Parses the string into a detached DOM, removes blocked tags, strips + * attributes outside the strict `allowedAttributes` list, and scrubs + * `javascript:` URLs. Returns the sanitized HTML string. + * + * Use this when you have an HTML string from an unknown source and need + * to render it via `innerHTML`. Prefer `sanitizeDOMTree` when the source + * is a trusted DOM tree that must keep its component attributes + * (`size`, `color`, `shape`, etc.). + * + * @param untrustedString - The HTML string to sanitize. Pass an + * `IonicSafeString` to bypass sanitization, or `undefined` to short-circuit. + * @returns The sanitized HTML string, or `undefined` if the input was + * `undefined`. Returns `''` if sanitization fails or the input contains + * an inline `onload=` handler. */ export const sanitizeDOMString = (untrustedString: IonicSafeString | string | undefined): string | undefined => { try { @@ -88,13 +102,43 @@ export const sanitizeDOMString = (untrustedString: IonicSafeString | string | un } }; +/** + * Sanitize an entire trusted DOM tree in place. + * + * Removes blocked tags (`script`, `iframe`, etc.) from the subtree and + * then sanitizes attributes on every remaining element. Component + * attributes like `size`, `color`, and `shape` are preserved; event + * handlers (`on*`) and `javascript:` URLs are stripped. + * + * Use this when you have a DOM tree the developer controls (e.g. + * cloned slot content from a component) and you need to render it + * elsewhere safely. + * + * @param root - The root element whose subtree will be sanitized in + * place. No-op when the sanitizer is disabled via `Ionic.config`. + */ +export const sanitizeDOMTree = (root: HTMLElement) => { + if (!isSanitizerEnabled()) { + return; + } + + blockedTags.forEach((tag) => { + const matches = root.querySelectorAll(tag); + for (let i = matches.length - 1; i >= 0; i--) { + matches[i].remove(); + } + }); + + sanitizeElement(root, true); +}; + /** * Clean up current element based on allowed attributes * and then recursively dig down into any child elements to * clean those up as well */ // TODO(FW-2832): type (using Element triggers other type errors as well) -const sanitizeElement = (element: any) => { +const sanitizeElement = (element: any, allowSafeAttributes = false) => { // IE uses childNodes, so ignore nodes that are not elements if (element.nodeType && element.nodeType !== 1) { return; @@ -114,9 +158,17 @@ const sanitizeElement = (element: any) => { for (let i = element.attributes.length - 1; i >= 0; i--) { const attribute = element.attributes.item(i); const attributeName = attribute.name; + const lowerName = attributeName.toLowerCase(); // remove non-allowed attribs - if (!allowedAttributes.includes(attributeName.toLowerCase())) { + if (!allowSafeAttributes && !allowedAttributes.includes(lowerName)) { + element.removeAttribute(attributeName); + continue; + } + + // strip event-handler attributes (already removed by the allowlist + // when !allowSafeAttributes; this guards the permissive path) + if (lowerName.startsWith('on')) { element.removeAttribute(attributeName); continue; } @@ -132,10 +184,14 @@ const sanitizeElement = (element: any) => { */ const propertyValue = element[attributeName]; + // Only call .toLowerCase() when propertyValue is a string. Some DOM + // properties (e.g. `disabled`) are booleans and would throw. /* eslint-disable */ if ( (attributeValue != null && attributeValue.toLowerCase().includes('javascript:')) || - (propertyValue != null && propertyValue.toLowerCase().includes('javascript:')) + (propertyValue != null && + typeof propertyValue === 'string' && + propertyValue.toLowerCase().includes('javascript:')) ) { element.removeAttribute(attributeName); } @@ -149,7 +205,7 @@ const sanitizeElement = (element: any) => { /* eslint-disable-next-line */ for (let i = 0; i < childElements.length; i++) { - sanitizeElement(childElements[i]); + sanitizeElement(childElements[i], allowSafeAttributes); } }; @@ -175,8 +231,57 @@ const isSanitizerEnabled = (): boolean => { return true; }; +/** + * Mirror known custom-element DOM properties onto attributes so they + * survive `cloneNode`. Call this on a DOM subtree before cloning it for + * rendering elsewhere (e.g. cloning slotted option content into an + * overlay). + * + * Only sets the attribute when the property holds a non-empty string + * and the attribute isn't already present, so existing attributes + * take precedence. + * + * @param root - The root element whose subtree (and itself) will be + * inspected. + */ +export const reflectPropertiesToAttributes = (root: Element): void => { + const candidates: Element[] = []; + if (root.tagName in elementPropsToReflect) { + candidates.push(root); + } + for (const tagName of Object.keys(elementPropsToReflect)) { + candidates.push(...Array.from(root.querySelectorAll(tagName.toLowerCase()))); + } + + for (const el of candidates) { + if (!(el.tagName in elementPropsToReflect)) { + continue; + } + const props = elementPropsToReflect[el.tagName]; + for (const prop of props) { + const value = (el as unknown as Record)[prop]; + if (typeof value === 'string' && value.length > 0 && !el.hasAttribute(prop)) { + el.setAttribute(prop, value); + } + } + } +}; + const allowedAttributes = ['class', 'id', 'href', 'src', 'name', 'slot']; const blockedTags = ['script', 'style', 'iframe', 'meta', 'link', 'object', 'embed']; +/** + * Properties on custom elements that frameworks (Vue, Angular) often + * set as DOM properties rather than attributes. `cloneNode` only copies + * attributes, so these values are lost when slotted content is cloned + * into an overlay. For each known custom element, we mirror the listed + * properties onto attributes so the cloned copy still has the data it + * needs to render. + * + * Keyed by uppercased tagName so the lookup matches `Element.tagName`. + */ +const elementPropsToReflect: Record = { + 'ION-ICON': ['icon', 'name', 'src', 'ios', 'md'], +}; export class IonicSafeString { constructor(public value: string) {} diff --git a/core/src/utils/sanitization/test/sanitization.spec.ts b/core/src/utils/sanitization/test/sanitization.spec.ts index 295dd306279..e0e11463385 100644 --- a/core/src/utils/sanitization/test/sanitization.spec.ts +++ b/core/src/utils/sanitization/test/sanitization.spec.ts @@ -1,4 +1,4 @@ -import { IonicSafeString, sanitizeDOMString } from '..'; +import { IonicSafeString, reflectPropertiesToAttributes, sanitizeDOMString, sanitizeDOMTree } from '..'; describe('sanitizeDOMString', () => { it('disable sanitizer', () => { @@ -64,6 +64,150 @@ describe('sanitizeDOMString', () => { }); }); +describe('sanitizeDOMTree', () => { + beforeEach(() => { + enableSanitizer(true); + }); + + it('should strip a blocked '; + + sanitizeDOMTree(root); + + expect(root.querySelector('script')).toBeNull(); + expect(root.querySelector('p')).not.toBeNull(); + }); + + it('should strip every blocked tag type', () => { + const root = document.createElement('div'); + root.innerHTML = ` + + + + + + + + keep + `; + + sanitizeDOMTree(root); + + for (const blocked of ['script', 'style', 'iframe', 'meta', 'link', 'object', 'embed']) { + expect(root.querySelector(blocked)).toBeNull(); + } + expect(root.querySelector('span')).not.toBeNull(); + }); + + it('should strip blocked elements nested deep in the tree', () => { + const root = document.createElement('div'); + root.innerHTML = '
keep
'; + + sanitizeDOMTree(root); + + expect(root.querySelector('script')).toBeNull(); + expect(root.querySelector('span')?.textContent).toBe('keep'); + }); + + it('should remove on* event-handler attributes', () => { + const root = document.createElement('div'); + root.innerHTML = ''; + + sanitizeDOMTree(root); + + const button = root.querySelector('button')!; + expect(button.hasAttribute('onclick')).toBe(false); + expect(button.hasAttribute('onmouseover')).toBe(false); + }); + + it('should strip javascript: URLs while keeping the element', () => { + const root = document.createElement('div'); + root.innerHTML = 'link'; + + sanitizeDOMTree(root); + + const anchor = root.querySelector('a')!; + expect(anchor).not.toBeNull(); + expect(anchor.hasAttribute('href')).toBe(false); + }); + + it('should preserve component attributes like size, color, and shape', () => { + const root = document.createElement('div'); + root.innerHTML = 'button'; + + sanitizeDOMTree(root); + + const button = root.querySelector('ion-button')!; + expect(button.getAttribute('size')).toBe('small'); + expect(button.getAttribute('color')).toBe('primary'); + expect(button.getAttribute('shape')).toBe('round'); + }); + + it('should be a no-op when the sanitizer is disabled', () => { + enableSanitizer(false); + const root = document.createElement('div'); + root.innerHTML = ''; + + sanitizeDOMTree(root); + + expect(root.querySelector('script')).not.toBeNull(); + expect(root.querySelector('button')!.hasAttribute('onclick')).toBe(true); + }); +}); + +describe('reflectPropertiesToAttributes', () => { + it('should reflect a known DOM property onto its attribute', () => { + const icon = document.createElement('ion-icon'); + (icon as any).name = 'star'; + + reflectPropertiesToAttributes(icon); + + expect(icon.getAttribute('name')).toBe('star'); + }); + + it('should reflect properties on a nested element', () => { + const root = document.createElement('div'); + const icon = document.createElement('ion-icon'); + (icon as any).icon = 'logo-ionic'; + root.appendChild(icon); + + reflectPropertiesToAttributes(root); + + expect(icon.getAttribute('icon')).toBe('logo-ionic'); + }); + + it('should not overwrite an attribute that is already present', () => { + const icon = document.createElement('ion-icon'); + icon.setAttribute('name', 'existing'); + (icon as any).name = 'from-property'; + + reflectPropertiesToAttributes(icon); + + expect(icon.getAttribute('name')).toBe('existing'); + }); + + it('should ignore empty-string and non-string property values', () => { + const icon = document.createElement('ion-icon'); + (icon as any).name = ''; + (icon as any).icon = 42; + + reflectPropertiesToAttributes(icon); + + expect(icon.hasAttribute('name')).toBe(false); + expect(icon.hasAttribute('icon')).toBe(false); + }); + + it('should leave elements without reflected properties untouched', () => { + const div = document.createElement('div'); + (div as any).name = 'value'; + + reflectPropertiesToAttributes(div); + + expect(div.hasAttribute('name')).toBe(false); + }); +}); + const enableSanitizer = (enable = true) => { (window as any).Ionic = {}; (window as any).Ionic.config = {}; diff --git a/core/src/utils/select-option-render.tsx b/core/src/utils/select-option-render.tsx index a8e11e3302f..bd52d9a7b2f 100644 --- a/core/src/utils/select-option-render.tsx +++ b/core/src/utils/select-option-render.tsx @@ -1,9 +1,8 @@ +import type { VNode } from '@stencil/core'; import { h } from '@stencil/core'; import type { RichContentOption as RichContentOpt } from '../components/select/select-interface'; -import { sanitizeDOMString } from './sanitization'; - interface RichContentOption extends RichContentOpt { /** Unique identifier for stable virtual DOM keys across re-renders. */ id: string; @@ -12,18 +11,66 @@ interface RichContentOption extends RichContentOpt { } /** - * Cache that maps rendered span elements to the source HTMLElement - * they were cloned from. This prevents flickering when a user - * selects an option that has rich content, as the content will only be - * re-rendered if the source HTMLElement changes. + * Converts a DOM node into a Stencil VNode (or text string) so the + * resulting tree is rendered through the component's normal render + * path. Rendering through Stencil ensures that scoped CSS classes + * (e.g. `sc-ion-action-sheet-ionic`) are applied to every element. + * + * Highly recommended to pre-sanitize the source DOM (see + * `getOptionContent` in select.tsx). This function performs pure + * structural conversion — no security filtering. + * + * Preserves attributes only — properties set imperatively on the source + * element (e.g. `input.value` after a user types) won't carry through. + * In practice this isn't a concern: interactive controls shouldn't + * appear in select-option rich content since they'd nest inside the + * overlay's button/radio/checkbox wrapper, which is invalid HTML and + * an accessibility issue. + * + * @param node - The DOM node to convert. Text nodes become strings, + * element nodes become VNodes, and any other node types are skipped. + * @param keyPrefix - String prefix used to build a stable VNode key, + * so Stencil's diff can preserve elements across re-renders. + * @param index - Position of this node among its siblings. Combined + * with `keyPrefix` to form the final unique key. + * @returns The converted VNode, a text string, or `null` if the node + * type isn't supported. + * + * @internal Exported only so it can be unit tested; not part of the + * public API. */ -const contentCache = new WeakMap(); +export const cloneToVNode = (node: Node, keyPrefix: string, index: number): VNode | string | null => { + if (node.nodeType === Node.TEXT_NODE) { + return node.textContent ?? ''; + } + + if (node.nodeType !== Node.ELEMENT_NODE) { + return null; + } + + const el = node as Element; + const tag = el.tagName.toLowerCase(); + const key = `${keyPrefix}-${index}`; + + const attrs: Record = { key }; + for (let i = 0; i < el.attributes.length; i++) { + const attr = el.attributes.item(i)!; + attrs[attr.name] = attr.value; + } + + const children = Array.from(el.childNodes) + .map((child, i) => cloneToVNode(child, key, i)) + .filter((c): c is VNode | string => c !== null); + + return h(tag as any, attrs, children as any); +}; /** - * Renders cloned DOM content into an element via a ref callback. - * The content is only cloned when the source element changes, - * preventing flicker caused by destroying and recreating web - * components (e.g., ion-avatar) on every re-render cycle. + * Renders cloned DOM content as Stencil JSX. Walking the source DOM + * into VNodes (rather than injecting it via innerHTML) keeps the + * content inside Stencil's render path, so scoped CSS classes are + * applied automatically and component attributes like `size` or + * `color` survive intact. * * Span elements should be used when this content renders within buttons, * depending on the select interface. Buttons can only have phrasing @@ -31,29 +78,16 @@ const contentCache = new WeakMap(); * * @param id - Unique identifier for generating stable virtual DOM keys. * @param content - The HTMLElement container whose child nodes will be cloned. - * @param className - CSS class applied to the wrapper span. + * @param className - CSS class applied to the wrapper element. * @param useSpan - Whether to use a span element instead of a div for the wrapper. */ const renderClonedContent = (id: string, content: HTMLElement, className: string, useSpan = false) => { const Tag = useSpan ? 'span' : 'div'; + const keyPrefix = `${className}-${id}`; return ( - { - if (el) { - const cached = contentCache.get(el); - // Skip if this element already has clones from the same source - if (cached === content) { - return; - } - - const sanitized = sanitizeDOMString(content.innerHTML); - el.innerHTML = sanitized ?? ''; - contentCache.set(el, content); - } - }} - > + + {Array.from(content.childNodes).map((child, i) => cloneToVNode(child, keyPrefix, i))} + ); }; @@ -109,7 +143,7 @@ export const renderOptionLabel = ( // Render label with rich content (start, end, description) return ( - + {startContent && renderClonedContent(id, startContent, 'select-option-start', useSpan)} {labelEl} diff --git a/core/src/utils/test/select-option-render.spec.tsx b/core/src/utils/test/select-option-render.spec.tsx new file mode 100644 index 00000000000..8fff2d40651 --- /dev/null +++ b/core/src/utils/test/select-option-render.spec.tsx @@ -0,0 +1,153 @@ +import type { VNode } from '@stencil/core'; + +import { cloneToVNode } from '../select-option-render'; + +/** + * `cloneToVNode` returns Stencil's internal VNode object, whose fields are + * name-mangled (`$tag$`, `$attrs$`, etc.). Casting to this shape keeps the + * assertions readable without depending on the public `VNode` type, which + * does not expose those runtime fields. + */ +interface RuntimeVNode { + $tag$: string | null; + $text$: string | null; + $attrs$: Record | null; + $children$: RuntimeVNode[] | null; + $key$: string | null; +} + +const asVNode = (value: VNode | string | null): RuntimeVNode => value as unknown as RuntimeVNode; + +describe('cloneToVNode', () => { + describe('text nodes', () => { + it('should return the text content of a text node as a string', () => { + const node = document.createTextNode('hello world'); + + expect(cloneToVNode(node, 'prefix', 0)).toBe('hello world'); + }); + + it('should return an empty string when text content is empty', () => { + const node = document.createTextNode(''); + + expect(cloneToVNode(node, 'prefix', 0)).toBe(''); + }); + }); + + describe('unsupported nodes', () => { + it('should return null for a comment node', () => { + const node = document.createComment('a comment'); + + expect(cloneToVNode(node, 'prefix', 0)).toBeNull(); + }); + }); + + describe('element nodes', () => { + it('should convert an element to a VNode with the lowercased tag name', () => { + const el = document.createElement('SPAN'); + + const vnode = asVNode(cloneToVNode(el, 'prefix', 0)); + + expect(vnode.$tag$).toBe('span'); + }); + + it('should build a stable key from the prefix and index', () => { + const el = document.createElement('div'); + + const vnode = asVNode(cloneToVNode(el, 'prefix', 3)); + + expect(vnode.$key$).toBe('prefix-3'); + expect(vnode.$attrs$?.key).toBe('prefix-3'); + }); + + it('should copy all attributes from the source element', () => { + const el = document.createElement('span'); + el.setAttribute('class', 'foo bar'); + el.setAttribute('data-value', '42'); + el.setAttribute('aria-label', 'label'); + + const vnode = asVNode(cloneToVNode(el, 'prefix', 0)); + + expect(vnode.$attrs$).toEqual({ + key: 'prefix-0', + class: 'foo bar', + 'data-value': '42', + 'aria-label': 'label', + }); + }); + + it('should recursively convert child element nodes', () => { + const el = document.createElement('div'); + el.appendChild(document.createElement('span')); + el.appendChild(document.createElement('img')); + + const vnode = asVNode(cloneToVNode(el, 'prefix', 0)); + + expect(vnode.$children$).toHaveLength(2); + expect(vnode.$children$?.[0].$tag$).toBe('span'); + expect(vnode.$children$?.[1].$tag$).toBe('img'); + }); + + it('should derive child keys from the parent key', () => { + const el = document.createElement('div'); + el.appendChild(document.createElement('span')); + + const vnode = asVNode(cloneToVNode(el, 'prefix', 2)); + + // Parent key is `prefix-2`, so the first child key is `prefix-2-0` + expect(vnode.$children$?.[0].$key$).toBe('prefix-2-0'); + }); + + it('should preserve text child content', () => { + const el = document.createElement('span'); + el.appendChild(document.createTextNode('inner text')); + + const vnode = asVNode(cloneToVNode(el, 'prefix', 0)); + + // `cloneToVNode` returns the text as a string, which `h` wraps into a + // text VNode (`$tag$` null, content on `$text$`). + expect(vnode.$children$).toHaveLength(1); + expect(vnode.$children$?.[0].$tag$).toBeNull(); + expect(vnode.$children$?.[0].$text$).toBe('inner text'); + }); + + it('should filter out unsupported child nodes', () => { + const el = document.createElement('div'); + el.appendChild(document.createElement('span')); + el.appendChild(document.createComment('skip me')); + el.appendChild(document.createTextNode('keep me')); + + const vnode = asVNode(cloneToVNode(el, 'prefix', 0)); + + // The comment node is dropped, leaving the span and the text + expect(vnode.$children$).toHaveLength(2); + expect(vnode.$children$?.[0].$tag$).toBe('span'); + expect(vnode.$children$?.[1].$text$).toBe('keep me'); + }); + + it('should convert a deeply nested structure', () => { + const el = document.createElement('div'); + const child = document.createElement('span'); + const grandchild = document.createElement('strong'); + grandchild.appendChild(document.createTextNode('deep')); + child.appendChild(grandchild); + el.appendChild(child); + + const vnode = asVNode(cloneToVNode(el, 'prefix', 0)); + + const span = vnode.$children$?.[0]; + const strong = span?.$children$?.[0]; + expect(span?.$tag$).toBe('span'); + expect(strong?.$tag$).toBe('strong'); + expect(strong?.$children$?.[0].$text$).toBe('deep'); + }); + + it('should produce no children for an empty element', () => { + const el = document.createElement('div'); + + const vnode = asVNode(cloneToVNode(el, 'prefix', 0)); + + // `h` normalizes an empty children array to `null` + expect(vnode.$children$).toBeNull(); + }); + }); +}); diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index c1dfbec20a0..1675c9be471 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -2275,14 +2275,14 @@ export declare interface IonSelectModal extends Components.IonSelectModal {} @ProxyCmp({ - inputs: ['description', 'disabled', 'mode', 'theme', 'value'] + inputs: ['description', 'disabled', 'justify', 'labelPlacement', 'mode', 'theme', 'value'] }) @Component({ selector: 'ion-select-option', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['description', 'disabled', 'mode', 'theme', 'value'], + inputs: ['description', 'disabled', 'justify', 'labelPlacement', 'mode', 'theme', 'value'], }) export class IonSelectOption { protected el: HTMLIonSelectOptionElement; diff --git a/packages/angular/standalone/src/directives/proxies.ts b/packages/angular/standalone/src/directives/proxies.ts index ef1a118f4f7..ba3f53b5633 100644 --- a/packages/angular/standalone/src/directives/proxies.ts +++ b/packages/angular/standalone/src/directives/proxies.ts @@ -2011,14 +2011,14 @@ export declare interface IonSelectModal extends Components.IonSelectModal {} @ProxyCmp({ defineCustomElementFn: defineIonSelectOption, - inputs: ['description', 'disabled', 'mode', 'theme', 'value'] + inputs: ['description', 'disabled', 'justify', 'labelPlacement', 'mode', 'theme', 'value'] }) @Component({ selector: 'ion-select-option', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['description', 'disabled', 'mode', 'theme', 'value'], + inputs: ['description', 'disabled', 'justify', 'labelPlacement', 'mode', 'theme', 'value'], standalone: true }) export class IonSelectOption { diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 116c608e061..6df720f8d8f 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -1010,7 +1010,9 @@ export const IonSelectModal: StencilVueComponent = /*@__PURE export const IonSelectOption: StencilVueComponent = /*@__PURE__*/ defineContainer('ion-select-option', defineIonSelectOption, [ 'disabled', 'value', - 'description' + 'description', + 'labelPlacement', + 'justify' ]);