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..0dfa5f3a29e 100644 --- a/core/src/components/action-sheet/action-sheet.common.scss +++ b/core/src/components/action-sheet/action-sheet.common.scss @@ -1,4 +1,4 @@ -@import "./action-sheet.vars"; +@use "../../themes/mixins" as mixins; // Action Sheet // -------------------------------------------------- @@ -41,25 +41,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 +64,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 +78,6 @@ min-height: var(--min-height); max-height: var(--max-height); - z-index: $z-index-overlay-wrapper; pointer-events: none; } @@ -109,6 +105,10 @@ opacity: 0.4; } +.action-sheet-button:disabled ion-icon { + color: currentColor; +} + .action-sheet-button-inner { display: flex; @@ -177,7 +177,7 @@ // -------------------------------------------------- .action-sheet-button::after { - @include button-state(); + @include mixins.button-state(); } // Action Sheet: Selected @@ -209,7 +209,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 +217,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 @@ -243,10 +249,30 @@ align-items: center; } +.action-sheet-button-label-has-rich-content, .select-option-content { flex: 1; } +.select-option-start, +.select-option-end { + display: flex; + + align-items: center; +} + +/** + * Cap slotted children so they can't stretch the option + * row out of proportion, keeping rows visually uniform + * regardless of the content. ion-icon is excluded because + * its `contain: strict` makes intrinsic sizes resolve to 0, + * which would collapse the icon to zero width. + */ +.select-option-start > *:not(ion-icon), +.select-option-end > *:not(ion-icon) { + max-width: fit-content; +} + .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..b2877c9ce6e 100644 --- a/core/src/components/action-sheet/action-sheet.ionic.scss +++ b/core/src/components/action-sheet/action-sheet.ionic.scss @@ -1,10 +1,111 @@ @use "../../themes/ionic/ionic.globals.scss" as globals; +@use "../../themes/mixins" as mixins; @use "./action-sheet.common"; -@use "./action-sheet.md" as action-sheet-md; // Ionic Action Sheet // -------------------------------------------------- +:host { + --background: #{globals.$ion-bg-surface-default}; + --backdrop-opacity: 0.7; + --button-background: transparent; + --button-background-selected: #{globals.$ion-semantics-primary-100}; + --button-background-selected-opacity: 1; + --button-background-activated: #{globals.$ion-semantics-primary-100}; + --button-background-activated-opacity: 1; + --button-background-hover: #{globals.$ion-semantics-primary-100}; + --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-wrapper { + @include mixins.margin(var(--ion-safe-area-top, 0), auto, 0, auto); +} + +.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); + + 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-300, + globals.$ion-space-400, + globals.$ion-space-300, + 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); + @include globals.typography(globals.$ion-body-md-regular); + + color: var(--color, globals.$ion-text-default); +} + +.action-sheet-button-inner { + justify-content: flex-start; +} + +.action-sheet-selected { + font-weight: bold; +} + // Action Sheet: Select Option // -------------------------------------------------- @@ -12,11 +113,24 @@ gap: globals.$ion-space-300; } +.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. + */ +.select-option-start > *, +.select-option-end > * { + max-height: globals.$ion-scale-1200; +} + .select-option-description { @include globals.typography(globals.$ion-body-md-regular); - @include globals.padding(0); + @include globals.padding(globals.$ion-space-0); color: globals.$ion-text-subtle; - - font-size: globals.$ion-font-size-350; } diff --git a/core/src/components/action-sheet/action-sheet.ios.scss b/core/src/components/action-sheet/action-sheet.ios.scss index 94b98447981..04485fe95c3 100644 --- a/core/src/components/action-sheet/action-sheet.ios.scss +++ b/core/src/components/action-sheet/action-sheet.ios.scss @@ -215,3 +215,16 @@ color: $action-sheet-ios-button-destructive-text-color; } } + +// Action Sheet: Select Option +// -------------------------------------------------- + +/** + * Cap slotted children so they can't stretch the option + * row out of proportion, keeping rows visually uniform + * regardless of the content. + */ +.select-option-start > *, +.select-option-end > * { + max-height: $action-sheet-ios-select-option-slot-size; +} diff --git a/core/src/components/action-sheet/action-sheet.ios.vars.scss b/core/src/components/action-sheet/action-sheet.ios.vars.scss index 0d2302b7183..6c301624c53 100644 --- a/core/src/components/action-sheet/action-sheet.ios.vars.scss +++ b/core/src/components/action-sheet/action-sheet.ios.vars.scss @@ -161,3 +161,6 @@ $action-sheet-ios-group-translucent-filter: saturate(280%) blur(20px); /// @prop - Filter of the translucent action-sheet button $action-sheet-ios-button-translucent-filter: saturate(120%); + +/// @prop - Maximum size of slotted children rendered in a select option's start/end slot +$action-sheet-ios-select-option-slot-size: dynamic-font-max(24px, 2); diff --git a/core/src/components/action-sheet/action-sheet.md.scss b/core/src/components/action-sheet/action-sheet.md.scss index e46f06085b3..ef4134c2c63 100644 --- a/core/src/components/action-sheet/action-sheet.md.scss +++ b/core/src/components/action-sheet/action-sheet.md.scss @@ -1,7 +1,7 @@ @import "./action-sheet.native"; @import "./action-sheet.md.vars"; -// Material Design Action Sheet Title +// Material Design Action Sheet // ----------------------------------------- :host { @@ -110,3 +110,16 @@ .action-sheet-selected { font-weight: bold; } + +// Action Sheet: Select Option +// -------------------------------------------------- + +/** + * Cap slotted children so they can't stretch the option + * row out of proportion, keeping rows visually uniform + * regardless of the content. + */ +.select-option-start > *, +.select-option-end > * { + max-height: 24px; +} diff --git a/core/src/components/action-sheet/action-sheet.native.scss b/core/src/components/action-sheet/action-sheet.native.scss index affa6aeb126..cdd91732bc6 100644 --- a/core/src/components/action-sheet/action-sheet.native.scss +++ b/core/src/components/action-sheet/action-sheet.native.scss @@ -1,4 +1,4 @@ -@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 "./action-sheet.common"; @@ -6,10 +6,28 @@ // Action Sheet: Native // -------------------------------------------------- +:host { + font-family: native.$font-family-base; + + z-index: native.$z-index-overlay; +} + +.action-sheet-wrapper { + z-index: native.$z-index-overlay-wrapper; +} + +// Action Sheet: Select Option +// -------------------------------------------------- + .action-sheet-button-label { gap: 12px; } +.select-option-start, +.select-option-end { + gap: 8px; +} + .select-option-description { @include mixins.padding(5px, 0, 0, 0); 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..756e33a6700 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..8251dc17fe4 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..00ab8017235 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..cf2d63235bb 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..290dd400493 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..8ced6234f39 100644 --- a/core/src/components/alert/alert.common.scss +++ b/core/src/components/alert/alert.common.scss @@ -1,4 +1,5 @@ -@import "./alert.vars"; +@use "../../themes/functions.font" as font; +@use "../../themes/mixins" as mixins; // Alert // -------------------------------------------------- @@ -17,14 +18,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 +35,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 +45,7 @@ } :host(.alert-top) { - @include padding(50px, null, null, null); + @include mixins.padding(50px, null, null, null); align-items: flex-start; } @@ -69,17 +67,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 +137,7 @@ } .alert-input { - @include padding(10px, 0); + @include mixins.padding(10px, 0); width: 100%; @@ -166,24 +163,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 +190,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 +240,8 @@ } .alert-tappable { - @include margin(0); - @include padding(0); + @include mixins.margin(0); + @include mixins.padding(0); display: flex; @@ -244,7 +275,7 @@ } textarea.alert-input { - min-height: $alert-input-min-height; + min-height: 37px; resize: none; } @@ -258,10 +289,38 @@ textarea.alert-input { align-items: center; } +/** + * Rich-content rows have inner pieces (start, content, description, end) + * that need the label to span the available row width. + */ +.alert-radio-label-has-rich-content, +.alert-checkbox-label-has-rich-content { + flex: 1; +} + .select-option-content { flex: 1; } +.select-option-start, +.select-option-end { + display: flex; + + align-items: center; +} + +/** + * Cap slotted children so they can't stretch the option + * row out of proportion, keeping rows visually uniform + * regardless of the content. ion-icon is excluded because + * its `contain: strict` makes intrinsic sizes resolve to 0, + * which would collapse the icon to zero width. + */ +.select-option-start > *:not(ion-icon), +.select-option-end > *:not(ion-icon) { + max-width: fit-content; +} + .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..1e493afc787 100644 --- a/core/src/components/alert/alert.ionic.scss +++ b/core/src/components/alert/alert.ionic.scss @@ -1,10 +1,373 @@ @use "../../themes/ionic/ionic.globals.scss" as globals; +@use "../../themes/mixins" as mixins; @use "./alert.common"; -@use "./alert.md" as alert-md; // Ionic Alert // -------------------------------------------------- +: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, +.alert-tappable[aria-checked="true"] { + background: globals.$ion-semantics-primary-100; +} + +// Ionic Alert Header +// -------------------------------------------------- + +.alert-head { + @include mixins.padding(globals.$ion-space-400); + + text-align: start; +} + +.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 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-300, + globals.$ion-space-700, + globals.$ion-space-300, + globals.$ion-scale-1400 + ); + + // 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-700); + + 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-700, globals.$ion-scale-1400); +} + +// 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); + + display: flex; + position: relative; + + 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-700); +} + +.radio-label-placement-end .alert-radio-icon { + @include globals.position-horizontal(globals.$ion-space-700, 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-300, + globals.$ion-space-700, + globals.$ion-space-300, + globals.$ion-scale-1400 + ); + + // 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-700); + + 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-700, globals.$ion-scale-1400); +} + +// 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; + + 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-700, null); +} + +.checkbox-label-placement-start .alert-checkbox-icon { + @include globals.position-horizontal(null, globals.$ion-space-700); +} + +.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; + text-transform: uppercase; + + overflow: hidden; +} + +.alert-button .alert-button-inner { + justify-content: flex-end; +} + +/** + * 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)}; + } +} + // Alert: Select Option // -------------------------------------------------- @@ -13,9 +376,24 @@ gap: globals.$ion-space-300; } +.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. + */ +.select-option-start > *, +.select-option-end > * { + max-height: globals.$ion-scale-1200; +} + .select-option-description { @include globals.typography(globals.$ion-body-md-regular); - @include globals.padding(0); + @include mixins.padding(globals.$ion-space-0); color: globals.$ion-text-subtle; diff --git a/core/src/components/alert/alert.ios.scss b/core/src/components/alert/alert.ios.scss index 2671dc0940b..091b9caf69c 100644 --- a/core/src/components/alert/alert.ios.scss +++ b/core/src/components/alert/alert.ios.scss @@ -166,9 +166,6 @@ $alert-ios-radio-label-padding-start ); - flex: 1; - order: 0; - color: $alert-ios-radio-label-text-color; } @@ -185,7 +182,7 @@ .alert-radio-icon { position: relative; - order: 1; + flex-shrink: 0; min-width: $alert-ios-radio-min-width; } @@ -194,7 +191,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 +207,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 +228,6 @@ $alert-ios-checkbox-label-padding-start ); - flex: 1; - color: $alert-ios-checkbox-label-text-color; } @@ -231,15 +236,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 +254,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 +278,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 +294,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 +375,7 @@ background-color: $alert-ios-button-background-color-activated; } -// iOS Action Sheet Button: Destructive +// iOS Alert Button: Destructive // --------------------------------------------------- .alert-button-role-destructive, @@ -360,3 +383,16 @@ .alert-button-role-destructive.ion-focused { color: $alert-ios-button-destructive-text-color; } + +// Alert: Select Option +// -------------------------------------------------- + +/** + * Cap slotted children so they can't stretch the option + * row out of proportion, keeping rows visually uniform + * regardless of the content. + */ +.select-option-start > *, +.select-option-end > * { + max-height: $alert-ios-select-option-slot-size; +} diff --git a/core/src/components/alert/alert.ios.vars.scss b/core/src/components/alert/alert.ios.vars.scss index 2cb4fca98af..c1d0539278c 100644 --- a/core/src/components/alert/alert.ios.vars.scss +++ b/core/src/components/alert/alert.ios.vars.scss @@ -316,3 +316,6 @@ $alert-ios-translucent-filter: saturate(180%) blur(20px); /// @prop - Height of the tappable inputs in the checkbox alert $alert-ios-tappable-height: $item-ios-min-height; + +/// @prop - Maximum size of slotted children rendered in a select option's start/end slot +$alert-ios-select-option-slot-size: dynamic-font-max(24px, 2); diff --git a/core/src/components/alert/alert.md.scss b/core/src/components/alert/alert.md.scss index 2fbd0fd8775..185f9671193 100644 --- a/core/src/components/alert/alert.md.scss +++ b/core/src/components/alert/alert.md.scss @@ -164,18 +164,29 @@ $alert-md-radio-label-padding-start ); - 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-left); color: $alert-md-radio-label-text-color; font-size: $alert-md-radio-label-font-size; } +.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 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; @@ -189,6 +200,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 // --------------------------------------------------- @@ -234,22 +255,29 @@ $alert-md-checkbox-label-padding-start ); - flex: 1; - // 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-left); color: $alert-md-checkbox-label-text-color; font-size: $alert-md-checkbox-label-font-size; } +.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 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; @@ -264,6 +292,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 +369,7 @@ overflow: hidden; } -.alert-button-inner { +.alert-button .alert-button-inner { justify-content: $alert-md-button-group-justify-content; } @@ -345,3 +383,16 @@ --max-height: #{$alert-md-max-height-tablet}; } } + +// Alert: Select Option +// -------------------------------------------------- + +/** + * Cap slotted children so they can't stretch the option + * row out of proportion, keeping rows visually uniform + * regardless of the content. + */ +.select-option-start > *, +.select-option-end > * { + max-height: 24px; +} diff --git a/core/src/components/alert/alert.native.scss b/core/src/components/alert/alert.native.scss index e2d5a87b8a5..d7cf5c086d3 100644 --- a/core/src/components/alert/alert.native.scss +++ b/core/src/components/alert/alert.native.scss @@ -1,4 +1,4 @@ -@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 "./alert.common"; @@ -6,11 +6,34 @@ // Alert: Native // -------------------------------------------------- +:host { + font-family: native.$font-family-base; + + z-index: native.$z-index-overlay; +} + +.alert-wrapper { + z-index: native.$z-index-overlay-wrapper; +} + +.alert-button.ion-focused, +.alert-tappable.ion-focused { + background: native.$background-color-step-100; +} + +// Alert: Select Option +// -------------------------------------------------- + .alert-radio-label, .alert-checkbox-label { gap: 12px; } +.select-option-start, +.select-option-end { + gap: 8px; +} + .select-option-description { @include mixins.padding(5px, 0, 0, 0); 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/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..8c0d45a4bbd 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..9949d816f28 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..95cbabb9230 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..893fcca3859 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..e5b8db5b3c6 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..b31df9f6116 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..71e54f8f337 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..97c46d4cdc0 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..2e97682108a 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..2fda93ec795 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..0f564f091b2 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..2f403c2d82a 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..1282255ce53 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..7b53c054102 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..a412cb20932 100644 --- a/core/src/components/select-modal/select-modal.common.scss +++ b/core/src/components/select-modal/select-modal.common.scss @@ -8,12 +8,57 @@ // Select Modal: Select Option // -------------------------------------------------- -.select-option-label { +/** + * 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; +} + +.select-option-start, +.select-option-end { display: flex; align-items: center; } +/** + * Cap slotted children so they can't stretch the option + * row out of proportion, keeping rows visually uniform + * regardless of the content. ion-icon is excluded because + * its `contain: strict` makes intrinsic sizes resolve to 0, + * which would collapse the icon to zero width. + */ +.select-option-start > *:not(ion-icon), +.select-option-end > *:not(ion-icon) { + max-width: fit-content; +} + .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..7c22f663b06 100644 --- a/core/src/components/select-modal/select-modal.ionic.scss +++ b/core/src/components/select-modal/select-modal.ionic.scss @@ -12,7 +12,14 @@ // ---------------------------------------------------------------- ion-item { - --border-width: 0; + --border-width: #{globals.$ion-border-size-0}; + --background-focused: transparent; + --background-focused-opacity: 1; + --background-hover: #{globals.$ion-semantics-primary-100}; + --background-hover-opacity: 1; + --background-activated: #{globals.$ion-semantics-primary-100}; + --background-activated-opacity: 1; + --border-radius: #{globals.$ion-border-radius-400}; } ion-item.ion-focused::part(native)::after { @@ -20,6 +27,12 @@ ion-item.ion-focused::part(native)::after { border: none; } +ion-item.ion-focused.item-checkbox-checked, +ion-item.ion-focused.item-radio-checked { + --background-focused: #{globals.$ion-semantics-primary-100}; + --background-focused-opacity: 1; +} + // Toolbar // ---------------------------------------------------------------- @@ -35,7 +48,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 @@ -52,7 +65,6 @@ 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}; } // Content @@ -71,11 +83,6 @@ 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 @@ -85,6 +92,21 @@ ion-content { gap: globals.$ion-space-300; } +.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. + */ +.select-option-start > *, +.select-option-end > * { + max-height: globals.$ion-scale-1200; +} + .select-option-description { @include globals.typography(globals.$ion-body-md-regular); diff --git a/core/src/components/select-modal/select-modal.ios.scss b/core/src/components/select-modal/select-modal.ios.scss index abac9c8220b..a55fd69701a 100644 --- a/core/src/components/select-modal/select-modal.ios.scss +++ b/core/src/components/select-modal/select-modal.ios.scss @@ -22,3 +22,20 @@ ion-radio::after { content: ""; } + +// Select Modal: Select Option +// -------------------------------------------------- + +/** + * Cap slotted children so they can't stretch the option + * row out of proportion, keeping rows visually uniform + * regardless of the content. + */ +.select-option-start > *, +.select-option-end > * { + max-height: dynamic-font-max(24px, 2); +} + +.select-option-has-rich-content { + @include padding-horizontal(null, $item-ios-padding-end); +} diff --git a/core/src/components/select-modal/select-modal.md.scss b/core/src/components/select-modal/select-modal.md.scss index 260f6aba5be..919390f9c01 100644 --- a/core/src/components/select-modal/select-modal.md.scss +++ b/core/src/components/select-modal/select-modal.md.scss @@ -28,3 +28,16 @@ ion-item { --background-hover: #{$item-md-color}; --color: #{ion-color(primary, base)}; } + +// Select Modal: Select Option +// -------------------------------------------------- + +/** + * Cap slotted children so they can't stretch the option + * row out of proportion, keeping rows visually uniform + * regardless of the content. + */ +.select-option-start > *, +.select-option-end > * { + max-height: 24px; +} diff --git a/core/src/components/select-modal/select-modal.native.scss b/core/src/components/select-modal/select-modal.native.scss index 29b81819fcf..e844a3cb15f 100644 --- a/core/src/components/select-modal/select-modal.native.scss +++ b/core/src/components/select-modal/select-modal.native.scss @@ -6,10 +6,18 @@ // Select Modal: Native // -------------------------------------------------- +// Select Modal: Select Option +// -------------------------------------------------- + .select-option-label { gap: 12px; } +.select-option-start, +.select-option-end { + gap: 8px; +} + .select-option-description { @include mixins.padding(5px, 0, 0, 0); diff --git a/core/src/components/select-modal/select-modal.tsx b/core/src/components/select-modal/select-modal.tsx index ca21e73a1f0..41eea2740b2 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'; @@ -49,7 +50,11 @@ export class SelectModal implements ComponentInterface { @Prop() options: SelectModalOption[] = []; - private closeModal() { + private closeModal(isOptionDisabled = false) { + if (isOptionDisabled) { + return; + } + const modal = this.el.closest('ion-modal'); if (modal) { @@ -115,6 +120,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 +133,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 +141,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()} + justify={richOption.justify ?? defaultJustify} + labelPlacement={richOption.labelPlacement ?? defaultLabelPlacement} + onClick={() => this.closeModal(option.disabled)} onKeyDown={(ev) => { if (ev.key === 'Enter' && !ev.repeat) { this.pendingEnterTarget = ev.currentTarget as HTMLElement; @@ -158,12 +172,12 @@ export class SelectModal implements ComponentInterface { onKeyUp={(ev) => { if (ev.key === ' ') { // Space selects and dismisses in one press. - this.closeModal(); + this.closeModal(option.disabled); } else if (ev.key === 'Enter') { const shouldClose = this.pendingEnterTarget === ev.currentTarget; this.pendingEnterTarget = null; if (shouldClose) { - this.closeModal(); + this.closeModal(option.disabled); } } }} @@ -178,6 +192,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 +201,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 +209,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..cb702b2adc9 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..4c7369d690d 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..11c7fccd9bc 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..ea4e6da6642 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..1a5c734ed99 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..126d833c4a8 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..9fa3e299445 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..68a16081370 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..641cbfdf3c7 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..03dabf1f699 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.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/select-option.e2e.ts b/core/src/components/select-option/test/label-placement/select-option.e2e.ts new file mode 100644 index 00000000000..2eab64cb60a --- /dev/null +++ b/core/src/components/select-option/test/label-placement/select-option.e2e.ts @@ -0,0 +1,96 @@ +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 JUSTIFY_VARIANTS = ['start', 'end', 'space-between'] 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'), () => { + test.describe('alert interface', () => { + for (const placement of ['start', 'end'] as const) { + test(`placement ${placement}`, async ({ page }) => { + await setContentForInterface(page, 'alert', placement, config); + await page.addStyleTag({ content: ALERT_SIZE_OVERRIDES }); + + const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent'); + await page.locator('#select').click(); + await ionAlertDidPresent.next(); + + const alertWrapper = page.locator('ion-alert .alert-wrapper'); + await expect(alertWrapper).toHaveScreenshot(screenshot(`select-option-label-alert-${placement}`)); + }); + } + }); + + test.describe('popover interface', () => { + for (const placement of ['start', 'end'] as const) { + test(`placement ${placement}`, async ({ page }) => { + await setContentForInterface(page, 'popover', placement, config); + + const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent'); + await page.locator('#select').click(); + await ionPopoverDidPresent.next(); + + const popover = page.locator('ion-popover'); + await expect(popover).toHaveScreenshot(screenshot(`select-option-label-popover-${placement}`)); + }); + } + }); + + test.describe('modal interface', () => { + for (const placement of ['start', 'end'] as const) { + test(`placement ${placement}`, async ({ page }) => { + await setContentForInterface(page, 'modal', placement, config); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + await page.locator('#select').click(); + await ionModalDidPresent.next(); + + const modal = page.locator('ion-modal'); + await expect(modal).toHaveScreenshot(screenshot(`select-option-label-modal-${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..867586fa9de 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..23e6a4cadf5 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..34ff27dea65 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..f262d9111db 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..abb8c8375c2 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..d63ce62a2ef 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..b5b743045e1 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..08399bde422 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..a6ce6914962 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..974f58b6c96 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..d49bb762d21 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..d5cf4d53d67 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..1f9e6604d2c 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..3851fe5531a 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..e50ecd2eae4 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..d471abb4691 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..b20d9576b41 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..fd7c8a01188 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..4111bbf6365 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..6a399641e01 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..bb75bc8db36 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..ca55f41c63a 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..a1e32533c43 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..316bb30efaf 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..694638caf7f 100644 --- a/core/src/components/select-popover/select-popover.common.scss +++ b/core/src/components/select-popover/select-popover.common.scss @@ -1,15 +1,15 @@ -@import "../../themes/native/native.globals"; +@use "../../themes/mixins" as mixins; // Select Popover // -------------------------------------------------- :host ion-list { - @include margin(0); + @include mixins.margin(0); } ion-list-header, ion-label { - @include margin(0); + @include mixins.margin(0); } /** @@ -25,7 +25,20 @@ ion-label { // Select Popover: Select Option // -------------------------------------------------- -.select-option-label { +/** + * 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; @@ -37,6 +50,38 @@ ion-label { flex-wrap: wrap; } +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; +} + +.select-option-start, +.select-option-end { + display: flex; + + align-items: center; +} + +/** + * Cap slotted children so they can't stretch the option + * row out of proportion, keeping rows visually uniform + * regardless of the content. ion-icon is excluded because + * its `contain: strict` makes intrinsic sizes resolve to 0, + * which would collapse the icon to zero width. + */ +.select-option-start > *:not(ion-icon), +.select-option-end > *:not(ion-icon) { + max-width: fit-content; +} + .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..0a117793383 100644 --- a/core/src/components/select-popover/select-popover.ionic.scss +++ b/core/src/components/select-popover/select-popover.ionic.scss @@ -1,10 +1,53 @@ @use "../../themes/ionic/ionic.globals.scss" as globals; +@use "../../themes/mixins" as mixins; @use "./select-popover.common"; -@use "./select-popover.md" as select-popover-md; // Ionic Select Popover // -------------------------------------------------- +ion-item { + --border-width: #{globals.$ion-border-size-0}; + --background-focused: transparent; + --background-focused-opacity: 1; + --background-hover: #{globals.$ion-semantics-primary-100}; + --background-hover-opacity: 1; + --background-activated: #{globals.$ion-semantics-primary-100}; + --background-activated-opacity: 1; +} + +ion-item.ion-focused.item-checkbox-checked, +ion-item.ion-focused.item-radio-checked { + --background-focused: #{globals.$ion-semantics-primary-100}; + --background-focused-opacity: 1; +} + +// Radio +// ---------------------------------------------------------------- + +ion-list ion-radio::part(container) { + display: none; +} + +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); +} + +// Radio and Checkbox: Checked +// ---------------------------------------------------------------- + +.item-radio-checked, +.item-checkbox-checked { + --background: #{globals.$ion-semantics-primary-100}; +} + // Select Popover: Select Option // -------------------------------------------------- @@ -12,9 +55,24 @@ gap: globals.$ion-space-300; } +.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. + */ +.select-option-start > *, +.select-option-end > * { + max-height: globals.$ion-scale-1200; +} + .select-option-description { @include globals.typography(globals.$ion-body-md-regular); - @include globals.padding(0); + @include mixins.padding(globals.$ion-space-0); color: globals.$ion-text-subtle; diff --git a/core/src/components/select-popover/select-popover.ios.scss b/core/src/components/select-popover/select-popover.ios.scss index de3cfea6135..50f909d94ec 100644 --- a/core/src/components/select-popover/select-popover.ios.scss +++ b/core/src/components/select-popover/select-popover.ios.scss @@ -1,2 +1,15 @@ @import "./select-popover.native"; @import "./select-popover.ios.vars"; + +// Select Popover: Select Option +// -------------------------------------------------- + +/** + * Cap slotted children so they can't stretch the option + * row out of proportion, keeping rows visually uniform + * regardless of the content. + */ +.select-option-start > *, +.select-option-end > * { + max-height: $select-popover-ios-select-option-slot-size; +} diff --git a/core/src/components/select-popover/select-popover.ios.vars.scss b/core/src/components/select-popover/select-popover.ios.vars.scss index 188e3f5f97b..c45d4804054 100644 --- a/core/src/components/select-popover/select-popover.ios.vars.scss +++ b/core/src/components/select-popover/select-popover.ios.vars.scss @@ -3,3 +3,6 @@ // iOS Select Popover // -------------------------------------------------- + +/// @prop - Maximum size of slotted children rendered in a select option's start/end slot +$select-popover-ios-select-option-slot-size: dynamic-font-max(24px, 2); diff --git a/core/src/components/select-popover/select-popover.md.scss b/core/src/components/select-popover/select-popover.md.scss index c7728bcaf04..d65c3cf4e34 100644 --- a/core/src/components/select-popover/select-popover.md.scss +++ b/core/src/components/select-popover/select-popover.md.scss @@ -27,3 +27,16 @@ ion-item { --background-hover: #{$item-md-color}; --color: #{ion-color(primary, base)}; } + +// Select Popover: Select Option +// -------------------------------------------------- + +/** + * Cap slotted children so they can't stretch the option + * row out of proportion, keeping rows visually uniform + * regardless of the content. + */ +.select-option-start > *, +.select-option-end > * { + max-height: 24px; +} diff --git a/core/src/components/select-popover/select-popover.native.scss b/core/src/components/select-popover/select-popover.native.scss index 0b52fafe932..f93ea9ecbcb 100644 --- a/core/src/components/select-popover/select-popover.native.scss +++ b/core/src/components/select-popover/select-popover.native.scss @@ -6,10 +6,18 @@ // Select Popover: Native // -------------------------------------------------- +// Select Popover: Select Option +// -------------------------------------------------- + .select-option-label { gap: 12px; } +.select-option-start, +.select-option-end { + gap: 8px; +} + .select-option-description { @include mixins.padding(5px, 0, 0, 0); diff --git a/core/src/components/select-popover/select-popover.tsx b/core/src/components/select-popover/select-popover.tsx index b7f0f9bb839..38dbd1bd856 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'; @@ -82,7 +83,11 @@ export class SelectPopover implements ComponentInterface { * Dismisses the host popover that the `ion-select-popover` * is rendered within. */ - private dismissParentPopover() { + private dismissParentPopover(isOptionDisabled = false) { + if (isOptionDisabled) { + return; + } + const popover = this.el.closest('ion-popover'); if (popover) { popover.dismiss(); @@ -127,6 +132,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 +141,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 +149,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 +186,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 +199,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 +210,8 @@ export class SelectPopover implements ComponentInterface { return ( this.dismissParentPopover()} + justify={richOption.justify ?? getOverlayLabelJustify(theme, 'radio')} + labelPlacement={richOption.labelPlacement ?? getOverlayLabelPlacement(theme, 'radio')} + onClick={() => this.dismissParentPopover(option.disabled)} onKeyDown={(ev) => { if (ev.key === 'Enter' && !ev.repeat) { this.pendingEnterTarget = ev.currentTarget as HTMLElement; @@ -212,12 +235,12 @@ export class SelectPopover implements ComponentInterface { onKeyUp={(ev) => { if (ev.key === ' ') { // Space selects and dismisses in one press. - this.dismissParentPopover(); + this.dismissParentPopover(option.disabled); } else if (ev.key === 'Enter') { const shouldDismiss = this.pendingEnterTarget === ev.currentTarget; this.pendingEnterTarget = null; if (shouldDismiss) { - this.dismissParentPopover(); + this.dismissParentPopover(option.disabled); } } }} 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..114d5d307e7 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..f54ce4f4962 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..b9ac891364a 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..1ac0704bb5a 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..13c8b49f885 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..dee1f660858 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..b0834156969 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..b6423368abd 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..07f9c36862f 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..3a4e4e53392 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..f6b7673bab0 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,15 +591,11 @@ 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, handler: () => { this.setValue(value); @@ -608,8 +604,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 +635,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 +664,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 +678,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 +1633,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 +1654,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 +1678,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 +1733,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..80a989f72f0 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..5e6ec9899d5 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..69b2954222b 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/rich-content-option/index.html b/core/src/components/select/test/rich-content-option/index.html index 7bdf2881d3a..6b2e25dc017 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,46 @@ 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 + + - + NEW Full Content - This is a span element @@ -146,18 +173,46 @@ 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 + + - + NEW Full Content - This is a span element @@ -194,18 +249,46 @@ 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 + + - + NEW Full Content - This is a span element @@ -242,6 +325,35 @@ 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 + + @@ -259,7 +371,6 @@ Full Content - This is a span element @@ -296,18 +407,46 @@ 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 + + - + NEW Full Content - This is a span element @@ -344,18 +483,47 @@ 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 + + - + NEW Full Content - This is a span element @@ -392,6 +560,35 @@ 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 + + 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..65cee010950 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,82 @@ 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(); + + const firstOption = page.locator('ion-action-sheet .action-sheet-button-label').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(); + + 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(); + + const firstOption = page.locator('ion-modal ion-item').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(); + + 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('.action-sheet-button-label').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 +86,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 +101,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('.alert-radio-button').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 +132,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 +141,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('.alert-checkbox-label').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 +162,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 +181,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 +202,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 +217,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 +239,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 +257,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 +278,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 +293,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 +315,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 +343,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 +358,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 +382,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 +445,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..13537396574 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..4afaf3e6d5f 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..959b8290a20 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..416dd570ab4 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..a7fbb9918d3 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..7913302a9cb 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..95d2664cb46 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..32a5f0ea276 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..8092bd424ee 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..2678e2bd64e 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..0e6c76dffec 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..0fbcac6d812 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..50b5b2e7bab 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..b59d48183c6 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..e35f904d780 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..be6c8ca2b1f 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..61a84a4d270 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..928e5954687 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..7cda533e368 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..4cf0a82b3bb 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..c2989d6c7d2 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..5187c0d15e2 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..2ed6cd9e4ac 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..b12068a1e51 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..d8edd929edc 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..cb47b68838b 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..5b56a37a53d 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..e3e8063e59c 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..f63d66bad44 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..64ebdb6273c 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..eecb78aa84b 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..a5b0dcfc6c1 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..5925d614968 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..258a853c370 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/utils/overlay-control-label.ts b/core/src/utils/overlay-control-label.ts new file mode 100644 index 00000000000..364417123a3 --- /dev/null +++ b/core/src/utils/overlay-control-label.ts @@ -0,0 +1,56 @@ +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/select-option-render.tsx b/core/src/utils/select-option-render.tsx index a8e11e3302f..cce5d4a5956 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,63 @@ 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. */ -const contentCache = new WeakMap(); +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 +75,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 +140,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/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' ]);