diff --git a/core/src/components/action-sheet/action-sheet.common.scss b/core/src/components/action-sheet/action-sheet.common.scss index 7e857346ac9..b5427110f52 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):not(.action-sheet-selected) { color: var(--button-color-focused); &::after { @@ -243,10 +243,28 @@ 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. + */ +.select-option-start > *, +.select-option-end > * { + 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..d3bbc4c58b8 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..23cba718d75 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..59ccb166f40 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..dfbf11c43f8 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; @@ -209,8 +201,8 @@ } .alert-tappable { - @include margin(0); - @include padding(0); + @include mixins.margin(0); + @include mixins.padding(0); display: flex; @@ -244,7 +236,7 @@ } textarea.alert-input { - min-height: $alert-input-min-height; + min-height: 37px; resize: none; } @@ -262,6 +254,23 @@ textarea.alert-input { 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. + */ +.select-option-start > *, +.select-option-end > * { + 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..1ea7558dee1 100644 --- a/core/src/components/alert/alert.ionic.scss +++ b/core/src/components/alert/alert.ionic.scss @@ -1,10 +1,335 @@ @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 + ); + + flex: 1; + + color: globals.$ion-text-default; +} + +// Ionic Alert Radio Outer Circle: Unchecked +// --------------------------------------------------- + +.alert-radio-icon { + @include globals.position(globals.$ion-space-0, null, null, globals.$ion-space-700); + @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; +} + +// 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 + ); + + flex: 1; + + // Required for the checkbox icon to stay on the screen without + // being squished when the font size scales up. + width: calc(100% - globals.$ion-scale-1400); + + color: globals.$ion-text-default; +} + +// Ionic Alert Checkbox Outline: Unchecked +// -------------------------------------------------- + +.alert-checkbox-icon { + @include globals.position(globals.$ion-space-0, null, null, globals.$ion-space-700); + @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; +} + +.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-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 +338,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..a0457a76564 100644 --- a/core/src/components/alert/alert.ios.scss +++ b/core/src/components/alert/alert.ios.scss @@ -352,7 +352,7 @@ background-color: $alert-ios-button-background-color-activated; } -// iOS Action Sheet Button: Destructive +// iOS Alert Button: Destructive // --------------------------------------------------- .alert-button-role-destructive, @@ -360,3 +360,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..68f0b08323b 100644 --- a/core/src/components/alert/alert.md.scss +++ b/core/src/components/alert/alert.md.scss @@ -345,3 +345,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 db9ed59c9ce..ee6d2d89bd7 100644 --- a/core/src/components/alert/alert.tsx +++ b/core/src/components/alert/alert.tsx @@ -596,13 +596,20 @@ export class Alert implements ComponentInterface, OverlayInterface { 'alert-tappable': true, 'alert-checkbox': true, 'alert-checkbox-button': true, - 'ion-focusable': true, + 'ion-focusable': !i.disabled, + 'ion-activatable': !i.disabled, 'alert-checkbox-button-disabled': i.disabled || false, }} >
-
+ {theme === 'ionic' ? ( + + ) : ( +
+ )}
{renderOptionLabel(optionLabelOptions, 'alert-checkbox-label')}
@@ -652,7 +659,8 @@ export class Alert implements ComponentInterface, OverlayInterface { 'alert-radio-button': true, 'alert-tappable': true, 'alert-radio': true, - 'ion-focusable': true, + 'ion-focusable': !i.disabled, + 'ion-activatable': !i.disabled, 'alert-radio-button-disabled': i.disabled || false, }} role="radio" diff --git a/core/src/components/alert/alert.vars.scss b/core/src/components/alert/alert.vars.scss deleted file mode 100644 index 7502383bd3b..00000000000 --- a/core/src/components/alert/alert.vars.scss +++ /dev/null @@ -1,19 +0,0 @@ -@import "../../themes/native/native.globals"; - -// Alert -// -------------------------------------------------- - -/// @prop - Minimum width of the alert -$alert-min-width: 250px; - -/// @prop - Maximum height of the alert -$alert-max-height: 90%; - -/// @prop - Line height of the alert button -$alert-button-line-height: dynamic-font(20px); - -/// @prop - Font size of the alert button -$alert-button-font-size: dynamic-font(14px); - -/// @prop - Minimum height of a textarea in the alert -$alert-input-min-height: 37px; diff --git a/core/src/components/alert/test/basic/alert.e2e.ts b/core/src/components/alert/test/basic/alert.e2e.ts index 92b6c5572a4..f415490db5c 100644 --- a/core/src/components/alert/test/basic/alert.e2e.ts +++ b/core/src/components/alert/test/basic/alert.e2e.ts @@ -56,7 +56,7 @@ configs({ directions: ['ltr'] }).forEach(({ config, screenshot, title }) => { }); }); -configs().forEach(({ config, screenshot, title }) => { +configs({ modes: ['md', 'ios', 'ionic-md'] }).forEach(({ config, screenshot, title }) => { test.describe(title('should not have visual regressions'), () => { let alertFixture!: AlertFixture; diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..61b2d4e7be2 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..b6c7f32c98e Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..a13ebe87bde Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-rtl-light-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-rtl-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..71b316b4d1f Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-rtl-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-rtl-light-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-rtl-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..aa4655f802f Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-rtl-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-rtl-light-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-rtl-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..357228b20ea Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-ionic-md-rtl-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..6a12b3caba2 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..a3f4af5c8b3 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..3ef774873a1 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-rtl-light-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-rtl-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..969d949fedc Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-rtl-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-rtl-light-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-rtl-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..2b2ab74ac3e Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-rtl-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-rtl-light-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-rtl-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..018e608ff8b Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-ionic-md-rtl-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..d5549e112d9 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..2d53b39f608 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..f5f303b74d6 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-rtl-light-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-rtl-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..1085905a0cc Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-rtl-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-rtl-light-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-rtl-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..6d1e3f99f1e Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-rtl-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-rtl-light-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-rtl-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..8e51217d351 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-ionic-md-rtl-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..17203ef17d3 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..729513de92e Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..7cbcd8318b5 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-rtl-light-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-rtl-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..590fd979877 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-rtl-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-rtl-light-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-rtl-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..3f5671659ed Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-rtl-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-rtl-light-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-rtl-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..4fa83c5a0e9 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-ionic-md-rtl-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..66b6636bd89 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..0b6221a0fec Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..6b7f9bd4906 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-rtl-light-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-rtl-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..ce173b36579 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-rtl-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-rtl-light-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-rtl-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..7cd2ed8d19e Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-rtl-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-rtl-light-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-rtl-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..4ad81681811 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-ionic-md-rtl-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..ab34389351b Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..d3cf22a698d Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..b57210280e9 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-rtl-light-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-rtl-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..0f749428667 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-rtl-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-rtl-light-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-rtl-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..c02487ad5f8 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-rtl-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-rtl-light-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-rtl-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..dc3400906fb Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-ionic-md-rtl-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..fcff62675c9 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..00a68be5363 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..c4cc2fa0541 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-rtl-light-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-rtl-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..ef92eb585d2 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-rtl-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-rtl-light-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-rtl-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..5061204b95c Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-rtl-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-rtl-light-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-rtl-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..8f0131b2a45 Binary files /dev/null and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-ionic-md-rtl-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/alert/test/states/alert.e2e.ts b/core/src/components/alert/test/states/alert.e2e.ts new file mode 100644 index 00000000000..95f51290a51 --- /dev/null +++ b/core/src/components/alert/test/states/alert.e2e.ts @@ -0,0 +1,60 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +/** + * This behavior does not vary across directions. + */ +configs({ directions: ['ltr'], modes: ['ios', 'md', 'ionic-md'] }).forEach(({ config, screenshot, title }) => { + test.describe(title('alert: input states'), () => { + /** + * `(any-hover: hover)` evaluates to "none" in all three mobile-emulated + * projects, suppressing the hover rules: + * + * - Chromium and WebKit suppress it because of hasTouch and isMobile. + * - Headless Firefox doesn't detect input devices and reports no hover + * capability regardless of context options, so override it via + * launchOptions.firefoxUserPrefs. Bit values: 4 = FINE (mouse), + * 8 = HOVER, 12 = FINE | HOVER. + * + * Viewport, userAgent, and scale factor remain mobile-sized. + */ + test.use({ + hasTouch: false, + isMobile: false, + }); + + test.beforeEach(async ({ page }) => { + await page.goto(`/src/components/alert/test/states`, config); + }); + + test('should render all radio states', async ({ page }) => { + const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent'); + await page.locator('#radio').click(); + await ionAlertDidPresent.next(); + + const alert = page.locator('ion-alert'); + await expect(alert).toBeVisible(); + + const defaultRadio = alert.locator('button.alert-radio-button').first(); + await defaultRadio.hover(); + + await expect(alert).toHaveScreenshot(screenshot('alert-radio-states')); + }); + + test('should render all checkbox states', async ({ page }) => { + const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent'); + await page.locator('#checkbox').click(); + await ionAlertDidPresent.next(); + + const alert = page.locator('ion-alert'); + await expect(alert).toBeVisible(); + + const defaultCheckbox = alert.locator('button.alert-checkbox-button').first(); + await defaultCheckbox.hover(); + + await page.waitForChanges(); + + await expect(alert).toHaveScreenshot(screenshot('alert-checkbox-states')); + }); + }); +}); diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..4dac73cddb0 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..f2d0560630d 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..9f0c497aeb7 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..a5cbd2c0d6c 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..d421d107b1f 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..fb57f17ca0e 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..d02d4300f38 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..a57d4ba9b1b 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..f8b5fed8ed7 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..f66d20e96a2 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 b56f1c0c9fc..17d0aa4051b 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..ec528caf337 100644 --- a/core/src/components/select-modal/select-modal.common.scss +++ b/core/src/components/select-modal/select-modal.common.scss @@ -14,6 +14,36 @@ 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. + */ +.select-option-start > *, +.select-option-end > * { + 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..6fe502d5eb3 100644 --- a/core/src/components/select-modal/select-modal.ios.scss +++ b/core/src/components/select-modal/select-modal.ios.scss @@ -22,3 +22,16 @@ 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); +} 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..80a4717e1d1 100644 --- a/core/src/components/select-modal/select-modal.tsx +++ b/core/src/components/select-modal/select-modal.tsx @@ -49,7 +49,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) { @@ -127,6 +131,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, @@ -138,6 +143,8 @@ export class SelectModal implements ComponentInterface { return ( this.closeModal()} + onClick={() => this.closeModal(option.disabled)} onKeyDown={(ev) => { if (ev.key === 'Enter' && !ev.repeat) { this.pendingEnterTarget = ev.currentTarget as HTMLElement; @@ -158,12 +168,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); } } }} @@ -186,6 +196,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, @@ -196,6 +207,8 @@ export class SelectModal implements ComponentInterface { return ( - Select - Modal + Select Modal - Basic - + Cancel Text (default) - + 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..0a6929df656 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..655a81fcba3 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..23e07fab5b8 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..c004e0b506d 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..0f8d0ccb367 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..f3746ce72f7 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..61b415935e0 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..9483615da50 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-popover/select-popover.common.scss b/core/src/components/select-popover/select-popover.common.scss index 095b6660f35..8fa81cc75e3 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); } /** @@ -37,6 +37,36 @@ 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. + */ +.select-option-start > *, +.select-option-end > * { + 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..4b8b2d42be4 100644 --- a/core/src/components/select-popover/select-popover.tsx +++ b/core/src/components/select-popover/select-popover.tsx @@ -82,7 +82,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(); @@ -135,6 +139,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, @@ -145,6 +150,8 @@ export class SelectPopover implements ComponentInterface { return ( this.dismissParentPopover()} + onClick={() => this.dismissParentPopover(option.disabled)} onKeyDown={(ev) => { if (ev.key === 'Enter' && !ev.repeat) { this.pendingEnterTarget = ev.currentTarget as HTMLElement; @@ -212,12 +228,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..2414709a33a 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..7afa8b07144 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..f7375999964 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..067300429d4 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..4a61fa74b92 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..61d6dc6aa4a 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..f0e74b2af61 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..c2d202194df 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.tsx b/core/src/components/select/select.tsx index cde4a52850f..fb0c8368543 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 { 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,21 +635,17 @@ 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, }; }); @@ -670,14 +662,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,8 +676,8 @@ export class Select implements ComponentInterface { this.close(); } }, - startContent: startContent ?? undefined, - endContent: endContent ?? undefined, + startContent, + endContent, description: option.description, }; }); @@ -1641,8 +1629,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; }); } @@ -1667,11 +1662,15 @@ const getOptionContent = ( container.appendChild(clone); }); + // Sanitize the cloned DOM in place. Author-written 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 +1715,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/rich-content-option/index.html b/core/src/components/select/test/rich-content-option/index.html index 7bdf2881d3a..64679f36b35 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,40 @@ 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 + + + + + + + - + NEW Full Content - This is a span element @@ -146,18 +167,40 @@ 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 + + + + + + + - + NEW Full Content - This is a span element @@ -194,18 +237,40 @@ 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 + + + + + + + - + NEW Full Content - This is a span element @@ -242,6 +307,29 @@ 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 + + + + + + + @@ -259,7 +347,6 @@ Full Content - This is a span element @@ -296,18 +383,40 @@ 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 + + + + + + + - + NEW Full Content - This is a span element @@ -344,18 +453,41 @@ 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 + - + NEW Full Content - This is a span element @@ -392,6 +524,29 @@ 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 + + + + + + + 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..fe1acaf6022 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,26 +2,81 @@ 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 .alert-radio-label').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 .select-option-label').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-option-label').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'; @@ -32,11 +87,7 @@ 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(); @@ -52,18 +103,18 @@ 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-label').first(); const avatar = firstOption.locator('ion-avatar'); const spanText = await firstOption.locator('.span-style').textContent(); const firstOptionText = 'Full Content'; @@ -94,18 +145,18 @@ 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'; @@ -116,7 +167,11 @@ 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(); @@ -132,18 +187,18 @@ 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-option-label').first(); const avatar = firstOption.locator('ion-avatar'); const spanText = await firstOption.locator('.span-style').textContent(); const firstOptionText = 'Full Content'; @@ -154,7 +209,7 @@ 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(); @@ -170,18 +225,18 @@ 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-option-label').first(); const avatar = firstOption.locator('ion-avatar'); const spanText = await firstOption.locator('.span-style').textContent(); const firstOptionText = 'Full Content'; @@ -193,10 +248,10 @@ 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(); @@ -212,18 +267,18 @@ 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-option-label').first(); const avatar = firstOption.locator('ion-avatar'); const spanText = await firstOption.locator('.span-style').textContent(); const firstOptionText = 'Full Content'; @@ -234,7 +289,7 @@ 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(); @@ -250,18 +305,18 @@ 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-option-label').first(); const avatar = firstOption.locator('ion-avatar'); const spanText = await firstOption.locator('.span-style').textContent(); const firstOptionText = 'Full Content'; @@ -273,10 +328,10 @@ 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(); 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..253d4a8d6f9 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..f86a76304c5 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..fe6704eb912 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..b3895b188d4 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..66000419283 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..c70cd41f6f7 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..08b317dd876 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..3df02bed816 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..ff6d2bc6a59 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..416bec16a19 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..f0882ac5089 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..6748f93e37e 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..2d18ce6b456 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..cf1bd30d487 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..3fe97c99fe8 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..e14de7d5ef2 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..471c34d00f5 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..0d6f9bd0363 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..486b0da7a5a 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..d7a35085c07 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..e32a11f7399 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..cbb0686a52b 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..1ceee15887d 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..33d992b4b55 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..dd43d6c3e3e 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..2657180c1b9 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..c8dc9d7da7f 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..df5f44864a6 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..20e6b4fb867 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..abd347c4bd6 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/sanitization/index.ts b/core/src/utils/sanitization/index.ts index acab505d828..bb47caf5cf5 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 author-controlled DOM 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 author-controlled DOM tree in place. + * + * Removes blocked tags (`script`, `iframe`, etc.) from the subtree and + * then sanitizes attributes on every remaining element. Author-written + * attributes like `size`, `color`, and `shape` are preserved; event + * handlers (`on*`) and `javascript:` URLs are stripped. + * + * Use this when you have a DOM tree that the developer authored (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, allowSafeAuthorAttributes = 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 (!allowSafeAuthorAttributes && !allowedAttributes.includes(lowerName)) { + element.removeAttribute(attributeName); + continue; + } + + // strip event-handler attributes (already removed by the allowlist + // when !allowSafeAuthorAttributes; 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], allowSafeAuthorAttributes); } }; 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}