diff --git a/core/api.txt b/core/api.txt
index 067b07e818c..8333cd73e79 100644
--- a/core/api.txt
+++ b/core/api.txt
@@ -1741,6 +1741,20 @@ ion-select,css-prop,--placeholder-opacity,ios
ion-select,css-prop,--placeholder-opacity,md
ion-select,css-prop,--ripple-color,ios
ion-select,css-prop,--ripple-color,md
+ion-select,css-prop,--select-text-gap,ios
+ion-select,css-prop,--select-text-gap,md
+ion-select,css-prop,--select-text-media-border-color,ios
+ion-select,css-prop,--select-text-media-border-color,md
+ion-select,css-prop,--select-text-media-border-radius,ios
+ion-select,css-prop,--select-text-media-border-radius,md
+ion-select,css-prop,--select-text-media-border-style,ios
+ion-select,css-prop,--select-text-media-border-style,md
+ion-select,css-prop,--select-text-media-border-width,ios
+ion-select,css-prop,--select-text-media-border-width,md
+ion-select,css-prop,--select-text-media-height,ios
+ion-select,css-prop,--select-text-media-height,md
+ion-select,css-prop,--select-text-media-width,ios
+ion-select,css-prop,--select-text-media-width,md
ion-select,part,bottom
ion-select,part,container
ion-select,part,error-text
@@ -1760,7 +1774,11 @@ ion-select-modal,prop,multiple,boolean | undefined,undefined,false,false
ion-select-modal,prop,options,SelectModalOption[],[],false,false
ion-select-option,shadow
+ion-select-option,prop,description,string | undefined,undefined,false,false
ion-select-option,prop,disabled,boolean,false,false,false
+ion-select-option,prop,justify,"end" | "space-between" | "start" | undefined,undefined,false,false
+ion-select-option,prop,labelPlacement,"end" | "start" | undefined,undefined,false,false
+ion-select-option,prop,mode,"ios" | "md",undefined,false,false
ion-select-option,prop,value,any,undefined,false,false
ion-skeleton-text,shadow
diff --git a/core/src/components.d.ts b/core/src/components.d.ts
index 2d8a59f1903..f17aacb39e2 100644
--- a/core/src/components.d.ts
+++ b/core/src/components.d.ts
@@ -3143,10 +3143,26 @@ export namespace Components {
}
interface IonSelectOption {
/**
- * If `true`, the user cannot interact with the select option. This property does not apply when `interface="action-sheet"` as `ion-action-sheet` does not allow for disabled buttons.
+ * Text that is placed underneath the option text to provide additional details about the option.
+ */
+ "description"?: string;
+ /**
+ * If `true`, the user cannot interact with the select option.
* @default false
*/
"disabled": boolean;
+ /**
+ * How to pack the label and the option's selection control within a line. `"start"`: The label and radio will appear on the left in LTR and on the right in RTL. `"end"`: The label and radio will appear on the right in LTR and on the left in RTL. `"space-between"`: The label and radio will appear on opposite ends of the line with space between the two elements. Applies to the `alert`, `popover`, and `modal` interfaces, but has no visible effect on radio options in `popover` or `modal` on `md` (the radio control is hidden there). When unset, the interface picks a default based on mode and control type.
+ */
+ "justify"?: 'start' | 'end' | 'space-between';
+ /**
+ * Where the label is placed relative to the option's selection control (radio circle or checkbox box) when the option is rendered in an `alert`, `popover`, or `modal` interface. `"start"`: The label will appear to the left of the radio in LTR and to the right in RTL. `"end"`: The label will appear to the right of the radio in LTR and to the left in RTL. Applies to the `alert`, `popover`, and `modal` interfaces, but has no visible effect on radio options in `popover` or `modal` on `md` (the radio control is hidden there). When unset, the interface picks a default based on mode and control type.
+ */
+ "labelPlacement"?: 'start' | 'end';
+ /**
+ * The mode determines the platform behaviors of the component.
+ */
+ "mode"?: "ios" | "md";
/**
* The text value of the option.
*/
@@ -8345,10 +8361,26 @@ declare namespace LocalJSX {
}
interface IonSelectOption {
/**
- * If `true`, the user cannot interact with the select option. This property does not apply when `interface="action-sheet"` as `ion-action-sheet` does not allow for disabled buttons.
+ * Text that is placed underneath the option text to provide additional details about the option.
+ */
+ "description"?: string;
+ /**
+ * If `true`, the user cannot interact with the select option.
* @default false
*/
"disabled"?: boolean;
+ /**
+ * How to pack the label and the option's selection control within a line. `"start"`: The label and radio will appear on the left in LTR and on the right in RTL. `"end"`: The label and radio will appear on the right in LTR and on the left in RTL. `"space-between"`: The label and radio will appear on opposite ends of the line with space between the two elements. Applies to the `alert`, `popover`, and `modal` interfaces, but has no visible effect on radio options in `popover` or `modal` on `md` (the radio control is hidden there). When unset, the interface picks a default based on mode and control type.
+ */
+ "justify"?: 'start' | 'end' | 'space-between';
+ /**
+ * Where the label is placed relative to the option's selection control (radio circle or checkbox box) when the option is rendered in an `alert`, `popover`, or `modal` interface. `"start"`: The label will appear to the left of the radio in LTR and to the right in RTL. `"end"`: The label will appear to the right of the radio in LTR and to the left in RTL. Applies to the `alert`, `popover`, and `modal` interfaces, but has no visible effect on radio options in `popover` or `modal` on `md` (the radio control is hidden there). When unset, the interface picks a default based on mode and control type.
+ */
+ "labelPlacement"?: 'start' | 'end';
+ /**
+ * The mode determines the platform behaviors of the component.
+ */
+ "mode"?: "ios" | "md";
/**
* The text value of the option.
*/
@@ -9528,6 +9560,9 @@ declare namespace LocalJSX {
interface IonSelectOptionAttributes {
"disabled": boolean;
"value": string;
+ "description": string;
+ "labelPlacement": 'start' | 'end';
+ "justify": 'start' | 'end' | 'space-between';
}
interface IonSelectPopoverAttributes {
"header": string;
diff --git a/core/src/components/action-sheet/action-sheet.ios.scss b/core/src/components/action-sheet/action-sheet.ios.scss
index 672b0728726..26710a4a366 100644
--- a/core/src/components/action-sheet/action-sheet.ios.scss
+++ b/core/src/components/action-sheet/action-sheet.ios.scss
@@ -1,3 +1,4 @@
+@use "../select-option/select-option.ios.overlay";
@import "./action-sheet";
@import "./action-sheet.ios.vars";
diff --git a/core/src/components/action-sheet/action-sheet.md.scss b/core/src/components/action-sheet/action-sheet.md.scss
index e3480feefe4..38075e28639 100644
--- a/core/src/components/action-sheet/action-sheet.md.scss
+++ b/core/src/components/action-sheet/action-sheet.md.scss
@@ -1,7 +1,8 @@
+@use "../select-option/select-option.md.overlay";
@import "./action-sheet";
@import "./action-sheet.md.vars";
-// Material Design Action Sheet Title
+// Material Design Action Sheet
// -----------------------------------------
:host {
diff --git a/core/src/components/action-sheet/action-sheet.scss b/core/src/components/action-sheet/action-sheet.scss
index 2519c214f3f..ef1d5aec575 100644
--- a/core/src/components/action-sheet/action-sheet.scss
+++ b/core/src/components/action-sheet/action-sheet.scss
@@ -109,6 +109,10 @@
opacity: 0.4;
}
+.action-sheet-button:disabled ion-icon {
+ color: currentColor;
+}
+
.action-sheet-button-inner {
display: flex;
@@ -213,7 +217,7 @@
// Action Sheet: Focused
// --------------------------------------------------
-.action-sheet-button.ion-focused {
+.action-sheet-button.ion-focused:not(.ion-activated) {
color: var(--button-color-focused);
&::after {
@@ -221,6 +225,12 @@
opacity: var(--button-background-focused-opacity);
}
+
+ &.action-sheet-selected::after {
+ background: var(--button-background-focused, var(--button-background-selected));
+
+ opacity: var(--button-background-focused-opacity, var(--button-background-selected-opacity));
+ }
}
// Action Sheet: Hover
diff --git a/core/src/components/action-sheet/action-sheet.tsx b/core/src/components/action-sheet/action-sheet.tsx
index f5fae2c0ebb..c48e95d862d 100644
--- a/core/src/components/action-sheet/action-sheet.tsx
+++ b/core/src/components/action-sheet/action-sheet.tsx
@@ -17,11 +17,13 @@ import {
safeCall,
setOverlayId,
} from '@utils/overlays';
+import { renderOptionLabel } from '@utils/select-option-render';
import { getClassMap } from '@utils/theme';
import { getIonMode } from '../../global/ionic-global';
import type { AnimationBuilder, CssClassMap, FrameworkDelegate, OverlayInterface } from '../../interface';
import type { OverlayEventDetail } from '../../utils/overlays-interface';
+import type { SelectActionSheetButton } from '../select/select-interface';
import type { ActionSheetButton } from './action-sheet-interface';
import { iosEnterAnimation } from './animations/ios.enter';
@@ -562,6 +564,21 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
htmlAttrs['aria-checked'] = isActiveRadio ? 'true' : 'false';
}
+ /**
+ * Cast to `SelectActionSheetButton` to access rich content
+ * fields (`startContent`, `endContent`, `description`)
+ * that are passed through from `ion-select` but not
+ * part of the public `ActionSheetButton` interface.
+ */
+ const richButton = b as SelectActionSheetButton;
+ const optionLabelOptions = {
+ id: buttonId,
+ label: richButton.text,
+ startContent: richButton.startContent,
+ endContent: richButton.endContent,
+ description: richButton.description,
+ };
+
return (
{b.icon && }
- {b.text}
+ {renderOptionLabel(optionLabelOptions, 'action-sheet-button-label', true)}
{mode === 'md' && }
diff --git a/core/src/components/action-sheet/test/basic/index.html b/core/src/components/action-sheet/test/basic/index.html
index 640801b2ddf..73f78ca635d 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..d3d7eac4b8b
--- /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'] }).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-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..5a3e78894ef
Binary files /dev/null and b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..15b8f9a3b2f
Binary files /dev/null and b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ios-ltr-Mobile-Safari-linux.png b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..d03447bdf25
Binary files /dev/null and b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-md-ltr-Mobile-Chrome-linux.png b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..26357e0ef67
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..74d193b19c5
Binary files /dev/null and b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-md-ltr-Mobile-Safari-linux.png b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..1d3bc166290
Binary files /dev/null and b/core/src/components/action-sheet/test/states/action-sheet.e2e.ts-snapshots/action-sheet-states-diff-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/action-sheet/test/states/index.html b/core/src/components/action-sheet/test/states/index.html
new file mode 100644
index 00000000000..5d5339d5d1b
--- /dev/null
+++ b/core/src/components/action-sheet/test/states/index.html
@@ -0,0 +1,97 @@
+
+
+
+
+ Action Sheet - States
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Action Sheet - States
+
+
+
+
+ Basic
+
+
+
+
+
+
+
diff --git a/core/src/components/alert/alert.ios.scss b/core/src/components/alert/alert.ios.scss
index 259d00e0112..4241b89c591 100644
--- a/core/src/components/alert/alert.ios.scss
+++ b/core/src/components/alert/alert.ios.scss
@@ -1,3 +1,4 @@
+@use "../select-option/select-option.ios.overlay";
@import "./alert";
@import "./alert.ios.vars";
@@ -146,9 +147,6 @@
.alert-radio-label {
@include padding($alert-ios-radio-label-padding-top, $alert-ios-radio-label-padding-end, $alert-ios-radio-label-padding-bottom, $alert-ios-radio-label-padding-start);
- flex: 1;
- order: 0;
-
color: $alert-ios-radio-label-text-color;
}
@@ -167,7 +165,7 @@
.alert-radio-icon {
position: relative;
- order: 1;
+ flex-shrink: 0;
min-width: $alert-ios-radio-min-width;
}
@@ -176,8 +174,8 @@
// iOS Alert Radio Checked
// -----------------------------------------
-[aria-checked=true] .alert-radio-inner {
- @include position($alert-ios-radio-icon-top, null, null, $alert-ios-radio-icon-start);
+[aria-checked="true"] .alert-radio-inner {
+ @include position($alert-ios-radio-icon-top, null, null, null);
position: absolute;
@@ -193,6 +191,15 @@
border-color: $alert-ios-radio-icon-border-color;
}
+// The icon's inline offset gives it a gap from whichever edge the icon
+// sits on, which depends on label placement
+[aria-checked="true"] .radio-label-placement-end .alert-radio-inner {
+ @include position-horizontal(null, $alert-ios-radio-icon-start);
+}
+
+[aria-checked="true"] .radio-label-placement-start .alert-radio-inner {
+ @include position-horizontal($alert-ios-radio-icon-start, null);
+}
// iOS Alert Checkbox Label
// --------------------------------------------------
@@ -200,8 +207,6 @@
.alert-checkbox-label {
@include padding($alert-ios-checkbox-label-padding-top, $alert-ios-checkbox-label-padding-end, $alert-ios-checkbox-label-padding-bottom, $alert-ios-checkbox-label-padding-start);
- flex: 1;
-
color: $alert-ios-checkbox-label-text-color;
}
@@ -210,10 +215,12 @@
.alert-checkbox-icon {
@include border-radius($alert-ios-checkbox-border-radius);
- @include margin($alert-ios-checkbox-margin-top, $alert-ios-checkbox-margin-end, $alert-ios-checkbox-margin-bottom, $alert-ios-checkbox-margin-start);
+ @include margin($alert-ios-checkbox-margin-top, null, $alert-ios-checkbox-margin-bottom, null);
position: relative;
+ flex-shrink: 0;
+
width: $alert-ios-checkbox-size;
height: $alert-ios-checkbox-size;
@@ -226,6 +233,16 @@
contain: strict;
}
+// The icon's inline margins are asymmetric (larger gap from the row
+// edge, smaller gap toward the label), so they swap with label
+// placement.
+.checkbox-label-placement-end .alert-checkbox-icon {
+ @include margin-horizontal($alert-ios-checkbox-margin-start, $alert-ios-checkbox-margin-end);
+}
+
+.checkbox-label-placement-start .alert-checkbox-icon {
+ @include margin-horizontal($alert-ios-checkbox-margin-end, $alert-ios-checkbox-margin-start);
+}
// iOS Alert Checkbox Outer Circle: Checked
// -----------------------------------------
@@ -240,8 +257,8 @@
// iOS Alert Checkbox Inner Checkmark: Checked
// -----------------------------------------
-[aria-checked=true] .alert-checkbox-inner {
- @include position($alert-ios-checkbox-icon-top, null, null, $alert-ios-checkbox-icon-start);
+[aria-checked="true"] .alert-checkbox-inner {
+ @include position($alert-ios-checkbox-icon-top, null, null, null);
position: absolute;
@@ -257,6 +274,15 @@
border-color: $alert-ios-checkbox-icon-border-color;
}
+// The icon's inline offset gives it a gap from whichever edge the icon
+// sits on, which depends on label placement
+[aria-checked="true"] .checkbox-label-placement-end .alert-checkbox-inner {
+ @include position-horizontal($alert-ios-checkbox-icon-start, null);
+}
+
+[aria-checked="true"] .checkbox-label-placement-start .alert-checkbox-inner {
+ @include position-horizontal(null, $alert-ios-checkbox-icon-start);
+}
// iOS Alert Button
// --------------------------------------------------
@@ -329,7 +355,7 @@
background-color: $alert-ios-button-background-color-activated;
}
-// iOS Action Sheet Button: Destructive
+// iOS Alert Button: Destructive
// ---------------------------------------------------
.alert-button-role-destructive,
diff --git a/core/src/components/alert/alert.md.scss b/core/src/components/alert/alert.md.scss
index 3ab3833e6c0..951105c0818 100644
--- a/core/src/components/alert/alert.md.scss
+++ b/core/src/components/alert/alert.md.scss
@@ -1,3 +1,4 @@
+@use "../select-option/select-option.md.overlay";
@import "./alert";
@import "./alert.md.vars";
@@ -140,25 +141,47 @@
// --------------------------------------------------
.alert-radio-label {
- @include padding($alert-md-radio-label-padding-top, $alert-md-radio-label-padding-end, $alert-md-radio-label-padding-bottom, $alert-md-radio-label-padding-start);
+ @include padding($alert-md-radio-label-padding-top, null, $alert-md-radio-label-padding-bottom, null);
- flex: 1;
+ // Required for the radio icon to stay on the screen without
+ // being squished when the font size scales up.
+ max-width: calc(100% - $alert-md-radio-width);
color: $alert-md-radio-label-text-color;
font-size: $alert-md-radio-label-font-size;
}
+.radio-label-placement-end .alert-radio-label {
+ /**
+ * The label's inline padding clears space for the icon on the icon's side.
+ * When the label is placed at the end, the icon is at the start, so the
+ * larger padding clears it on the start side.
+ */
+ @include padding-horizontal($alert-md-radio-label-padding-start, $alert-md-radio-label-padding-end);
+}
+
+.radio-label-placement-start .alert-radio-label {
+ /**
+ * The label's inline padding clears space for the icon on the icon's side.
+ * When the label is placed at the start, the icon is at the end, so the
+ * larger padding clears it on the end side.
+ */
+ @include padding-horizontal($alert-md-radio-label-padding-end, $alert-md-radio-label-padding-start);
+}
+
// Material Design Alert Radio Unchecked Circle
// ---------------------------------------------------
.alert-radio-icon {
- @include position($alert-md-radio-top, null, null, $alert-md-radio-left);
+ @include position($alert-md-radio-top, null, null, null);
@include border-radius($alert-md-radio-border-radius);
display: block;
position: relative;
+ flex-shrink: 0;
+
width: $alert-md-radio-width;
height: $alert-md-radio-height;
@@ -167,6 +190,16 @@
border-color: $alert-md-radio-border-color-off;
}
+// The icon's inline offset gives it a gap from whichever edge the icon
+// sits on, which depends on label placement
+.radio-label-placement-end .alert-radio-icon {
+ @include position-horizontal($alert-md-radio-left, null);
+}
+
+.radio-label-placement-start .alert-radio-icon {
+ @include position-horizontal(null, $alert-md-radio-left);
+}
+
// Material Design Alert Radio Checked Dot
// ---------------------------------------------------
@@ -207,29 +240,46 @@
// --------------------------------------------------
.alert-checkbox-label {
- @include padding($alert-md-checkbox-label-padding-top, $alert-md-checkbox-label-padding-end, $alert-md-checkbox-label-padding-bottom, $alert-md-checkbox-label-padding-start);
-
- flex: 1;
+ @include padding($alert-md-checkbox-label-padding-top, null, $alert-md-checkbox-label-padding-bottom, null);
// Required for the checkbox icon to stay on the screen without
// being squished when the font size scales up.
- width: calc(100% - $alert-md-checkbox-label-padding-start);
+ max-width: calc(100% - $alert-md-checkbox-width);
color: $alert-md-checkbox-label-text-color;
font-size: $alert-md-checkbox-label-font-size;
}
+.checkbox-label-placement-end .alert-checkbox-label {
+ /**
+ * The label's inline padding clears space for the icon on the icon's side.
+ * When the label is placed at the end, the icon is at the start, so the
+ * larger padding clears it on the start side.
+ */
+ @include padding-horizontal($alert-md-checkbox-label-padding-start, $alert-md-checkbox-label-padding-end);
+}
+
+.checkbox-label-placement-start .alert-checkbox-label {
+ /**
+ * The label's inline padding clears space for the icon on the icon's side.
+ * When the label is placed at the start, the icon is at the end, so the
+ * larger padding clears it on the end side.
+ */
+ @include padding-horizontal($alert-md-checkbox-label-padding-end, $alert-md-checkbox-label-padding-start);
+}
// Material Design Alert Checkbox Outline: Unchecked
// --------------------------------------------------
.alert-checkbox-icon {
- @include position($alert-md-checkbox-top, null, null, $alert-md-checkbox-left);
+ @include position($alert-md-checkbox-top, null, null, null);
@include border-radius($alert-md-checkbox-border-radius);
position: relative;
+ flex-shrink: 0;
+
width: $alert-md-checkbox-width;
height: $alert-md-checkbox-height;
@@ -240,6 +290,16 @@
contain: strict;
}
+// The icon's inline offset gives it a gap from whichever edge the icon
+// sits on, which depends on label placement
+.checkbox-label-placement-end .alert-checkbox-icon {
+ @include position-horizontal($alert-md-checkbox-left, null);
+}
+
+.checkbox-label-placement-start .alert-checkbox-icon {
+ @include position-horizontal(null, $alert-md-checkbox-left);
+}
+
// Material Design Alert Checkbox Checkmark: Checked
// --------------------------------------------------
@@ -298,7 +358,7 @@
overflow: hidden;
}
-.alert-button-inner {
+.alert-button-group .alert-button-inner {
justify-content: $alert-md-button-group-justify-content;
}
diff --git a/core/src/components/alert/alert.md.vars.scss b/core/src/components/alert/alert.md.vars.scss
index 5f56d9161da..df47f3d0664 100644
--- a/core/src/components/alert/alert.md.vars.scss
+++ b/core/src/components/alert/alert.md.vars.scss
@@ -33,11 +33,14 @@ $alert-md-background-color: $overlay-md-background-color;
/// @prop - Box shadow color of the alert
$alert-md-box-shadow: 0 11px 15px -7px rgba(0, 0, 0, .2), 0 24px 38px 3px rgba(0, 0, 0, .14), 0 9px 46px 8px rgba(0, 0, 0, .12);
+/// @prop - Padding end of the alert
+$alert-md-padding-end: 24px;
+
/// @prop - Padding top of the alert head
$alert-md-head-padding-top: 20px;
/// @prop - Padding end of the alert head
-$alert-md-head-padding-end: 23px;
+$alert-md-head-padding-end: $alert-md-padding-end;
/// @prop - Padding bottom of the alert head
$alert-md-head-padding-bottom: 15px;
@@ -67,7 +70,7 @@ $alert-md-sub-title-text-color: $text-color;
$alert-md-message-padding-top: 20px;
/// @prop - Padding end of the alert message
-$alert-md-message-padding-end: 24px;
+$alert-md-message-padding-end: $alert-md-padding-end;
/// @prop - Padding bottom of the alert message
$alert-md-message-padding-bottom: $alert-md-message-padding-top;
@@ -186,11 +189,14 @@ $alert-md-list-border-top: 1px solid $alert-md-input-border-c
/// @prop - Border bottom of the alert list
$alert-md-list-border-bottom: $alert-md-list-border-top;
+/// @prop - Spacing between control and label
+$alert-md-control-label-spacing: 32px;
+
/// @prop - Top of the alert radio
$alert-md-radio-top: 0;
/// @prop - Left of the alert radio
-$alert-md-radio-left: 26px;
+$alert-md-radio-left: $alert-md-padding-end;
/// @prop - Width of the alert radio
$alert-md-radio-width: 20px;
@@ -241,13 +247,13 @@ $alert-md-radio-icon-transition: transform 280ms cubic-bezier(.4, 0
$alert-md-radio-label-padding-top: 13px;
/// @prop - Padding end on the label for the radio alert
-$alert-md-radio-label-padding-end: 26px;
+$alert-md-radio-label-padding-end: $alert-md-padding-end;
/// @prop - Padding bottom on the label for the radio alert
$alert-md-radio-label-padding-bottom: $alert-md-radio-label-padding-top;
/// @prop - Padding start on the label for the radio alert
-$alert-md-radio-label-padding-start: $alert-md-radio-label-padding-end + 26px;
+$alert-md-radio-label-padding-start: $alert-md-radio-left + $alert-md-control-label-spacing;
/// @prop - Font size of the label for the radio alert
$alert-md-radio-label-font-size: dynamic-font(16px);
@@ -262,7 +268,7 @@ $alert-md-radio-label-text-color-checked: $alert-md-radio-label-text-color;
$alert-md-checkbox-top: 0;
/// @prop - Left of the checkbox in the alert
-$alert-md-checkbox-left: 26px;
+$alert-md-checkbox-left: $alert-md-padding-end;
/// @prop - Width of the checkbox in the alert
$alert-md-checkbox-width: 16px;
@@ -313,13 +319,13 @@ $alert-md-checkbox-icon-transform: rotate(45deg);
$alert-md-checkbox-label-padding-top: 13px;
/// @prop - Padding end of the label for the checkbox in the alert
-$alert-md-checkbox-label-padding-end: $alert-md-checkbox-left;
+$alert-md-checkbox-label-padding-end: $alert-md-padding-end;
/// @prop - Padding bottom of the label for the checkbox in the alert
$alert-md-checkbox-label-padding-bottom: $alert-md-checkbox-label-padding-top;
/// @prop - Padding start of the label for the checkbox in the alert
-$alert-md-checkbox-label-padding-start: $alert-md-checkbox-label-padding-end + 27px;
+$alert-md-checkbox-label-padding-start: $alert-md-checkbox-left + $alert-md-control-label-spacing;
/// @prop - Text color of the label for the checkbox in the alert
$alert-md-checkbox-label-text-color: $text-color-step-150;
diff --git a/core/src/components/alert/alert.scss b/core/src/components/alert/alert.scss
index 12cdac9040b..9d93a4e74d9 100644
--- a/core/src/components/alert/alert.scss
+++ b/core/src/components/alert/alert.scss
@@ -199,6 +199,46 @@
}
+// Alert Option: Label Placement
+// --------------------------------------------------
+
+/**
+ * Label is on the right of the radio in LTR and
+ * on the left in RTL.
+ */
+.radio-label-placement-start,
+.checkbox-label-placement-start {
+ flex-direction: row-reverse;
+}
+
+/**
+ * Label is on the left of the radio in LTR and
+ * on the right in RTL.
+ */
+.radio-label-placement-end,
+.checkbox-label-placement-end {
+ flex-direction: row;
+}
+
+// Alert Option: Justify
+// --------------------------------------------------
+
+.radio-justify-start,
+.checkbox-justify-start {
+ justify-content: start;
+}
+
+.radio-justify-end,
+.checkbox-justify-end {
+ justify-content: end;
+}
+
+.radio-justify-space-between,
+.checkbox-justify-space-between {
+ justify-content: space-between;
+}
+
+
// Alert Button: Disabled
// --------------------------------------------------
.alert-input-disabled,
diff --git a/core/src/components/alert/alert.tsx b/core/src/components/alert/alert.tsx
index ac7f84d802f..3d5ed015873 100644
--- a/core/src/components/alert/alert.tsx
+++ b/core/src/components/alert/alert.tsx
@@ -6,6 +6,7 @@ import { createButtonActiveGesture } from '@utils/gesture/button-active';
import { raf } from '@utils/helpers';
import { createLockController } from '@utils/lock-controller';
import { printIonWarning } from '@utils/logging';
+import { getOverlayLabelJustify, getOverlayLabelPlacement } from '@utils/overlay-control-label';
import {
BACKDROP,
cleanupRootFocusTrapAccessibility,
@@ -20,6 +21,7 @@ import {
setOverlayId,
} from '@utils/overlays';
import { sanitizeDOMString } from '@utils/sanitization';
+import { renderOptionLabel } from '@utils/select-option-render';
import { getClassMap } from '@utils/theme';
import { config } from '../../global/config';
@@ -27,6 +29,7 @@ import { getIonMode } from '../../global/ionic-global';
import type { AnimationBuilder, CssClassMap, OverlayInterface, FrameworkDelegate } from '../../interface';
import type { OverlayEventDetail } from '../../utils/overlays-interface';
import type { IonicSafeString } from '../../utils/sanitization';
+import type { SelectAlertInput } from '../select/select-interface';
import type { AlertButton, AlertInput } from './alert-interface';
import { iosEnterAnimation } from './animations/ios.enter';
@@ -339,25 +342,20 @@ export class Alert implements ComponentInterface, OverlayInterface {
}
this.inputType = inputTypes.values().next().value;
- this.processedInputs = inputs.map(
- (i, index) =>
- ({
- type: i.type || 'text',
- name: i.name || `${index}`,
- placeholder: i.placeholder || '',
- value: i.value,
- label: i.label,
- checked: !!i.checked,
- disabled: !!i.disabled,
- id: i.id || `alert-input-${this.overlayIndex}-${index}`,
- handler: i.handler,
- min: i.min,
- max: i.max,
- cssClass: i.cssClass ?? '',
- attributes: i.attributes || {},
- tabindex: i.type === 'radio' && i !== focusable ? -1 : 0,
- } as AlertInput)
- );
+ this.processedInputs = inputs.map((i, index) => {
+ return {
+ ...i,
+ type: i.type || 'text',
+ name: i.name || `${index}`,
+ placeholder: i.placeholder || '',
+ checked: !!i.checked,
+ disabled: !!i.disabled,
+ id: i.id || `alert-input-${this.overlayIndex}-${index}`,
+ cssClass: i.cssClass ?? '',
+ attributes: i.attributes || {},
+ tabindex: i.type === 'radio' && i !== focusable ? -1 : 0,
+ } as AlertInput;
+ });
}
connectedCallback() {
@@ -595,39 +593,66 @@ export class Alert implements ComponentInterface, OverlayInterface {
return (
- {inputs.map((i) => (
-
this.cbClick(i)}
- aria-checked={`${i.checked}`}
- id={i.id}
- disabled={i.disabled}
- tabIndex={i.tabindex}
- role="checkbox"
- class={{
- ...getClassMap(i.cssClass),
- 'alert-tappable': true,
- 'alert-checkbox': true,
- 'alert-checkbox-button': true,
- 'ion-focusable': true,
- 'alert-checkbox-button-disabled': i.disabled || false,
- }}
- >
-
);
}
private renderRadio() {
const inputs = this.processedInputs;
+ const mode = getIonMode(this);
if (inputs.length === 0) {
return null;
@@ -635,32 +660,58 @@ export class Alert implements ComponentInterface, OverlayInterface {
return (
- {inputs.map((i) => (
-
this.rbClick(i)}
- aria-checked={`${i.checked}`}
- disabled={i.disabled}
- id={i.id}
- tabIndex={i.tabindex}
- class={{
- ...getClassMap(i.cssClass),
- 'alert-radio-button': true,
- 'alert-tappable': true,
- 'alert-radio': true,
- 'ion-focusable': true,
- 'alert-radio-button-disabled': i.disabled || false,
- }}
- role="radio"
- >
-
);
}
diff --git a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-checkbox-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-checkbox-scale-md-ltr-Mobile-Chrome-linux.png
index 82f5a675ff2..63cfb58a6d3 100644
Binary files a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-checkbox-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-checkbox-scale-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-checkbox-scale-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-checkbox-scale-md-ltr-Mobile-Firefox-linux.png
index 8bbc9a0fded..e2db4aa2d88 100644
Binary files a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-checkbox-scale-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-checkbox-scale-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-checkbox-scale-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-checkbox-scale-md-ltr-Mobile-Safari-linux.png
index ac7fcd60656..1d2434495de 100644
Binary files a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-checkbox-scale-md-ltr-Mobile-Safari-linux.png and b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-checkbox-scale-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-radio-scale-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-radio-scale-md-ltr-Mobile-Chrome-linux.png
index fbab7b3c127..1dff67d6512 100644
Binary files a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-radio-scale-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-radio-scale-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-radio-scale-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-radio-scale-md-ltr-Mobile-Firefox-linux.png
index 6aba2454439..529339e45fb 100644
Binary files a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-radio-scale-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-radio-scale-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-radio-scale-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-radio-scale-md-ltr-Mobile-Safari-linux.png
index 2b87b888ccc..ff95e10a47f 100644
Binary files a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-radio-scale-md-ltr-Mobile-Safari-linux.png and b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-radio-scale-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-scale-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-scale-md-ltr-Mobile-Firefox-linux.png
index d61d5e44519..3ba0f549b0a 100644
Binary files a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-scale-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-scale-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-scale-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-scale-md-ltr-Mobile-Safari-linux.png
index bdf6ff7664d..c0473ed80fa 100644
Binary files a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-scale-md-ltr-Mobile-Safari-linux.png and b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-scale-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-text-fields-scale-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-text-fields-scale-md-ltr-Mobile-Firefox-linux.png
index fb44a89f3bd..b76b5e4a91b 100644
Binary files a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-text-fields-scale-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-text-fields-scale-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-text-fields-scale-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-text-fields-scale-md-ltr-Mobile-Safari-linux.png
index 43b7f9973c2..dbf38dccaf1 100644
Binary files a/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-text-fields-scale-md-ltr-Mobile-Safari-linux.png and b/core/src/components/alert/test/a11y/alert.e2e.ts-snapshots/alert-text-fields-scale-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-checkboxes-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-checkboxes-md-ltr-Mobile-Chrome-linux.png
index 875ba984f0e..53c925c731d 100644
Binary files a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-checkboxes-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-checkboxes-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-checkboxes-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-checkboxes-md-ltr-Mobile-Firefox-linux.png
index f78319e0c58..6d9f5cd8314 100644
Binary files a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-checkboxes-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-checkboxes-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-checkboxes-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-checkboxes-md-ltr-Mobile-Safari-linux.png
index 604c74fe03e..7e3cfb5a49d 100644
Binary files a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-checkboxes-md-ltr-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-checkboxes-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-radios-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-radios-md-ltr-Mobile-Chrome-linux.png
index 629f5524caf..3deff89a5ef 100644
Binary files a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-radios-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-radios-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-radios-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-radios-md-ltr-Mobile-Firefox-linux.png
index 22a582bb2ec..3c657abecdd 100644
Binary files a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-radios-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-radios-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-radios-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-radios-md-ltr-Mobile-Safari-linux.png
index 0d52402aa7b..e7bc550a76f 100644
Binary files a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-radios-md-ltr-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-radios-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-text-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-text-md-ltr-Mobile-Chrome-linux.png
index 4fb47c8462f..bfe764faa7b 100644
Binary files a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-text-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-text-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-text-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-text-md-ltr-Mobile-Firefox-linux.png
index 99f2b2a62a1..d498bbb9a3b 100644
Binary files a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-text-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-text-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-text-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-text-md-ltr-Mobile-Safari-linux.png
index cfa85dde2ee..3d4cd744f3a 100644
Binary files a/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-text-md-ltr-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert-tablet.e2e.ts-snapshots/alert-tablet-text-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts b/core/src/components/alert/test/basic/alert.e2e.ts
index 6bc4e8105de..558c13a228b 100644
--- a/core/src/components/alert/test/basic/alert.e2e.ts
+++ b/core/src/components/alert/test/basic/alert.e2e.ts
@@ -177,6 +177,10 @@ class AlertFixture {
this.alert = this.page.locator('ion-alert');
await expect(this.alert).toBeVisible();
+ // Move mouse to the top-left corner of the page to avoid hover
+ // styles on buttons when taking screenshots
+ await this.page.mouse.move(0, 0);
+
return this.alert;
}
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-ltr-Mobile-Chrome-linux.png
index b544405dc17..54729d709e4 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-ltr-Mobile-Firefox-linux.png
index 8d14c6da0da..06b138615b2 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-ltr-Mobile-Safari-linux.png
index 50e3f5a756a..dd6a467d623 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-ltr-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-rtl-Mobile-Chrome-linux.png
index 4d40a498a94..d07e663283c 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-rtl-Mobile-Firefox-linux.png
index 7c20607d68d..3bee5e8962d 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-rtl-Mobile-Safari-linux.png
index fe59bd22194..55e7b342cb6 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-rtl-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-basic-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-ltr-Mobile-Chrome-linux.png
index 9eeec2cae89..847911f0ccb 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-ltr-Mobile-Firefox-linux.png
index c45d81932a2..b7e63249828 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-ltr-Mobile-Safari-linux.png
index 5b101a66ada..41cbd40df7f 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-ltr-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-rtl-Mobile-Chrome-linux.png
index 5cc66bb36cc..667f810f723 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-rtl-Mobile-Firefox-linux.png
index f6bc85257db..e53c4b536d6 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-rtl-Mobile-Safari-linux.png
index 6c96c6b2c25..98da9530843 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-rtl-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-checkbox-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-ltr-Mobile-Chrome-linux.png
index 819c171b6bb..8825f312940 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-ltr-Mobile-Firefox-linux.png
index e0d9b5eda10..e69e3194c61 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-ltr-Mobile-Safari-linux.png
index 11232bf65b7..f06381c5399 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-ltr-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-rtl-Mobile-Chrome-linux.png
index 372f5016adc..08b4addbb41 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-rtl-Mobile-Firefox-linux.png
index 3deaf96762c..f99fe4e2a92 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-rtl-Mobile-Safari-linux.png
index d590a6cff47..eb1259e24bc 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-rtl-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-confirm-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-ltr-Mobile-Chrome-linux.png
index 80d4cdcab06..d59b891d451 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-ltr-Mobile-Firefox-linux.png
index c9bec4c0f2b..d2fb6ef6fcf 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-ltr-Mobile-Safari-linux.png
index 57748f5ebf0..1df18290354 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-ltr-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-rtl-Mobile-Chrome-linux.png
index b0c282d061e..d7d5bbf6bc3 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-rtl-Mobile-Firefox-linux.png
index 39a780c0ef6..b13c70deeb9 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-rtl-Mobile-Safari-linux.png
index 7f3aaf6a477..c0c717f8820 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-rtl-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-longMessage-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-Mobile-Chrome-linux.png
index 4afb9ec90d7..57f6cbfed1b 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-Mobile-Firefox-linux.png
index 01e08d6fd05..31af72d38ed 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-Mobile-Safari-linux.png
index 99b52f26e9d..594983832ea 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-dark-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-dark-Mobile-Chrome-linux.png
index b50cef0cb16..7c7fad2aba5 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-dark-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-dark-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-dark-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-dark-Mobile-Firefox-linux.png
index 65486d0602d..ee3a4939487 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-dark-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-dark-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-dark-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-dark-Mobile-Safari-linux.png
index abc154963b2..4e7e6d9288e 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-dark-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-ltr-dark-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-Mobile-Chrome-linux.png
index aaf682977d2..ffa93bf0a91 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-Mobile-Firefox-linux.png
index e5cf2a79066..48929aafa61 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-Mobile-Safari-linux.png
index 7f7a9fb2f66..97e621d3884 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-dark-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-dark-Mobile-Chrome-linux.png
index be51a5bd790..b78b1518281 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-dark-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-dark-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-dark-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-dark-Mobile-Firefox-linux.png
index cfbd79cee3b..56f83e51c32 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-dark-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-dark-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-dark-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-dark-Mobile-Safari-linux.png
index 97bc8c5c013..b32534e6e2f 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-dark-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-multipleButtons-md-rtl-dark-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-ltr-Mobile-Chrome-linux.png
index a9007f0c66f..72767c3e3b7 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-ltr-Mobile-Firefox-linux.png
index 83770d39e49..9d1fc9d6842 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-ltr-Mobile-Safari-linux.png
index edfef39ba94..bb4f4e3e5fd 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-ltr-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-rtl-Mobile-Chrome-linux.png
index 07421e42313..ac336362129 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-rtl-Mobile-Firefox-linux.png
index 20fa217753d..d5dff762f4d 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-rtl-Mobile-Safari-linux.png
index 66a77d88b6c..381efebc6cf 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-rtl-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-noMessage-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-ltr-Mobile-Chrome-linux.png
index c34939bea16..e79d6e3c871 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-ltr-Mobile-Firefox-linux.png
index 2ea645f03e8..fab77e40ddc 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-ltr-Mobile-Safari-linux.png
index d445b3d2107..0d1d8c65a0c 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-ltr-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-rtl-Mobile-Chrome-linux.png
index 6076a3d6c1e..76e841e42fc 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-rtl-Mobile-Firefox-linux.png
index 1617a4f94eb..b58450b4c15 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-rtl-Mobile-Safari-linux.png
index 380b4ac4a38..4262cb68b32 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-rtl-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-prompt-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-ltr-Mobile-Chrome-linux.png
index fa707c18004..aa0de460234 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-ltr-Mobile-Firefox-linux.png
index 362c05843c3..433c9b190e0 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-ltr-Mobile-Safari-linux.png
index 22ba2d0d561..ee37c717f01 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-ltr-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-rtl-Mobile-Chrome-linux.png
index 9d4ddfb29e8..432a458b70a 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-rtl-Mobile-Firefox-linux.png
index 69761cb043f..23087f87fcb 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-rtl-Mobile-Safari-linux.png
index 00c661b0858..1e33449c32b 100644
Binary files a/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-rtl-Mobile-Safari-linux.png and b/core/src/components/alert/test/basic/alert.e2e.ts-snapshots/alert-radio-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts b/core/src/components/alert/test/label-placement/alert.e2e.ts
new file mode 100644
index 00000000000..7d80bcb4181
--- /dev/null
+++ b/core/src/components/alert/test/label-placement/alert.e2e.ts
@@ -0,0 +1,83 @@
+import { expect } from '@playwright/test';
+import type { Locator } from '@playwright/test';
+import type { E2EPage } from '@utils/test/playwright';
+import { configs, test } from '@utils/test/playwright';
+
+/**
+ * Force every theme's alert to the same canvas so the
+ * label-placement snapshots can be compared one-to-one.
+ * iOS does not respect the viewport so styles must be
+ * updated instead.
+ */
+const ALERT_SIZE_OVERRIDES = `
+ ion-alert {
+ --max-width: 560px !important;
+ --max-height: none !important;
+ }
+ ion-alert .alert-radio-group,
+ ion-alert .alert-checkbox-group {
+ max-height: none !important;
+ }
+`;
+
+configs({ modes: ['md', 'ios'] }).forEach(({ config, screenshot, title }) => {
+ test.describe(title('alert: label placement'), () => {
+ let alertFixture!: AlertFixture;
+
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/src/components/alert/test/label-placement', config);
+ await page.addStyleTag({ content: ALERT_SIZE_OVERRIDES });
+ alertFixture = new AlertFixture(page, screenshot);
+ });
+
+ test('radio - placement start', async () => {
+ await alertFixture.open('#radioStart');
+ await alertFixture.screenshot('radio-placement-start');
+ });
+
+ test('radio - placement end', async () => {
+ await alertFixture.open('#radioEnd');
+ await alertFixture.screenshot('radio-placement-end');
+ });
+
+ test('checkbox - placement start', async () => {
+ await alertFixture.open('#checkboxStart');
+ await alertFixture.screenshot('checkbox-placement-start');
+ });
+
+ test('checkbox - placement end', async () => {
+ await alertFixture.open('#checkboxEnd');
+ await alertFixture.screenshot('checkbox-placement-end');
+ });
+ });
+});
+
+class AlertFixture {
+ readonly page: E2EPage;
+ readonly screenshotFn: (file: string) => string;
+
+ private alert!: Locator;
+
+ constructor(page: E2EPage, screenshot: (file: string) => string) {
+ this.page = page;
+ this.screenshotFn = screenshot;
+ }
+
+ async open(selector: string) {
+ const ionAlertDidPresent = await this.page.spyOnEvent('ionAlertDidPresent');
+ await this.page.locator(selector).click();
+ await ionAlertDidPresent.next();
+ this.alert = this.page.locator('ion-alert');
+ await expect(this.alert).toBeVisible();
+
+ // Move mouse to away from the alert so hover styles don't interfere with screenshots
+ await this.page.mouse.move(0, 0);
+
+ return this.alert;
+ }
+
+ async screenshot(modifier: string) {
+ const alertWrapper = this.alert.locator('.alert-wrapper');
+ await expect(alertWrapper).toHaveScreenshot(this.screenshotFn(`alert-label-${modifier}`));
+ }
+}
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..16e8911851d
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..920c9674de8
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..ee629a04cb9
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..61a4aafec8e
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..74cc33373d8
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..330afd91237
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-ios-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..12b418fbc1b
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..bd0405549bc
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..8f7cf54cf93
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..f274268d505
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..6e23a5c4134
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..82be96f080e
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-end-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..e7b055c154e
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..00090539c8a
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..003fe874eec
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..558053878a0
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..9c9c3ee03f6
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..328a0a0c570
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-ios-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..c4361d6d920
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..60d513773f7
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..cb661e24a2e
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..f1f798a7a52
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..90fe40e6a91
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..19969217b15
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-checkbox-placement-start-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..47dbb642831
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..79f10d7f319
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..e973fafacd2
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..bd043774956
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..2e783739091
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..7149ec72591
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-ios-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..125562126b4
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..0196421ff44
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..3205f3f852b
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..4d5cdf0946a
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..0773337d024
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..d442fabd85c
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-end-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..580fb5950d5
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..08d86d09ab6
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..86625e010d2
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..c8e6777d48b
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..3aa7ae55dbd
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..57924f5eb36
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-ios-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..e4066100bb8
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..bb63f3638f3
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..610484362dc
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..b6931727559
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..bb69498cebb
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..1230387f2eb
Binary files /dev/null and b/core/src/components/alert/test/label-placement/alert.e2e.ts-snapshots/alert-label-radio-placement-start-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/label-placement/index.html b/core/src/components/alert/test/label-placement/index.html
new file mode 100644
index 00000000000..ffa252a212f
--- /dev/null
+++ b/core/src/components/alert/test/label-placement/index.html
@@ -0,0 +1,78 @@
+
+
+
+
+ Alert - Label Placement
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Alert - Label Placement
+
+
+
+
+ Radio — Placement Start
+ Radio — Placement End
+
+ Checkbox — Placement Start
+
+
+ Checkbox — Placement End
+
+
+
+
+
+
+
diff --git a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Chrome-linux.png
index 5f17e5fabe5..7b5e4f5ea4d 100644
Binary files a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Firefox-linux.png
index 84a81e8c938..9f9d68e7dba 100644
Binary files a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Safari-linux.png
index a81b5cbc533..89ee88aaf08 100644
Binary files a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Safari-linux.png and b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Chrome-linux.png b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Chrome-linux.png
index dd305e62607..ef35d402577 100644
Binary files a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Firefox-linux.png b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Firefox-linux.png
index ee5173b6a18..fc7270f5b30 100644
Binary files a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Safari-linux.png b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Safari-linux.png
index 37593c15e62..fd8cd04d32b 100644
Binary files a/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Safari-linux.png and b/core/src/components/alert/test/standalone/alert.e2e.ts-snapshots/alert-standalone-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/states/alert.e2e.ts b/core/src/components/alert/test/states/alert.e2e.ts
new file mode 100644
index 00000000000..3f6902fd84f
--- /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'] }).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-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..d1c9a799e3d
Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..eb8e000d874
Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ios-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..061da6fa38b
Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..0068646af36
Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..6c6164d688a
Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..3a4180d8cb6
Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-checkbox-states-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..1b509e947d5
Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..55756965353
Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..f3cadc60546
Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Chrome-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..276ea72ed09
Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Firefox-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..d3fce613898
Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Safari-linux.png b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..3ff1ea6eeb1
Binary files /dev/null and b/core/src/components/alert/test/states/alert.e2e.ts-snapshots/alert-radio-states-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/alert/test/states/index.html b/core/src/components/alert/test/states/index.html
new file mode 100644
index 00000000000..4591962127a
--- /dev/null
+++ b/core/src/components/alert/test/states/index.html
@@ -0,0 +1,159 @@
+
+
+
+
+ Alert - States
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Alert - States
+
+
+
+
+ Radio
+ Checkbox
+
+
+
+
+
+
diff --git a/core/src/components/popover/popover.scss b/core/src/components/popover/popover.scss
index ecd8acf2dc0..5ae35171b3e 100644
--- a/core/src/components/popover/popover.scss
+++ b/core/src/components/popover/popover.scss
@@ -128,3 +128,21 @@
--offset-x: 5px;
}
}
+
+// Select Popover
+// --------------------------------------------------
+
+:host(.select-popover-rich-content) {
+ /**
+ * Rich content options (start/end slots, descriptions) need
+ * more horizontal space than the default fixed width provides,
+ * otherwise content gets cut off.
+ *
+ * A viewport-relative value is used instead of `auto` or
+ * `max-content` because the enter animation reads `--width`
+ * via getBoundingClientRect() before the first layout pass
+ * completes. Intrinsic values return 0 at that point and
+ * cause the popover to be positioned off-screen on first open.
+ */
+ --width: clamp(250px, calc(100vw - 40px), 400px);
+}
diff --git a/core/src/components/select-modal/select-modal.ios.scss b/core/src/components/select-modal/select-modal.ios.scss
index a86db3e188b..db1044badb1 100644
--- a/core/src/components/select-modal/select-modal.ios.scss
+++ b/core/src/components/select-modal/select-modal.ios.scss
@@ -1,3 +1,4 @@
+@use "../select-option/select-option.ios.overlay";
@import "./select-modal";
@import "../item/item.ios.vars";
@import "../radio/radio.ios.vars";
diff --git a/core/src/components/select-modal/select-modal.md.scss b/core/src/components/select-modal/select-modal.md.scss
index 511c8d786ab..bf36299137b 100644
--- a/core/src/components/select-modal/select-modal.md.scss
+++ b/core/src/components/select-modal/select-modal.md.scss
@@ -1,3 +1,4 @@
+@use "../select-option/select-option.md.overlay";
@import "./select-modal";
@import "../../themes/ionic.mixins.scss";
@import "../item/item.md.vars";
diff --git a/core/src/components/select-modal/select-modal.tsx b/core/src/components/select-modal/select-modal.tsx
index 74358033caf..1e1b2ec0bbf 100644
--- a/core/src/components/select-modal/select-modal.tsx
+++ b/core/src/components/select-modal/select-modal.tsx
@@ -1,11 +1,14 @@
import { getIonMode } from '@global/ionic-global';
import type { ComponentInterface } from '@stencil/core';
import { Component, Element, Host, Prop, forceUpdate, h } from '@stencil/core';
+import { getOverlayLabelJustify, getOverlayLabelPlacement } from '@utils/overlay-control-label';
import { safeCall } from '@utils/overlays';
+import { renderOptionLabel } from '@utils/select-option-render';
import { getClassMap } from '@utils/theme';
import type { CheckboxCustomEvent } from '../checkbox/checkbox-interface';
import type { RadioGroupCustomEvent } from '../radio-group/radio-group-interface';
+import type { SelectOverlayOption } from '../select/select-interface';
import type { SelectModalOption } from './select-modal-interface';
@@ -85,77 +88,129 @@ export class SelectModal implements ComponentInterface {
}
private renderRadioOptions() {
+ const mode = getIonMode(this);
const checked = this.options.filter((o) => o.checked).map((o) => o.value)[0];
return (
this.callOptionHandler(ev)}>
- {this.options.map((option) => (
- {
+ /**
+ * Cast to `SelectOverlayOption` to access rich content
+ * fields (`startContent`, `endContent`, `description`)
+ * that are passed through from `ion-select` but not
+ * 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,
+ startContent: richOption.startContent,
+ endContent: richOption.endContent,
+ description: richOption.description,
+ };
+ const defaultLabelPlacement = getOverlayLabelPlacement(mode, 'radio', 'modal');
+ const defaultJustify = getOverlayLabelJustify(mode, 'radio', 'modal');
+
+ return (
+
- this.closeModal()}
- onKeyDown={(ev) => {
- if (ev.key === 'Enter' && !ev.repeat) {
- this.pendingEnterTarget = ev.currentTarget as HTMLElement;
- }
+ class={{
+ // TODO FW-4784
+ 'item-radio-checked': option.value === checked,
+ ...getClassMap(option.cssClass),
}}
- onKeyUp={(ev) => {
- if (ev.key === ' ') {
- // Space selects and dismisses in one press.
- this.closeModal();
- } else if (ev.key === 'Enter') {
- const shouldClose = this.pendingEnterTarget === ev.currentTarget;
- this.pendingEnterTarget = null;
- if (shouldClose) {
+ >
+ this.closeModal()}
+ onKeyDown={(ev) => {
+ if (ev.key === 'Enter' && !ev.repeat) {
+ this.pendingEnterTarget = ev.currentTarget as HTMLElement;
+ }
+ }}
+ onKeyUp={(ev) => {
+ if (ev.key === ' ') {
+ // Space selects and dismisses in one press.
this.closeModal();
+ } else if (ev.key === 'Enter') {
+ const shouldClose = this.pendingEnterTarget === ev.currentTarget;
+ this.pendingEnterTarget = null;
+ if (shouldClose) {
+ this.closeModal();
+ }
}
- }
- }}
- >
- {option.text}
-
-
- ))}
+ }}
+ >
+ {renderOptionLabel(optionLabelOptions, 'select-option-label')}
+
+
+ );
+ })}
);
}
private renderCheckboxOptions() {
- return this.options.map((option) => (
- {
+ /**
+ * Cast to `SelectOverlayOption` to access rich content
+ * fields (`startContent`, `endContent`, `description`)
+ * that are passed through from `ion-select` but not
+ * 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,
+ startContent: richOption.startContent,
+ endContent: richOption.endContent,
+ description: richOption.description,
+ };
+ const defaultLabelPlacement = getOverlayLabelPlacement(mode, 'checkbox', 'modal');
+ const defaultJustify = getOverlayLabelJustify(mode, 'checkbox', 'modal');
+
+ return (
+
- {
- this.setChecked(ev);
- this.callOptionHandler(ev);
+ class={{
// TODO FW-4784
- forceUpdate(this);
+ 'item-checkbox-checked': option.checked,
+ ...getClassMap(option.cssClass),
}}
>
- {option.text}
-
-
- ));
+ {
+ this.setChecked(ev);
+ this.callOptionHandler(ev);
+ // TODO FW-4784
+ forceUpdate(this);
+ }}
+ >
+ {renderOptionLabel(optionLabelOptions, 'select-option-label')}
+
+
+ );
+ });
}
render() {
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
+
+
+
+
+ Radio
+ Checkbox
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..1001513de8b
--- /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'] }).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-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..fd1716eb2db
Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..f07a61b8c87
Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..7bbc80f2147
Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..e071c2f6b99
Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..45eff1780f8
Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..0fec86f8133
Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-checkbox-states-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..5905048cdd4
Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..566ca560f64
Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..53fb60527bb
Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..f68735e8dd6
Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..68abf408834
Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..61a79657dd1
Binary files /dev/null and b/core/src/components/select-modal/test/states/select-modal.e2e.ts-snapshots/select-modal-radio-states-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/select-option.ios.overlay.scss b/core/src/components/select-option/select-option.ios.overlay.scss
new file mode 100644
index 00000000000..4cecfc471eb
--- /dev/null
+++ b/core/src/components/select-option/select-option.ios.overlay.scss
@@ -0,0 +1,47 @@
+@use "./select-option.overlay";
+@use "../../themes/ionic.mixins" as mixins;
+@use "../item/item.ios.vars" as item;
+
+// Select Option - Interface
+// --------------------------------------------------
+
+/**
+ * Cap slotted children so they can't stretch the option
+ * row out of proportion, keeping rows visually uniform
+ * regardless of the content.
+ */
+
+// Avatar / Image / SVG / Thumbnail
+.select-option-start > ion-avatar,
+.select-option-start > ion-img,
+.select-option-start > ion-thumbnail,
+.select-option-start > img,
+.select-option-start > svg,
+.select-option-end > ion-avatar,
+.select-option-end > ion-img,
+.select-option-end > ion-thumbnail,
+.select-option-end > img,
+.select-option-end > svg {
+ width: 44px;
+ height: 44px;
+}
+
+// Icon
+.select-option-start > ion-icon,
+.select-option-end > ion-icon {
+ width: 28px;
+ height: 28px;
+}
+
+// Select Option: Action Sheet
+// --------------------------------------------------
+.action-sheet-button-label-text {
+ justify-content: center;
+}
+
+// Select Option: Select Modal
+// --------------------------------------------------
+
+.select-option-has-rich-content {
+ @include mixins.padding-horizontal(null, item.$item-ios-padding-end);
+}
diff --git a/core/src/components/select-option/select-option.md.overlay.scss b/core/src/components/select-option/select-option.md.overlay.scss
new file mode 100644
index 00000000000..4720d9710ea
--- /dev/null
+++ b/core/src/components/select-option/select-option.md.overlay.scss
@@ -0,0 +1,44 @@
+@use "./select-option.overlay";
+
+// Select Option - Interface
+// --------------------------------------------------
+
+/**
+ * Cap slotted children so they can't stretch the option
+ * row out of proportion, keeping rows visually uniform
+ * regardless of the content.
+ */
+
+// Avatar
+.select-option-start > ion-avatar,
+.select-option-end > ion-avatar {
+ width: 40px;
+ height: 40px;
+}
+
+// Icon
+.select-option-start > ion-icon,
+.select-option-end > ion-icon {
+ width: 24px;
+ height: 24px;
+}
+
+// Image / SVG / Thumbnail
+.select-option-start > ion-img,
+.select-option-start > img,
+.select-option-start > svg,
+.select-option-start > ion-thumbnail,
+.select-option-end > ion-img,
+.select-option-end > img,
+.select-option-end > svg,
+.select-option-end > ion-thumbnail {
+ width: 56px;
+ height: 56px;
+}
+
+// Video
+.select-option-start > video,
+.select-option-end > video {
+ width: 114px;
+ height: 56px;
+}
diff --git a/core/src/components/select-option/select-option.overlay.scss b/core/src/components/select-option/select-option.overlay.scss
new file mode 100644
index 00000000000..ba8c19ba5e0
--- /dev/null
+++ b/core/src/components/select-option/select-option.overlay.scss
@@ -0,0 +1,99 @@
+@use "../../themes/ionic.theme.default" as native;
+@use "../../themes/ionic.mixins" as mixins;
+@use "../../themes/ionic.functions.font" as font;
+
+// Select Option - Overlay
+// --------------------------------------------------
+
+// Outer label container, which also spaces the start
+// and end slots when a select option has rich content
+.action-sheet-button-label-has-rich-content,
+.alert-radio-label-has-rich-content,
+.alert-checkbox-label-has-rich-content,
+.select-option-label-has-rich-content {
+ display: flex;
+
+ align-items: center;
+
+ gap: 16px;
+}
+
+/**
+ * Outer label container has rich content
+ * (start, content, description, end) that needs the
+ * label to span the available row width.
+ */
+.action-sheet-button-label-has-rich-content,
+.alert-radio-label-has-rich-content,
+.alert-checkbox-label-has-rich-content,
+.select-option-content {
+ flex: 1;
+}
+
+// Inner label container of a select option when
+// there is rich content within the default slot
+.action-sheet-button-label-text,
+.alert-checkbox-label-text,
+.alert-radio-label-text,
+.select-option-label-text {
+ display: flex;
+
+ align-items: center;
+
+ gap: 12px;
+}
+
+// Start and end slots
+.select-option-start,
+.select-option-end {
+ display: flex;
+
+ align-items: center;
+
+ gap: 8px;
+}
+
+.select-option-description {
+ @include mixins.padding(5px, 0, 0, 0);
+
+ display: block;
+
+ color: native.$text-color-step-300;
+
+ font-size: font.dynamic-font(12px);
+}
+
+// Select Option: Select Modal / Select Popover
+// --------------------------------------------------
+
+/**
+ * Non-rich labels are plain text and should ellipsize when they
+ * overflow the row. Rich-content labels switch to flex so the
+ * start / content / end pieces can lay out side-by-side and wrap.
+ */
+.select-option-label:not(.select-option-label-has-rich-content) {
+ text-overflow: ellipsis;
+
+ white-space: nowrap;
+
+ overflow: hidden;
+}
+
+.select-option-label-has-rich-content {
+ display: flex;
+
+ align-items: center;
+}
+
+ion-radio.select-option-has-rich-content::part(label),
+ion-checkbox.select-option-has-rich-content::part(label),
+.select-option-content {
+ flex: 1;
+
+ /**
+ * Let rich content wrap instead of inheriting the label part's
+ * single-line truncation, so arbitrary slotted elements render
+ * without clipping.
+ */
+ white-space: normal;
+}
diff --git a/core/src/components/select-option/select-option.scss b/core/src/components/select-option/select-option.scss
index cc36f78bfa0..b166e375c82 100644
--- a/core/src/components/select-option/select-option.scss
+++ b/core/src/components/select-option/select-option.scss
@@ -1,3 +1,6 @@
+// Select Option
+// --------------------------------------------------
+
:host {
display: none;
}
diff --git a/core/src/components/select-option/select-option.tsx b/core/src/components/select-option/select-option.tsx
index a3a69815d39..a26231897c6 100644
--- a/core/src/components/select-option/select-option.tsx
+++ b/core/src/components/select-option/select-option.tsx
@@ -3,6 +3,13 @@ import { Component, Element, Host, Prop, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
+/**
+ * @virtualProp {"ios" | "md"} mode - The mode determines the platform behaviors of the component.
+ *
+ * @slot - Content is placed between the named slots if provided without a slot.
+ * @slot start - Content is placed to the left of the select option text in LTR, and to the right in RTL.
+ * @slot end - Content is placed to the right of the select option text in LTR, and to the left in RTL.
+ */
@Component({
tag: 'ion-select-option',
shadow: true,
@@ -14,7 +21,7 @@ export class SelectOption implements ComponentInterface {
@Element() el!: HTMLElement;
/**
- * If `true`, the user cannot interact with the select option. This property does not apply when `interface="action-sheet"` as `ion-action-sheet` does not allow for disabled buttons.
+ * If `true`, the user cannot interact with the select option.
*/
@Prop() disabled = false;
@@ -23,6 +30,43 @@ export class SelectOption implements ComponentInterface {
*/
@Prop() value?: any | null;
+ /**
+ * Text that is placed underneath the option text to provide additional details about the option.
+ */
+ @Prop() description?: string;
+
+ /**
+ * Where the label is placed relative to the option's selection control
+ * (radio circle or checkbox box) when the option is rendered in an
+ * `alert`, `popover`, or `modal` interface.
+ * `"start"`: The label will appear to the left of the radio in LTR and to the right in RTL.
+ * `"end"`: The label will appear to the right of the radio in LTR and to the left in RTL.
+ *
+ * Applies to the `alert`, `popover`, and `modal` interfaces, but has no
+ * visible effect on radio options in `popover` or `modal` on `md` (the radio control is hidden there).
+ *
+ * When unset, the interface picks a default based on mode and control
+ * type.
+ */
+ @Prop() labelPlacement?: 'start' | 'end';
+
+ /**
+ * How to pack the label and the option's selection control within a line.
+ * `"start"`: The label and radio will appear on the left in LTR and
+ * on the right in RTL.
+ * `"end"`: The label and radio will appear on the right in LTR and
+ * on the left in RTL.
+ * `"space-between"`: The label and radio will appear on opposite
+ * ends of the line with space between the two elements.
+ *
+ * Applies to the `alert`, `popover`, and `modal` interfaces, but has no
+ * visible effect on radio options in `popover` or `modal` on `md` (the radio control is hidden there).
+ *
+ * When unset, the interface picks a default based on mode and control
+ * type.
+ */
+ @Prop() justify?: 'start' | 'end' | 'space-between';
+
render() {
return ;
}
diff --git a/core/src/components/select-option/test/label-placement/index.html b/core/src/components/select-option/test/label-placement/index.html
new file mode 100644
index 00000000000..fdd0d235b5b
--- /dev/null
+++ b/core/src/components/select-option/test/label-placement/index.html
@@ -0,0 +1,68 @@
+
+
+
+
+ Select Option - Label Placement
+
+
+
+
+
+
+
+
+
+
+
+
+ Select Option - Label Placement
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts b/core/src/components/select-option/test/label-placement/select-option.e2e.ts
new file mode 100644
index 00000000000..38f0d321cfd
--- /dev/null
+++ b/core/src/components/select-option/test/label-placement/select-option.e2e.ts
@@ -0,0 +1,79 @@
+import { expect } from '@playwright/test';
+import type { Page } from '@playwright/test';
+import { configs, test } from '@utils/test/playwright';
+
+/**
+ * iOS does not respect the viewport so styles must be updated instead.
+ */
+const ALERT_SIZE_OVERRIDES = `
+ ion-alert {
+ --max-width: 560px !important;
+ --max-height: none !important;
+ }
+ ion-alert .alert-radio-group,
+ ion-alert .alert-checkbox-group {
+ max-height: none !important;
+ }
+`;
+
+const INTERFACES = [
+ { name: 'alert', presentEvent: 'ionAlertDidPresent', locator: 'ion-alert .alert-wrapper' },
+ { name: 'popover', presentEvent: 'ionPopoverDidPresent', locator: 'ion-popover' },
+ { name: 'modal', presentEvent: 'ionModalDidPresent', locator: 'ion-modal' },
+] as const;
+
+const JUSTIFY_VARIANTS = ['start', 'end', 'space-between'] as const;
+
+const LABEL_PLACEMENTS = ['start', 'end'] as const;
+
+const FIRST_OPTION_VALUE = `${JUSTIFY_VARIANTS[0]}-short`;
+
+const renderOptions = (labelPlacement: 'start' | 'end') =>
+ JUSTIFY_VARIANTS.flatMap((justify) => {
+ const longLabel = `Justify ${justify} — ${'long label '.repeat(6).trim()}`;
+ return [
+ `Justify ${justify} `,
+ `${longLabel} `,
+ ];
+ }).join('');
+
+const setContentForInterface = async (
+ page: Page,
+ interfaceName: 'alert' | 'popover' | 'modal',
+ labelPlacement: 'start' | 'end',
+ config: object
+) => {
+ await page.setContent(
+ `
+
+ ${renderOptions(labelPlacement)}
+
+ `,
+ config
+ );
+};
+
+configs({ modes: ['md', 'ios'] }).forEach(({ config, screenshot, title }) => {
+ test.describe(title('select-option: label placement'), () => {
+ for (const { name, presentEvent, locator } of INTERFACES) {
+ test.describe(`${name} interface`, () => {
+ for (const placement of LABEL_PLACEMENTS) {
+ test(`placement ${placement}`, async ({ page }) => {
+ await setContentForInterface(page, name, placement, config);
+
+ if (name === 'alert') {
+ await page.addStyleTag({ content: ALERT_SIZE_OVERRIDES });
+ }
+
+ const didPresent = await page.spyOnEvent(presentEvent);
+ await page.locator('#select').click();
+ await didPresent.next();
+
+ const overlay = page.locator(locator);
+ await expect(overlay).toHaveScreenshot(screenshot(`select-option-label-${name}-${placement}`));
+ });
+ }
+ });
+ }
+ });
+});
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..6070403f4a4
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..2a48eadd256
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..cc0db575e60
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..8a8e68c7521
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..3cf41fcf865
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..3488917c50a
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-ios-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..46cfe39151f
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..e14857880ae
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..46f068d464f
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..27763acc007
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..0327ee6b7e7
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..84778d73bb8
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-end-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..b0db37f939f
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..dd2424d3c81
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..0d0e1049dab
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..daab103e13c
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..a1fecfdc9da
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..9216411f063
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-ios-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..6bcf0597a7b
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..06c7beebf6a
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..76c3afe9320
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..28e4b16fc74
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..736785f7f5d
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..40f1301db27
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-alert-start-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..7708f50533b
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..9b926b1a280
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..baa2f3680d2
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..1b9f62d7512
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..663016af26e
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..e4488119066
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-ios-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..daf825797f6
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..548ac3c0ecd
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..d5968bcfac1
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..804d8546ea6
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..ddc45c11b58
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..c977a6dbb3b
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-end-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..3a7a17300b5
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..e12990e4bd2
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..db4030312a0
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..ee00fc30921
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..be4d0056614
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..4a84e90bcdd
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-ios-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..633549f8754
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..f1581d3f4d4
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..722b846dc1e
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..caad951ecad
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..50a101c374d
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..ac996d14943
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-modal-start-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..f396184252c
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..8630aae6dc3
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..cf8c1533aae
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..05f9bf52df3
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..1e127cf49ec
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..aa70f017281
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-ios-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..7565efe1a44
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..5f935037abc
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..9c035e71043
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..4f099c35323
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..d07669d9757
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..923a4ca22f8
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-end-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..f1871ada104
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..9f5229da38b
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..779044231ee
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..609d1f9499b
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..3d8d71847f9
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..6bf85e0399e
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-ios-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..68ffc6acd73
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..6e1c8e45a58
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..855f16724b1
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Chrome-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..3c48349d082
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Firefox-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..e99348cc06b
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Safari-linux.png b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..cda4837fcc7
Binary files /dev/null and b/core/src/components/select-option/test/label-placement/select-option.e2e.ts-snapshots/select-option-label-popover-start-md-rtl-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-popover/select-popover.ios.scss b/core/src/components/select-popover/select-popover.ios.scss
index 22dc63cc3a2..f5878be6b49 100644
--- a/core/src/components/select-popover/select-popover.ios.scss
+++ b/core/src/components/select-popover/select-popover.ios.scss
@@ -1,3 +1,6 @@
+@use "../select-option/select-option.ios.overlay";
@import "./select-popover";
@import "./select-popover.ios.vars";
+// Select Popover: Select Option
+// --------------------------------------------------
diff --git a/core/src/components/select-popover/select-popover.md.scss b/core/src/components/select-popover/select-popover.md.scss
index 001b0123632..0f9c39328af 100644
--- a/core/src/components/select-popover/select-popover.md.scss
+++ b/core/src/components/select-popover/select-popover.md.scss
@@ -1,6 +1,10 @@
+@use "../select-option/select-option.md.overlay";
@import "./select-popover";
@import "./select-popover.md.vars";
+// Select Popover: Select Option
+// --------------------------------------------------
+
ion-list ion-radio::part(container) {
display: none;
}
diff --git a/core/src/components/select-popover/select-popover.tsx b/core/src/components/select-popover/select-popover.tsx
index 9e9b3fb13b5..3734b71df61 100644
--- a/core/src/components/select-popover/select-popover.tsx
+++ b/core/src/components/select-popover/select-popover.tsx
@@ -1,11 +1,14 @@
import type { ComponentInterface } from '@stencil/core';
import { Element, Component, Host, Prop, h, forceUpdate } from '@stencil/core';
+import { getOverlayLabelJustify, getOverlayLabelPlacement } from '@utils/overlay-control-label';
import { safeCall } from '@utils/overlays';
+import { renderOptionLabel } from '@utils/select-option-render';
import { getClassMap } from '@utils/theme';
import { getIonMode } from '../../global/ionic-global';
import type { CheckboxCustomEvent } from '../checkbox/checkbox-interface';
import type { RadioGroupCustomEvent } from '../radio-group/radio-group-interface';
+import type { SelectOverlayOption } from '../select/select-interface';
import type { SelectPopoverOption } from './select-popover-interface';
@@ -121,72 +124,124 @@ export class SelectPopover implements ComponentInterface {
}
renderCheckboxOptions(options: SelectPopoverOption[]) {
- return options.map((option) => (
- {
+ /**
+ * Cast to `SelectOverlayOption` to access rich content
+ * fields (`startContent`, `endContent`, `description`)
+ * that are passed through from `ion-select` but not
+ * 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,
+ startContent: richOption.startContent,
+ endContent: richOption.endContent,
+ description: richOption.description,
+ };
+ const defaultLabelPlacement = getOverlayLabelPlacement(mode, 'checkbox');
+ const defaultJustify = getOverlayLabelJustify(mode, 'checkbox');
+
+ return (
+
- {
- this.setChecked(ev);
- this.callOptionHandler(ev);
+ class={{
// TODO FW-4784
- forceUpdate(this);
+ 'item-checkbox-checked': option.checked,
+ ...getClassMap(option.cssClass),
}}
>
- {option.text}
-
-
- ));
+ {
+ this.setChecked(ev);
+ this.callOptionHandler(ev);
+ // TODO FW-4784
+ forceUpdate(this);
+ }}
+ >
+ {renderOptionLabel(optionLabelOptions, 'select-option-label')}
+
+
+ );
+ });
}
renderRadioOptions(options: SelectPopoverOption[]) {
+ const mode = getIonMode(this);
const checked = options.filter((o) => o.checked).map((o) => o.value)[0];
return (
this.callOptionHandler(ev)}>
- {options.map((option) => (
- {
+ /**
+ * Cast to `SelectOverlayOption` to access rich content
+ * fields (`startContent`, `endContent`, `description`)
+ * that are passed through from `ion-select` but not
+ * 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,
+ startContent: richOption.startContent,
+ endContent: richOption.endContent,
+ description: richOption.description,
+ };
+
+ return (
+
- this.dismissParentPopover()}
- onKeyDown={(ev) => {
- if (ev.key === 'Enter' && !ev.repeat) {
- this.pendingEnterTarget = ev.currentTarget as HTMLElement;
- }
+ class={{
+ // TODO FW-4784
+ 'item-radio-checked': option.value === checked,
+ ...getClassMap(option.cssClass),
}}
- onKeyUp={(ev) => {
- if (ev.key === ' ') {
- // Space selects and dismisses in one press.
- this.dismissParentPopover();
- } else if (ev.key === 'Enter') {
- const shouldDismiss = this.pendingEnterTarget === ev.currentTarget;
- this.pendingEnterTarget = null;
- if (shouldDismiss) {
+ >
+ this.dismissParentPopover()}
+ onKeyDown={(ev) => {
+ if (ev.key === 'Enter' && !ev.repeat) {
+ this.pendingEnterTarget = ev.currentTarget as HTMLElement;
+ }
+ }}
+ onKeyUp={(ev) => {
+ if (ev.key === ' ') {
+ // Space selects and dismisses in one press.
this.dismissParentPopover();
+ } else if (ev.key === 'Enter') {
+ const shouldDismiss = this.pendingEnterTarget === ev.currentTarget;
+ this.pendingEnterTarget = null;
+ if (shouldDismiss) {
+ this.dismissParentPopover();
+ }
}
- }
- }}
- >
- {option.text}
-
-
- ))}
+ }}
+ >
+ {renderOptionLabel(optionLabelOptions, 'select-option-label')}
+
+
+ );
+ })}
);
}
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
+
+
+
+
+ Radio
+ Checkbox
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..fd796dc4ba3
--- /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'] }).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-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..e2aaa6296c6
Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..cbbe126b948
Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..76753d37ccc
Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..bb05fb3d9e8
Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..6e3f5d6c393
Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..0c3f5a3f507
Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-checkbox-states-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..4814033cdfb
Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..fe87adfe765
Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..7896d663311
Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..a0448e47d3f
Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..486abb37a29
Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..2af5fa8df3f
Binary files /dev/null and b/core/src/components/select-popover/test/states/select-popover.e2e.ts-snapshots/select-popover-radio-states-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select/select-interface.ts b/core/src/components/select/select-interface.ts
index 8e65377a825..af1a397c9e1 100644
--- a/core/src/components/select/select-interface.ts
+++ b/core/src/components/select/select-interface.ts
@@ -1,3 +1,7 @@
+import type { ActionSheetButton } from '../action-sheet/action-sheet-interface';
+import type { AlertInput } from '../alert/alert-interface';
+import type { SelectPopoverOption } from '../select-popover/select-popover-interface';
+
export type SelectInterface = 'action-sheet' | 'popover' | 'alert' | 'modal';
export type SelectCompareFn = (currentValue: any, compareValue: any) => boolean;
@@ -10,3 +14,35 @@ export interface SelectCustomEvent extends CustomEvent {
detail: SelectChangeEventDetail;
target: HTMLIonSelectElement;
}
+
+export interface SelectActionSheetButton extends Omit, RichContentOption {
+ /** The main text for the option as a string or an HTMLElement. */
+ text?: string | HTMLElement;
+}
+
+export interface SelectAlertInput extends Omit, RichContentOption {
+ /** The main label for the option as a string or an HTMLElement. */
+ label?: string | HTMLElement;
+ /** Where the label sits relative to the option's selection control. */
+ labelPlacement?: 'start' | 'end';
+ /** How to pack the label and the option's selection control within a line. */
+ justify?: 'start' | 'end' | 'space-between';
+}
+
+export interface SelectOverlayOption extends Omit, RichContentOption {
+ /** The main text for the option as a string or an HTMLElement. */
+ text?: string | HTMLElement;
+ /** Where the label sits relative to the option's selection control. */
+ labelPlacement?: 'start' | 'end';
+ /** How to pack the label and the option's selection control within a line. */
+ justify?: 'start' | 'end' | 'space-between';
+}
+
+export interface RichContentOption {
+ /** Content to display at the start of the option. */
+ startContent?: HTMLElement;
+ /** Content to display at the end of the option. */
+ endContent?: HTMLElement;
+ /** A description for the option. */
+ description?: string;
+}
diff --git a/core/src/components/select/select.scss b/core/src/components/select/select.scss
index 157b2f5e358..98e9364271e 100644
--- a/core/src/components/select/select.scss
+++ b/core/src/components/select/select.scss
@@ -25,6 +25,14 @@
* @prop --border-width: Width of the select border
*
* @prop --ripple-color: The color of the ripple effect on MD mode.
+ *
+ * @prop --select-text-media-width: The width of media (icons/images) in the select text.
+ * @prop --select-text-media-height: The height of media (icons/images) in the select text.
+ * @prop --select-text-media-border-width: The border width of media (icons/images) in the select text.
+ * @prop --select-text-media-border-color: The border color of media (icons/images) in the select text.
+ * @prop --select-text-media-border-radius: The border radius of media (icons/images) in the select text.
+ * @prop --select-text-media-border-style: The border style of media (icons/images) in the select text.
+ * @prop --select-text-gap: The gap between elements in the select text.
*/
--padding-top: 0px;
--padding-end: 0px;
@@ -37,6 +45,9 @@
--highlight-color-focused: #{ion-color(primary, base)};
--highlight-color-valid: #{ion-color(success, base)};
--highlight-color-invalid: #{ion-color(danger, base)};
+ --select-text-media-height: 1.5em;
+ --select-text-media-width: 1.5em;
+ --select-text-gap: 12px;
/**
* This is a private API that is used to switch
@@ -189,6 +200,30 @@ button {
overflow: hidden;
}
+/**
+ * If the select text contains rich content, we want to add some
+ * spacing between the elements without changing the display to
+ * prevent losing the ellipses behavior.
+ */
+.select-text > * + * {
+ margin-inline-start: var(--select-text-gap);
+}
+
+.select-text img,
+.select-text ion-img,
+.select-text ion-icon,
+.select-text ion-thumbnail,
+.select-text ion-avatar {
+ @include border-radius(var(--select-text-media-border-radius));
+
+ width: var(--select-text-media-width);
+ height: var(--select-text-media-height);
+
+ border-width: var(--select-text-media-border-width);
+ border-style: var(--select-text-media-border-style);
+ border-color: var(--select-text-media-border-color);
+}
+
// Select Wrapper
// --------------------------------------------------
diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx
index 73fb6c59ac7..2dcf7c5cb8f 100644
--- a/core/src/components/select/select.tsx
+++ b/core/src/components/select/select.tsx
@@ -1,5 +1,6 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, h, forceUpdate } from '@stencil/core';
+import { ENABLE_HTML_CONTENT_DEFAULT } from '@utils/config';
import type { NotchController } from '@utils/forms';
import { compareOptions, createNotchController, isOptionSelected, checkInvalidState } from '@utils/forms';
import { focusVisibleElement, renderHiddenInput, inheritAttributes } from '@utils/helpers';
@@ -8,10 +9,12 @@ 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 { reflectPropertiesToAttributes, sanitizeDOMTree } from '@utils/sanitization';
import { createColorClasses, hostContext } from '@utils/theme';
import { watchForOptions } from '@utils/watch-options';
import { caretDownSharp, chevronExpand } from 'ionicons/icons';
+import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global';
import type {
ActionSheetOptions,
@@ -22,11 +25,15 @@ import type {
StyleEventDetail,
ModalOptions,
} from '../../interface';
-import type { ActionSheetButton } from '../action-sheet/action-sheet-interface';
-import type { AlertInput } from '../alert/alert-interface';
-import type { SelectPopoverOption } from '../select-popover/select-popover-interface';
-import type { SelectChangeEventDetail, SelectInterface, SelectCompareFn } from './select-interface';
+import type {
+ SelectChangeEventDetail,
+ SelectInterface,
+ SelectCompareFn,
+ SelectActionSheetButton,
+ SelectAlertInput,
+ SelectOverlayOption,
+} from './select-interface';
// TODO(FW-2832): types
@@ -68,8 +75,8 @@ export class Select implements ComponentInterface {
private nativeWrapperEl: HTMLElement | undefined;
private notchSpacerEl: HTMLElement | undefined;
private validationObserver?: MutationObserver;
-
private notchController?: NotchController;
+ private customHTMLEnabled = config.get('innerHTMLTemplatesEnabled', ENABLE_HTML_CONTENT_DEFAULT);
@Element() el!: HTMLIonSelectElement;
@@ -583,7 +590,7 @@ export class Select implements ComponentInterface {
}
}
- private createActionSheetButtons(data: HTMLIonSelectOptionElement[], selectValue: any): ActionSheetButton[] {
+ private createActionSheetButtons(data: HTMLIonSelectOptionElement[], selectValue: any): SelectActionSheetButton[] {
const actionSheetButtons = data.map((option) => {
const value = getOptionValue(option);
@@ -593,10 +600,12 @@ export class Select implements ComponentInterface {
.join(' ');
const optClass = `${OPTION_CLASS} ${copyClasses}`;
const isSelected = isOptionSelected(selectValue, value, this.compareWith);
+ const { content, startContent, endContent } = extractOptionContent(option, this.customHTMLEnabled);
return {
- text: option.textContent,
+ text: content ?? '',
cssClass: optClass,
+ disabled: option.disabled,
handler: () => {
this.setValue(value);
},
@@ -604,7 +613,10 @@ export class Select implements ComponentInterface {
'aria-checked': isSelected ? 'true' : 'false',
role: 'radio',
},
- } as ActionSheetButton;
+ startContent,
+ endContent,
+ description: option.description,
+ } as SelectActionSheetButton;
});
// Add "cancel" button
@@ -623,7 +635,7 @@ export class Select implements ComponentInterface {
data: HTMLIonSelectOptionElement[],
inputType: 'checkbox' | 'radio',
selectValue: any
- ): AlertInput[] {
+ ): SelectAlertInput[] {
const alertInputs = data.map((option) => {
const value = getOptionValue(option);
@@ -632,21 +644,27 @@ export class Select implements ComponentInterface {
.filter((cls) => cls !== 'hydrated')
.join(' ');
const optClass = `${OPTION_CLASS} ${copyClasses}`;
+ const { content, startContent, endContent } = extractOptionContent(option, this.customHTMLEnabled);
return {
type: inputType,
cssClass: optClass,
- label: option.textContent || '',
+ label: content ?? '',
value,
checked: isOptionSelected(selectValue, value, this.compareWith),
disabled: option.disabled,
+ startContent,
+ endContent,
+ description: option.description,
+ labelPlacement: option.labelPlacement,
+ justify: option.justify,
};
});
return alertInputs;
}
- private createOverlaySelectOptions(data: HTMLIonSelectOptionElement[], selectValue: any): SelectPopoverOption[] {
+ private createOverlaySelectOptions(data: HTMLIonSelectOptionElement[], selectValue: any): SelectOverlayOption[] {
const popoverOptions = data.map((option) => {
const value = getOptionValue(option);
@@ -655,9 +673,10 @@ export class Select implements ComponentInterface {
.filter((cls) => cls !== 'hydrated')
.join(' ');
const optClass = `${OPTION_CLASS} ${copyClasses}`;
+ const { content, startContent, endContent } = extractOptionContent(option, this.customHTMLEnabled);
return {
- text: option.textContent || '',
+ text: content ?? '',
cssClass: optClass,
value,
checked: isOptionSelected(selectValue, value, this.compareWith),
@@ -668,6 +687,11 @@ export class Select implements ComponentInterface {
this.close();
}
},
+ startContent,
+ endContent,
+ description: option.description,
+ labelPlacement: option.labelPlacement,
+ justify: option.justify,
};
});
@@ -708,6 +732,9 @@ export class Select implements ComponentInterface {
};
}
+ const options = this.createOverlaySelectOptions(this.childOpts, value);
+ const hasRichContent = options.some((opt) => opt.startContent || opt.endContent || opt.description);
+
const popoverOpts: PopoverOptions = {
mode,
event,
@@ -717,14 +744,18 @@ export class Select implements ComponentInterface {
...interfaceOptions,
component: 'ion-select-popover',
- cssClass: ['select-popover', interfaceOptions.cssClass],
+ cssClass: [
+ 'select-popover',
+ hasRichContent ? 'select-popover-rich-content' : undefined,
+ interfaceOptions.cssClass,
+ ],
componentProps: {
header: interfaceOptions.header,
subHeader: interfaceOptions.subHeader,
message: interfaceOptions.message,
multiple,
value,
- options: this.createOverlaySelectOptions(this.childOpts, value),
+ options,
},
};
@@ -895,12 +926,18 @@ export class Select implements ComponentInterface {
return;
}
- private getText(): string {
+ /**
+ * Returns the text to display in the select based on the selected value.
+ *
+ * @param useHTML If `true`, the returned text will include any custom HTML content from the selected option. If `false`, the returned text will be plain text without any HTML. Defaults to `false`.
+ * @returns The text to display in the select, either with or without HTML based on the `useHTML` parameter.
+ */
+ private getText(useHTML = false): string {
const selectedText = this.selectedText;
if (selectedText != null && selectedText !== '') {
return selectedText;
}
- return generateText(this.childOpts, this.value, this.compareWith);
+ return generateText(this.childOpts, this.value, this.compareWith, useHTML);
}
private setFocus() {
@@ -1061,6 +1098,56 @@ export class Select implements ComponentInterface {
return this.renderLabel();
}
+ /**
+ * Wraps text nodes in the select text with span elements
+ * so spacing can be added between elements without
+ * changing the display to prevent losing the ellipses
+ * behavior.
+ *
+ * Only wraps when the string contains HTML elements
+ * alongside text.
+ */
+ private wrapSelectTextNodes(html: string): string {
+ const temp = document.createElement('div');
+ temp.innerHTML = html;
+
+ const hasElements = Array.from(temp.childNodes).some((n) => n.nodeType === Node.ELEMENT_NODE);
+
+ // Return the plain text
+ if (!hasElements) {
+ return html;
+ }
+
+ Array.from(temp.childNodes).forEach((node) => {
+ if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) {
+ const text = node.textContent;
+
+ /**
+ * Split comma separator from the text content
+ * e.g., ", Bacon" becomes ", " text node + Bacon .
+ */
+ const commaMatch = text.match(/^(,\s*)(.*)/);
+ if (commaMatch) {
+ const commaNode = document.createTextNode(commaMatch[1]);
+ const wrapper = document.createElement('span');
+
+ wrapper.textContent = commaMatch[2];
+ node.parentNode?.replaceChild(wrapper, node);
+ wrapper.parentNode?.insertBefore(commaNode, wrapper);
+
+ return;
+ }
+
+ const wrapper = document.createElement('span');
+
+ node.parentNode?.replaceChild(wrapper, node);
+ wrapper.appendChild(node);
+ }
+ });
+
+ return temp.innerHTML;
+ }
+
/**
* Renders either the placeholder
* or the selected values based on
@@ -1069,7 +1156,7 @@ export class Select implements ComponentInterface {
private renderSelectText() {
const { placeholder } = this;
- const displayValue = this.getText();
+ const displayValue = this.getText(true);
let addPlaceholderClass = false;
let selectText = displayValue;
@@ -1085,6 +1172,11 @@ export class Select implements ComponentInterface {
const textPart = addPlaceholderClass ? 'placeholder' : 'text';
+ if (this.customHTMLEnabled) {
+ const wrapped = this.wrapSelectTextNodes(selectText);
+ return
;
+ }
+
return (
{selectText}
@@ -1113,6 +1205,7 @@ export class Select implements ComponentInterface {
private get ariaLabel() {
const { placeholder, inheritedAttributes } = this;
+ // Get the plain text from the selected text
const displayValue = this.getText();
// The aria label should be preferred over visible text if both are specified
@@ -1332,30 +1425,250 @@ const parseValue = (value: any) => {
const generateText = (
opts: HTMLIonSelectOptionElement[],
value: any | any[],
- compareWith?: string | SelectCompareFn | null
+ compareWith?: string | SelectCompareFn | null,
+ useHTML = false
) => {
if (value === undefined) {
return '';
}
if (Array.isArray(value)) {
return value
- .map((v) => textForValue(opts, v, compareWith))
+ .map((v) => textForValue(opts, v, compareWith, useHTML))
.filter((opt) => opt !== null)
.join(', ');
} else {
- return textForValue(opts, value, compareWith) || '';
+ return textForValue(opts, value, compareWith, useHTML) || '';
}
};
+/**
+ * Returns the display text for a given value from the list of options.
+ * When `useHTML` is true, returns sanitized HTML for the select text.
+ * When `useHTML` is false, returns plain text for aria-label and other
+ * text-only contexts.
+ *
+ * @param opts - The list of ion-select-option elements.
+ * @param value - The value to find the matching option for.
+ * @param compareWith - Custom comparison function or property name.
+ * @param useHTML - If true, returns HTML string. If false, returns plain text.
+ */
const textForValue = (
opts: HTMLIonSelectOptionElement[],
value: any,
- compareWith?: string | SelectCompareFn | null
+ compareWith?: string | SelectCompareFn | null,
+ useHTML = false
): string | null => {
const selectOpt = opts.find((opt) => {
return compareOptions(value, getOptionValue(opt), compareWith);
});
- return selectOpt ? selectOpt.textContent : null;
+ const customHTMLEnabled = config.get('innerHTMLTemplatesEnabled', ENABLE_HTML_CONTENT_DEFAULT);
+
+ if (!selectOpt) {
+ return null;
+ }
+
+ // Return sanitized HTML for the select text
+ if (customHTMLEnabled && useHTML) {
+ return getOptionContent(selectOpt, undefined, true) as string | null;
+ }
+
+ /**
+ * When custom HTML is enabled, extract only the default slot content.
+ * This ensures aria-label and other text-only contexts read only
+ * the relevant option text.
+ */
+ if (customHTMLEnabled) {
+ const content = getOptionContent(selectOpt);
+
+ if (typeof content === 'string') {
+ return content;
+ }
+
+ /**
+ * Elements were found in the default slot, extract and concatenate
+ * their text content while trimming whitespace.
+ */
+ if (content) {
+ const texts = Array.from(content.childNodes)
+ .map((n) => n.textContent?.trim())
+ .filter((t) => t);
+ return texts.join(' ') || null;
+ }
+
+ // Empty option
+ return null;
+ }
+
+ return getDefaultSlotPlainText(selectOpt);
+};
+
+/**
+ * Trims whitespace from all text nodes within a DOM tree.
+ * This prevents invisible layout shifts and unwanted gaps between
+ * elements when HTML content is injected via innerHTML or cloneNode,
+ * as browsers preserve whitespace (tabs, newlines, spaces) from
+ * the original source markup.
+ *
+ * @param node The root node to start trimming text nodes from.
+ */
+const trimTextNodes = (node: Node): void => {
+ node.childNodes.forEach((child) => {
+ if (child.nodeType === Node.TEXT_NODE) {
+ child.textContent = child.textContent?.trim() || '';
+ } else if (child.nodeType === Node.ELEMENT_NODE) {
+ trimTextNodes(child);
+ }
+ });
+};
+
+/**
+ * Extracts and clones content from an `ion-select-option` element
+ * for rendering within overlay interfaces or the select text when `customHTMLEnabled` is `true`.
+ *
+ * @param option - The `ion-select-option` element to extract content from.
+ * @param slotName - Optional slot name to extract. If omitted, extracts the default slot content.
+ * @param useHTML - If `true`, the returned string will include any custom HTML content. If `false`, the returned string will be plain text without any HTML.
+ * @returns When `useHTML` is `true`, a sanitized HTML string. When `false`, a
+ * div element containing cloned child nodes. Returns `null` if no matching
+ * content is found.
+ */
+const getOptionContent = (
+ option: HTMLIonSelectOptionElement,
+ slotName?: string,
+ useHTML: boolean = false
+): HTMLElement | string | null => {
+ let nodes: Node[];
+
+ if (slotName) {
+ // Named slot: get elements with matching slot attribute
+ nodes = Array.from(option.children).filter((el) => el.getAttribute('slot') === slotName);
+ } else {
+ // Default slot: get nodes without a slot attribute
+ const defaultSlot = getOptionDefaultSlot(option) || [];
+ nodes = defaultSlot.filter((node) => {
+ /**
+ * 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;
+ });
+ }
+
+ if (nodes.length === 0) {
+ return null;
+ }
+
+ // Return plain text if no elements are found
+ if (!slotName && nodes.every((n) => n.nodeType === Node.TEXT_NODE)) {
+ return nodes.map((n) => n.textContent?.trim()).join(' ') || null;
+ }
+
+ /**
+ * Mirror known custom-element properties (e.g. ion-icon's `icon`)
+ * onto attributes before cloning. Frameworks like Vue set these as
+ * DOM properties, which `cloneNode` doesn't copy, so without this
+ * step the cloned overlay copy renders without the prop's value.
+ */
+ nodes.forEach((n) => {
+ if (n.nodeType === Node.ELEMENT_NODE) {
+ reflectPropertiesToAttributes(n as Element);
+ }
+ });
+
+ // Clone each node into a temporary container
+ const container = document.createElement('div');
+ nodes.forEach((n) => {
+ const clone = n.cloneNode(true);
+ if (clone.nodeType === Node.TEXT_NODE) {
+ clone.textContent = clone.textContent?.trim() || '';
+ } else {
+ trimTextNodes(clone);
+ }
+ container.appendChild(clone);
+ });
+
+ /**
+ * Sanitize the cloned DOM in place. Trusted attributes (size, color,
+ * shape, etc.) are preserved; event handlers, javascript: URLs, and
+ * blocked tags are stripped.
+ */
+ sanitizeDOMTree(container);
+
+ if (useHTML) {
+ return container.innerHTML.trim() || null;
+ }
+
+ return container;
+};
+
+/**
+ * Returns the child nodes that belong to the default slot of an
+ * option element, excluding any nodes that are assigned to named
+ * slots.
+ *
+ * @param option - The `ion-select-option` element to extract default-slot nodes from.
+ * @returns An array of default slot nodes, or `null` if none are found.
+ */
+const getOptionDefaultSlot = (option: HTMLIonSelectOptionElement): Node[] | null => {
+ const defaultSlotNodes = Array.from(option.childNodes).filter((node) => {
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ return !(node as HTMLElement).hasAttribute('slot');
+ }
+ return node.nodeType === Node.TEXT_NODE;
+ });
+
+ if (defaultSlotNodes.length === 0) {
+ return null;
+ }
+
+ return defaultSlotNodes;
+};
+
+/**
+ * Extracts plain text from only the default slot of an option,
+ * excluding content assigned to named slots (start/end).
+ */
+const getDefaultSlotPlainText = (option: HTMLIonSelectOptionElement): string => {
+ const texts = Array.from(option.childNodes)
+ .filter((node) => {
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ return !(node as HTMLElement).hasAttribute('slot');
+ }
+ return node.nodeType === Node.TEXT_NODE;
+ })
+ .filter((node) => node.nodeType === Node.TEXT_NODE)
+ .map((n) => n.textContent?.trim())
+ .filter((t) => t);
+ 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;
diff --git a/core/src/components/select/test/basic/index.html b/core/src/components/select/test/basic/index.html
index 4e70f5a2603..ebcc1286782 100644
--- a/core/src/components/select/test/basic/index.html
+++ b/core/src/components/select/test/basic/index.html
@@ -29,7 +29,7 @@
-
+
Apples
Oranges
Pears
@@ -37,7 +37,7 @@
-
+
Apples
Oranges
Pears
@@ -45,7 +45,7 @@
-
+
Apples
Oranges
Pears
@@ -53,7 +53,7 @@
-
+
Apples
Oranges
Pears
@@ -67,7 +67,12 @@
-
+
Apple
Apricot
Avocado
@@ -105,12 +110,7 @@
-
+
Apple
Apricot
Avocado
@@ -148,7 +148,7 @@
-
+
Apple
Apricot
Avocado
@@ -186,7 +186,7 @@
-
+
Apple
Apricot
Avocado
@@ -240,7 +240,7 @@
-
+
Bird
Cat
Dog
@@ -249,7 +249,7 @@
-
+
Bird
Cat
Dog
@@ -263,14 +263,12 @@
Custom Interface Options
-
+
Pepperoni
Bacon
@@ -280,8 +278,15 @@
-
-
+
+
Pepperoni
Bacon
Extra Cheese
@@ -290,13 +295,8 @@
-
-
+
+
Pepperoni
Bacon
Extra Cheese
@@ -305,8 +305,8 @@
-
-
+
+
Pepperoni
Bacon
Extra Cheese
@@ -318,30 +318,22 @@
diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png
index b1a10395d7c..0becb62c81f 100644
Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png
index b55a80fa526..19954ab3299 100644
Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png
index 4c235db6b42..7884f70bed7 100644
Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-action-sheet-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png
index 8ce21e9a05e..47b045258b6 100644
Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png
index 3394b979256..527708ee45f 100644
Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png
index 40af8b073d5..d5869e74cbb 100644
Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Chrome-linux.png
index 92064755809..16c11b07e63 100644
Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Firefox-linux.png
index 09dbd660ee0..c88adff248c 100644
Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Safari-linux.png
index ed6becd6e68..93e0f922f04 100644
Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Safari-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-alert-scroll-to-selected-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png
index 058e9eb36b8..2ce706c3a85 100644
Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png
index f6dda21ddea..ef499318f24 100644
Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png
index a9540533623..426f785d18b 100644
Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Chrome-linux.png
index 48f5106e004..2e11447cc37 100644
Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Firefox-linux.png
index e13afdfc587..ed72883a1a0 100644
Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Safari-linux.png
index 82b3f630513..7717021c56a 100644
Binary files a/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Safari-linux.png and b/core/src/components/select/test/basic/select.e2e.ts-snapshots/select-basic-popover-scroll-to-selected-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select/test/disabled/select.e2e.ts b/core/src/components/select/test/disabled/select.e2e.ts
index 621ed048d3e..e567e2ff82e 100644
--- a/core/src/components/select/test/disabled/select.e2e.ts
+++ b/core/src/components/select/test/disabled/select.e2e.ts
@@ -1,8 +1,83 @@
import { expect } from '@playwright/test';
+import type { E2ELocator } from '@utils/test/playwright';
import { configs, test } from '@utils/test/playwright';
+const DISABLED_OPTION_INTERFACES = [
+ {
+ name: 'action-sheet',
+ overlayTag: 'ion-action-sheet',
+ didPresent: 'ionActionSheetDidPresent',
+ didDismiss: 'ionActionSheetDidDismiss',
+ // The option itself is the interactive button.
+ controlSuffix: '',
+ },
+ {
+ name: 'alert',
+ overlayTag: 'ion-alert',
+ didPresent: 'ionAlertDidPresent',
+ didDismiss: 'ionAlertDidDismiss',
+ // The option itself is the interactive radio button.
+ controlSuffix: '',
+ },
+ {
+ name: 'popover',
+ overlayTag: 'ion-popover',
+ didPresent: 'ionPopoverDidPresent',
+ didDismiss: 'ionPopoverDidDismiss',
+ // The interactive control is the nested ion-radio.
+ controlSuffix: ' ion-radio',
+ },
+ {
+ name: 'modal',
+ overlayTag: 'ion-modal',
+ didPresent: 'ionModalDidPresent',
+ didDismiss: 'ionModalDidDismiss',
+ // The interactive control is the nested ion-radio.
+ controlSuffix: ' ion-radio',
+ },
+] as const;
+
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('select: disabled options'), () => {
+ for (const { name, overlayTag, didPresent, didDismiss, controlSuffix } of DISABLED_OPTION_INTERFACES) {
+ test(`${name}: clicking a disabled option should not change the value or dismiss the overlay`, async ({
+ page,
+ }) => {
+ await page.setContent(
+ `
+
+ Oranges
+
+ `,
+ config
+ );
+
+ const select = page.locator('ion-select') as E2ELocator;
+
+ const ionChange = await select.spyOnEvent('ionChange');
+ const ionDidPresent = await page.spyOnEvent(didPresent);
+ const ionDidDismiss = await page.spyOnEvent(didDismiss);
+
+ await select.click();
+
+ await ionDidPresent.next();
+
+ const overlay = page.locator(overlayTag);
+ const disabledOption = overlay.locator(`.select-interface-option${controlSuffix}`);
+
+ await disabledOption.click({ force: true });
+
+ await page.waitForChanges();
+
+ const value = await select.evaluate((el: HTMLIonSelectElement) => el.value);
+ expect(value).toBeUndefined();
+
+ expect(ionChange).toHaveReceivedEventTimes(0);
+ expect(ionDidDismiss).toHaveReceivedEventTimes(0);
+ await expect(overlay).toBeVisible();
+ });
+ }
+
test('should not focus a disabled option when no value is set', async ({ page, skip }) => {
// TODO (ROU-5437)
skip.browser('webkit', 'Safari 16 only allows text fields and pop-up menus to be focused.');
diff --git a/core/src/components/select/test/rich-content-option/index.html b/core/src/components/select/test/rich-content-option/index.html
new file mode 100644
index 00000000000..78d3f3987c9
--- /dev/null
+++ b/core/src/components/select/test/rich-content-option/index.html
@@ -0,0 +1,696 @@
+
+
+
+
+ Select - Rich Content Option
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Select - Rich Content Option
+
+
+
+
+
+
+ Single Value
+
+
+
+
+
+ NEW
+
+
+
+ Full Content
+
+
+
+
+
+ Slots
+ NEW
+
+
+
+
+
+ Start Slot
+
+
+ End Slot
+ NEW
+
+ Description
+
+ Inner Content
+ NEW
+
+
+
+
+
+ This is a span element
+ NEW
+
+
+
+
+
+ NEW
+
+
+
+
+
+ This is a very long option label that demonstrates how the start and end slots stay at their intrinsic
+ widths while the content area absorbs the remaining row width
+ NEW
+
+
+
+
+
+ SVGs
+
+
+
+
+
+
+
+
+
+
+ Icons
+
+
+
+
+ Images
+
+
+
+
+
+
+ Thumbnails
+
+
+
+
+
+
+
+
+
+
+ NEW
+
+
+
+ Full Content
+
+
+
+
+
+ Slots
+ NEW
+
+
+
+
+
+ Start Slot
+
+
+ End Slot
+ NEW
+
+ Description
+
+ Inner Content
+ NEW
+
+
+
+
+
+ This is a span element
+ NEW
+
+
+
+
+
+ NEW
+
+
+
+
+
+ This is a very long option label that demonstrates how the start and end slots stay at their intrinsic
+ widths while the content area absorbs the remaining row width
+ NEW
+
+
+
+
+
+ SVGs
+
+
+
+
+
+
+
+
+
+
+ Icons
+
+
+
+
+ Images
+
+
+
+
+
+
+ Thumbnails
+
+
+
+
+
+
+
+
+
+
+ NEW
+
+
+
+ Full Content
+
+
+
+
+
+ Slots
+ NEW
+
+
+
+
+
+ Start Slot
+
+
+ End Slot
+ NEW
+
+ Description
+
+ Inner Content
+ NEW
+
+
+
+
+
+ This is a span element
+ NEW
+
+
+
+
+
+ NEW
+
+
+
+
+
+ This is a very long option label that demonstrates how the start and end slots stay at their intrinsic
+ widths while the content area absorbs the remaining row width
+ NEW
+
+
+
+
+
+ SVGs
+
+
+
+
+
+
+
+
+
+
+ Icons
+
+
+
+
+ Images
+
+
+
+
+
+
+ Thumbnails
+
+
+
+
+
+
+
+
+
+
+ NEW
+
+
+
+ Full Content
+
+
+
+
+
+ Slots
+ NEW
+
+
+
+
+
+ Start Slot
+
+
+ End Slot
+ NEW
+
+ Description
+
+ Inner Content
+ NEW
+
+
+
+
+
+ This is a span element
+ NEW
+
+
+
+
+
+ NEW
+
+
+
+
+
+ This is a very long option label that demonstrates how the start and end slots stay at their intrinsic
+ widths while the content area absorbs the remaining row width
+ NEW
+
+
+
+
+
+ SVGs
+
+
+
+
+
+
+
+
+
+
+ Icons
+
+
+
+
+ Images
+
+
+
+
+
+
+ Thumbnails
+
+
+
+
+
+
+
+
+
+
+ Multiple Value
+
+
+
+
+
+ NEW
+
+
+
+ Full Content
+
+
+
+
+
+ Slots
+ NEW
+
+
+
+
+
+ Start Slot
+
+
+ End Slot
+ NEW
+
+ Description
+
+ Inner Content
+ NEW
+
+
+
+
+
+ This is a span element
+ NEW
+
+
+
+
+
+ NEW
+
+
+
+
+
+ This is a very long option label that demonstrates how the start and end slots stay at their intrinsic
+ widths while the content area absorbs the remaining row width
+ NEW
+
+
+
+
+
+ SVGs
+
+
+
+
+
+
+
+
+
+
+ Icons
+
+
+
+
+ Images
+
+
+
+
+
+
+ Thumbnails
+
+
+
+
+
+
+
+
+
+
+ NEW
+
+
+
+ Full Content
+
+
+
+
+
+ Slots
+ NEW
+
+
+
+
+
+ Start Slot
+
+
+ End Slot
+ NEW
+
+ Description
+
+ Inner Content
+ NEW
+
+
+
+
+
+ This is a span element
+ NEW
+
+
+
+
+
+ NEW
+
+
+
+
+
+ This is a very long option label that demonstrates how the start and end slots stay at their intrinsic
+ widths while the content area absorbs the remaining row width
+ NEW
+
+
+
+
+
+ SVGs
+
+
+
+
+
+
+ SVG
+
+
+
+
+ Icons
+
+
+
+
+ Images
+
+
+
+
+
+
+ Thumbnails
+
+
+
+
+
+
+
+
+
+
+ NEW
+
+
+
+ Full Content
+
+
+
+
+
+ Slots
+ NEW
+
+
+
+
+
+ Start Slot
+
+
+ End Slot
+ NEW
+
+ Description
+
+ Inner Content
+ NEW
+
+
+
+
+
+ This is a span element
+ NEW
+
+
+
+
+
+ NEW
+
+
+
+
+
+ This is a very long option label that demonstrates how the start and end slots stay at their intrinsic
+ widths while the content area absorbs the remaining row width
+ NEW
+
+
+
+
+
+ SVGs
+
+
+
+
+
+
+
+
+
+
+ Icons
+
+
+
+
+ Images
+
+
+
+
+
+
+ Thumbnails
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts b/core/src/components/select/test/rich-content-option/select.e2e.ts
new file mode 100644
index 00000000000..3acf2d806ac
--- /dev/null
+++ b/core/src/components/select/test/rich-content-option/select.e2e.ts
@@ -0,0 +1,484 @@
+import { expect } from '@playwright/test';
+import { configs, test } from '@utils/test/playwright';
+
+/**
+ * This behavior does not vary across directions
+ */
+configs({ directions: ['ltr'], modes: ['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('should not have visual regressions for the action sheet interface', async ({ page }) => {
+ const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent');
+
+ await page.locator('#action-sheet-select').click();
+ await ionActionSheetDidPresent.next();
+
+ // Move mouse to away from the alert so hover styles don't interfere with screenshots
+ await page.mouse.move(0, 0);
+
+ const firstOption = page.locator('ion-action-sheet .select-interface-option').first();
+
+ await expect(firstOption).toHaveScreenshot(screenshot(`select-rich-content-action-sheet`));
+ });
+
+ test('should not have visual regressions for the alert interface', async ({ page }) => {
+ const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
+
+ await page.locator('#alert-select').click();
+ await ionAlertDidPresent.next();
+
+ // Move mouse to away from the alert so hover styles don't interfere with screenshots
+ await page.mouse.move(0, 0);
+
+ const firstOption = page.locator('ion-alert .select-interface-option').first();
+
+ await expect(firstOption).toHaveScreenshot(screenshot(`select-rich-content-alert`));
+ });
+
+ test('should not have visual regressions for the modal interface', async ({ page }) => {
+ const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
+
+ await page.locator('#modal-select').click();
+ await ionModalDidPresent.next();
+
+ // Move mouse to away from the alert so hover styles don't interfere with screenshots
+ await page.mouse.move(0, 0);
+
+ const firstOption = page.locator('ion-modal .select-interface-option').first();
+
+ await expect(firstOption).toHaveScreenshot(screenshot(`select-rich-content-modal`));
+ });
+
+ test('should not have visual regressions for the popover interface', async ({ page }) => {
+ const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
+
+ await page.locator('#popover-select').click();
+ await ionPopoverDidPresent.next();
+
+ // Move mouse to away from the alert so hover styles don't interfere with screenshots
+ await page.mouse.move(0, 0);
+
+ const firstOption = page.locator('ion-popover .select-interface-option').first();
+
+ await expect(firstOption).toHaveScreenshot(screenshot(`select-rich-content-popover`));
+ });
+ });
+});
+
+/**
+ * This behavior does not vary across modes/directions
+ */
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
+ test.describe(title('select: rich content option functionality'), () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/src/components/select/test/rich-content-option', config);
+ });
+
+ test('it should render for action sheet interface', async ({ page }) => {
+ const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent');
+ const ionActionSheetDidDismiss = await page.spyOnEvent('ionActionSheetDidDismiss');
+
+ const select = page.locator('#action-sheet-select');
+
+ await select.click();
+
+ await ionActionSheetDidPresent.next();
+
+ const actionSheet = page.locator('ion-action-sheet');
+ const firstOption = actionSheet.locator('.select-interface-option').first();
+ const avatar = firstOption.locator('ion-avatar');
+ const firstOptionText = 'Full Content';
+
+ await expect(firstOption).toContainText(firstOptionText);
+ await expect(avatar).toBeVisible();
+
+ // Click on the first option
+ await firstOption.click();
+
+ await ionActionSheetDidDismiss.next();
+
+ // Verify that the select text includes the option text
+ const selectText = await select.locator('.select-text').textContent();
+
+ expect(selectText).toContain(firstOptionText);
+
+ // Verify that the select text does not include the avatar and badge
+ const selectTextAvatar = select.locator('.select-text ion-avatar');
+ const selectTextBadge = select.locator('.select-text ion-badge');
+
+ await expect(selectTextAvatar).toHaveCount(0);
+ await expect(selectTextBadge).toHaveCount(0);
+ });
+
+ 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');
+
+ await select.click();
+
+ await ionAlertDidPresent.next();
+
+ const alert = page.locator('ion-alert');
+ const firstOption = alert.locator('.select-interface-option').first();
+ const avatar = firstOption.locator('ion-avatar');
+ const firstOptionText = 'Full Content';
+
+ await expect(firstOption).toContainText(firstOptionText);
+ await expect(avatar).toBeVisible();
+
+ // 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();
+
+ // Verify that the select text includes the option text
+ const selectText = await select.locator('.select-text').textContent();
+
+ expect(selectText).toContain(firstOptionText);
+
+ // Verify that the select text does not include the avatar and badge
+ const selectTextAvatar = select.locator('.select-text ion-avatar');
+ const selectTextBadge = select.locator('.select-text ion-badge');
+
+ await expect(selectTextAvatar).toHaveCount(0);
+ await expect(selectTextBadge).toHaveCount(0);
+ });
+
+ 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('#alert-select-multiple');
+
+ await select.click();
+
+ await ionAlertDidPresent.next();
+
+ const alert = page.locator('ion-alert');
+ const firstOption = alert.locator('.select-interface-option').first();
+ const avatar = firstOption.locator('ion-avatar');
+ const firstOptionText = 'Full Content';
+
+ await expect(firstOption).toContainText(firstOptionText);
+ await expect(avatar).toBeVisible();
+
+ // 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();
+
+ // Verify that the select text includes the option text
+ const selectText = await select.locator('.select-text').textContent();
+
+ expect(selectText).toContain(firstOptionText);
+
+ // Verify that the select text does not include the avatar and badge
+ const selectTextAvatar = select.locator('.select-text ion-avatar');
+ const selectTextBadge = select.locator('.select-text ion-badge');
+
+ await expect(selectTextAvatar).toHaveCount(0);
+ 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');
+
+ const select = page.locator('#modal-select');
+
+ await select.click();
+
+ await ionModalDidPresent.next();
+
+ const modal = page.locator('ion-modal');
+ const firstOption = modal.locator('.select-interface-option').first();
+ const avatar = firstOption.locator('ion-avatar');
+ const firstOptionText = 'Full Content';
+
+ await expect(firstOption).toContainText(firstOptionText);
+ await expect(avatar).toBeVisible();
+
+ // Click on the first option
+ await firstOption.click();
+
+ await ionModalDidDismiss.next();
+
+ // Verify that the select text includes the option text
+ const selectText = await select.locator('.select-text').textContent();
+
+ expect(selectText).toContain(firstOptionText);
+
+ // Verify that the select text does not include the avatar and badge
+ const selectTextAvatar = select.locator('.select-text ion-avatar');
+ const selectTextBadge = select.locator('.select-text ion-badge');
+
+ await expect(selectTextAvatar).toHaveCount(0);
+ 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');
+
+ const select = page.locator('#modal-select-multiple');
+
+ await select.click();
+
+ await ionModalDidPresent.next();
+
+ const modal = page.locator('ion-modal');
+ const firstOption = modal.locator('.select-interface-option').first();
+ const avatar = firstOption.locator('ion-avatar');
+ const firstOptionText = 'Full Content';
+
+ await expect(firstOption).toContainText(firstOptionText);
+ await expect(avatar).toBeVisible();
+
+ // Click on the first option
+ await firstOption.click();
+
+ // Confirm the selection
+ const cancelButton = modal.getByRole('button', { name: 'Cancel' });
+
+ await cancelButton.click();
+ await ionModalDidDismiss.next();
+
+ // Verify that the select text includes the option text
+ const selectText = await select.locator('.select-text').textContent();
+
+ expect(selectText).toContain(firstOptionText);
+
+ // Verify that the select text does not include the avatar and badge
+ const selectTextAvatar = select.locator('.select-text ion-avatar');
+ const selectTextBadge = select.locator('.select-text ion-badge');
+
+ await expect(selectTextAvatar).toHaveCount(0);
+ 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');
+
+ const select = page.locator('#popover-select');
+
+ await select.click();
+
+ await ionPopoverDidPresent.next();
+
+ const popover = page.locator('ion-popover');
+ const firstOption = popover.locator('.select-interface-option').first();
+ const avatar = firstOption.locator('ion-avatar');
+ const firstOptionText = 'Full Content';
+
+ await expect(firstOption).toContainText(firstOptionText);
+ await expect(avatar).toBeVisible();
+
+ // Click on the first option
+ await firstOption.click();
+
+ await ionPopoverDidDismiss.next();
+
+ // Verify that the select text includes the option text
+ const selectText = await select.locator('.select-text').textContent();
+
+ expect(selectText).toContain(firstOptionText);
+
+ // Verify that the select text does not include the avatar and badge
+ const selectTextAvatar = select.locator('.select-text ion-avatar');
+ const selectTextBadge = select.locator('.select-text ion-badge');
+
+ await expect(selectTextAvatar).toHaveCount(0);
+ 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');
+
+ const select = page.locator('#popover-select-multiple');
+
+ await select.click();
+
+ await ionPopoverDidPresent.next();
+
+ const popover = page.locator('ion-popover');
+ const firstOption = popover.locator('.select-interface-option').first();
+ const avatar = firstOption.locator('ion-avatar');
+ const firstOptionText = 'Full Content';
+
+ await expect(firstOption).toContainText(firstOptionText);
+ await expect(avatar).toBeVisible();
+
+ // Click on the first option
+ await firstOption.click();
+
+ // Confirm the selection
+ const backdrop = page.locator('ion-backdrop');
+ await backdrop.click({ position: { x: 10, y: 10 } });
+
+ await ionPopoverDidDismiss.next();
+
+ // Verify that the select text includes the option text
+ const selectText = await select.locator('.select-text').textContent();
+
+ expect(selectText).toContain(firstOptionText);
+
+ // Verify that the select text does not include the avatar and badge
+ const selectTextAvatar = select.locator('.select-text ion-avatar');
+ const selectTextBadge = select.locator('.select-text ion-badge');
+
+ await expect(selectTextAvatar).toHaveCount(0);
+ await expect(selectTextBadge).toHaveCount(0);
+ });
+
+ test('it should render the aria label as plain text', async ({ page }) => {
+ const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
+ const ionAlertDidDismiss = await page.spyOnEvent('ionAlertDidDismiss');
+
+ const select = page.locator('#alert-select');
+
+ await select.click();
+
+ await ionAlertDidPresent.next();
+
+ // The "no-text" option only has a for its label content,
+ // so its aria label should be the span's plain text.
+ const alert = page.locator('ion-alert');
+ const spanOption = alert.locator('.alert-radio-button', { hasText: 'This is a span element' });
+
+ await spanOption.click();
+
+ const confirmButton = alert.locator('.alert-button:not(.alert-button-role-cancel)');
+ await confirmButton.click();
+
+ await ionAlertDidDismiss.next();
+
+ const nativeButton = select.locator('button');
+ const ariaLabel = await nativeButton.getAttribute('aria-label');
+
+ expect(ariaLabel).toContain('This is a span element');
+ });
+ });
+});
+
+/**
+ * This behavior does not vary across modes
+ */
+configs({ modes: ['md'] }).forEach(({ title, 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 slots in the correct places', async ({ page }) => {
+ const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
+
+ const select = page.locator('#alert-select');
+
+ await select.click();
+
+ await ionAlertDidPresent.next();
+
+ const alert = page.locator('ion-alert');
+ const firstOption = alert.locator('.alert-radio-button').first();
+ const startContainer = firstOption.locator('.select-option-start');
+ const endContainer = firstOption.locator('.select-option-end');
+
+ const avatar = startContainer.locator('ion-avatar');
+ const badge = endContainer.locator('ion-badge');
+
+ await expect(avatar).toBeVisible();
+ await expect(badge).toBeVisible();
+
+ const isRTL = await page.evaluate(() => document.dir === 'rtl');
+ const optionBox = await firstOption.boundingBox();
+ const startBox = await startContainer.boundingBox();
+ const endBox = await endContainer.boundingBox();
+ const optionMidpointX = optionBox!.x + optionBox!.width / 2;
+
+ if (isRTL) {
+ // Verify the start container is rendered on the right,
+ // and the end container is rendered on the left
+ expect(startBox!.x).toBeGreaterThan(optionMidpointX);
+ expect(endBox!.x).toBeLessThan(optionMidpointX);
+ } else {
+ // Verify the start container is rendered on the left,
+ // and the end container is rendered on the right
+ expect(startBox!.x).toBeLessThan(optionMidpointX);
+ expect(endBox!.x).toBeGreaterThan(optionMidpointX);
+ }
+ });
+ });
+});
+
+/**
+ * This behavior does not vary across modes/directions
+ */
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
+ test.describe(title('select: rich content options'), () => {
+ test('it should only render text nodes when `innerHTMLTemplatesEnabled` is disabled', async ({ page }) => {
+ await page.setContent(
+ `
+
+
+ NEW
+
+
+
+ Full Content
+ This is a span element
+
+
+ `,
+ config
+ );
+
+ const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
+ const ionAlertDidDismiss = await page.spyOnEvent('ionAlertDidDismiss');
+
+ const select = page.locator('#alert-select');
+
+ await select.click();
+
+ await ionAlertDidPresent.next();
+
+ const alert = page.locator('ion-alert');
+ const firstOption = alert.locator('.alert-radio-button').first();
+ const startContainer = firstOption.locator('.select-option-start');
+ const endContainer = firstOption.locator('.select-option-end');
+ const span = firstOption.locator('.span-style');
+
+ await expect(startContainer).toHaveCount(0);
+ await expect(endContainer).toHaveCount(0);
+ await expect(span).toHaveCount(0);
+
+ // 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();
+
+ const selectText = select.locator('.select-text');
+ const selectTextSpan = selectText.locator('.span-style');
+
+ await expect(selectTextSpan).toHaveCount(0);
+ });
+ });
+});
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..37f31b6384d
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..9d7ddfaf83f
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..b65bbfd9a09
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..76ccc2b5eb6
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..2b3d84a04e5
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..ea751c3a109
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-action-sheet-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..65b83d62d23
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..9379f05868b
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..0e48794f233
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..a741ba882d2
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..db773de4fa1
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..c64bbb9e6cc
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-alert-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..886d1dd9f7d
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..743ddd27643
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..063821f6cd7
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..7bd6d9b5eb2
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..d43429fe447
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..0500953c143
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-modal-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..815c9a3e0da
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..4e96b0d7f45
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..528b3b23802
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-ios-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..c95a90b27dd
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..e10c5a191e9
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..ef1b48f49f0
Binary files /dev/null and b/core/src/components/select/test/rich-content-option/select.e2e.ts-snapshots/select-rich-content-popover-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select/test/select.spec.tsx b/core/src/components/select/test/select.spec.tsx
index f00c4c66884..ad7c0d3d050 100644
--- a/core/src/components/select/test/select.spec.tsx
+++ b/core/src/components/select/test/select.spec.tsx
@@ -1,6 +1,8 @@
import { h } from '@stencil/core';
import { newSpecPage } from '@stencil/core/testing';
+import { config } from '../../../global/config';
+import { SelectOption } from '../../select-option/select-option';
import { Select } from '../select';
describe('ion-select', () => {
@@ -157,3 +159,56 @@ describe('ion-select: required', () => {
expect(nativeButton.getAttribute('aria-required')).toBe('false');
});
});
+
+describe('ion-select: option content property reflection', () => {
+ beforeEach(() => {
+ // Cloning rich option content into the select text only happens when
+ // custom HTML rendering is enabled.
+ config.reset({ innerHTMLTemplatesEnabled: true });
+ });
+
+ afterEach(() => {
+ config.reset({});
+ });
+
+ it('should reflect ion-icon DOM properties onto attributes so they survive cloning into the select text', async () => {
+ const page = await newSpecPage({
+ components: [Select, SelectOption],
+ html: ` Star `,
+ });
+
+ const select = page.body.querySelector('ion-select')!;
+ const sourceIcon = select.querySelector('ion-icon')!;
+
+ /**
+ * Frameworks such as Vue set `icon` as a DOM property rather than an
+ * attribute. `cloneNode` only copies attributes, so without reflection
+ * the cloned copy in the select text would lose the icon value.
+ */
+ (sourceIcon as any).icon = 'logo-ionic';
+
+ // Selecting the option rebuilds the displayed text from the option content.
+ select.value = 'star';
+ await page.waitForChanges();
+
+ const renderedIcon = select.shadowRoot!.querySelector('.select-text ion-icon');
+ expect(renderedIcon).not.toBeNull();
+ expect(renderedIcon!.getAttribute('icon')).toBe('logo-ionic');
+ });
+
+ it('should preserve an ion-icon attribute that is already set when cloning into the select text', async () => {
+ const page = await newSpecPage({
+ components: [Select, SelectOption],
+ html: ` Star `,
+ });
+
+ const select = page.body.querySelector('ion-select')!;
+
+ select.value = 'star';
+ await page.waitForChanges();
+
+ const renderedIcon = select.shadowRoot!.querySelector('.select-text ion-icon');
+ expect(renderedIcon).not.toBeNull();
+ expect(renderedIcon!.getAttribute('icon')).toBe('logo-ionic');
+ });
+});
diff --git a/core/src/utils/overlay-control-label.ts b/core/src/utils/overlay-control-label.ts
new file mode 100644
index 00000000000..ffecb19fac7
--- /dev/null
+++ b/core/src/utils/overlay-control-label.ts
@@ -0,0 +1,56 @@
+// TODO(FW-6886, FW-6892, FW-6891): Remove this file in favor of the Modular Ionic component config. Each overlay will be able to select its own defaults for label placement and justify based on the interface and mode, so this utility will no longer be necessary.
+
+import type { Mode } from '../interface';
+
+/**
+ * Returns the default `labelPlacement` for a radio or checkbox option
+ * rendered inside an overlay. Defaults follow each mode's established
+ * option-row layout:
+ * - `ios`: `"start"` for radio in `alert` and `popover`. The `modal`
+ * interface flips iOS radio back to `"end"`. Checkbox is always
+ * `"end"` on iOS.
+ * - everything else (e.g. `md`): `"end"`.
+ *
+ * `interfaceType` is optional; only `"modal"` changes the result, so
+ * callers that aren't a modal can omit it.
+ *
+ * Used by `select-popover` and `select-modal` as the fallback when an
+ * option doesn't explicitly set `labelPlacement`.
+ */
+export const getOverlayLabelPlacement = (
+ mode: Mode,
+ control: 'radio' | 'checkbox',
+ interfaceType?: 'alert' | 'popover' | 'modal'
+): 'start' | 'end' => {
+ if (mode === 'ios' && control === 'radio' && interfaceType !== 'modal') {
+ return 'start';
+ }
+
+ return 'end';
+};
+
+/**
+ * Returns the default `justify` for a radio or checkbox option rendered
+ * inside an overlay. Defaults follow each mode's option-row layout:
+ * - `ios`: `"space-between"` for radio in `alert` and `popover`. The
+ * `modal` interface falls back to `"start"`. Checkbox is always `"start"`
+ * on iOS.
+ * - everything else (e.g. `md`): `"start"`.
+ *
+ * `interfaceType` is optional; only `"modal"` changes the result, so
+ * callers that aren't a modal can omit it.
+ *
+ * Used by `select-popover` and `select-modal` as the fallback when an
+ * option doesn't explicitly set `justify`.
+ */
+export const getOverlayLabelJustify = (
+ mode: Mode,
+ control: 'radio' | 'checkbox',
+ interfaceType?: 'alert' | 'popover' | 'modal'
+): 'start' | 'end' | 'space-between' => {
+ if (mode === 'ios' && control === 'radio' && interfaceType !== 'modal') {
+ return 'space-between';
+ }
+
+ return 'start';
+};
diff --git a/core/src/utils/sanitization/index.ts b/core/src/utils/sanitization/index.ts
index acab505d828..bb851fea7e0 100644
--- a/core/src/utils/sanitization/index.ts
+++ b/core/src/utils/sanitization/index.ts
@@ -1,8 +1,26 @@
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 narrow `domStringAllowedAttributes` list (refer
+ * `sanitizeElement`), and scrubs script-scheme 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`. Use `sanitizeDOMTree` instead when you already
+ * have a DOM tree and want to sanitize it in place without a string round
+ * trip. The two apply the same dangerous-vector scrubbing but different
+ * attribute allowlists: this path stays narrow so existing consumers
+ * (toast, loading, alert message, etc.) are unaffected, while
+ * `sanitizeDOMTree` uses the wider rich-content allowlist.
+ *
+ * @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 {
@@ -56,7 +74,7 @@ export const sanitizeDOMString = (untrustedString: IonicSafeString | string | un
/* eslint-disable-next-line */
for (let childIndex = 0; childIndex < childElements.length; childIndex++) {
- sanitizeElement(childElements[childIndex]);
+ sanitizeElement(childElements[childIndex], domStringAllowedAttributes);
}
}
});
@@ -71,7 +89,7 @@ export const sanitizeDOMString = (untrustedString: IonicSafeString | string | un
/* eslint-disable-next-line */
for (let childIndex = 0; childIndex < dfChildren.length; childIndex++) {
- sanitizeElement(dfChildren[childIndex]);
+ sanitizeElement(dfChildren[childIndex], domStringAllowedAttributes);
}
// Append document fragment to div
@@ -88,13 +106,46 @@ export const sanitizeDOMString = (untrustedString: IonicSafeString | string | un
}
};
+/**
+ * Sanitize an entire trusted DOM tree in place.
+ *
+ * Removes blocked tags (`script`, `iframe`, etc.) from the subtree and
+ * then sanitizes attributes on every remaining element using the wider
+ * `richContentAllowedAttributes` allowlist (refer `sanitizeElement`).
+ * Component presentational attributes (`size`, `color`, `shape`, inline
+ * SVG, `aria-*`, `data-*`) are preserved; `style`, event handlers (`on*`),
+ * form/navigation-hijack attributes, script-scheme URLs, and non-image
+ * `data:` URLs are stripped.
+ *
+ * Use this when you have a DOM tree the developer controls (e.g.
+ * cloned slot content from a component) and you need to render it
+ * elsewhere safely.
+ *
+ * @param root - The root element whose subtree will be sanitized in
+ * place. No-op when the sanitizer is disabled via `Ionic.config`.
+ */
+export const sanitizeDOMTree = (root: HTMLElement) => {
+ if (!isSanitizerEnabled()) {
+ return;
+ }
+
+ blockedTags.forEach((tag) => {
+ const matches = root.querySelectorAll(tag);
+ for (let i = matches.length - 1; i >= 0; i--) {
+ matches[i].remove();
+ }
+ });
+
+ sanitizeElement(root, richContentAllowedAttributes, richContentAllowedAttributePrefixes);
+};
+
/**
* 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, allowedAttributes: string[], allowedAttributePrefixes: string[] = []) => {
// IE uses childNodes, so ignore nodes that are not elements
if (element.nodeType && element.nodeType !== 1) {
return;
@@ -111,12 +162,27 @@ const sanitizeElement = (element: any) => {
return;
}
+ /**
+ * Always strip `style` (CSS injection, `background:url()` beaconing, UI
+ * spoofing). It is never on the allowlist, but it is removed explicitly
+ * here because some engines (e.g. jsdom) don't enumerate the CSSOM-backed
+ * `style` attribute in `element.attributes`, which would let the loop
+ * below skip over it.
+ */
+ element.removeAttribute('style');
+
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())) {
+ /**
+ * Remove any attribute that is not on the allowlist. This drops event
+ * handlers (`on*`), `style`, the form/navigation-hijack attributes
+ * (`formaction`, `action`, `target`), namespaced attributes like
+ * `xlink:href`, and anything else not explicitly known to be safe.
+ */
+ if (!isAttributeAllowed(lowerName, allowedAttributes, allowedAttributePrefixes)) {
element.removeAttribute(attributeName);
continue;
}
@@ -124,22 +190,40 @@ const sanitizeElement = (element: any) => {
// clean up any allowed attribs
// that attempt to do any JS funny-business
const attributeValue = attribute.value;
+ if (attributeValue == null) {
+ continue;
+ }
/**
- * We also need to check the property value
- * as javascript: can allow special characters
- * such as 	 and still be valid (i.e. java	script)
+ * Scrub dangerous schemes from the value. The value is normalized first
+ * (whitespace and ASCII control characters removed) so entity-obfuscated
+ * payloads such as `java script:`, which the parser decodes to
+ * `java\tscript:`, are still caught. Normalizing the value also covers
+ * namespaced attributes, where the previous `element[attributeName]`
+ * property reflection returned `undefined` and let them slip through.
*/
- const propertyValue = element[attributeName];
+ const normalizedValue = attributeValue.replace(controlCharactersAndWhitespace, '').toLowerCase();
- /* eslint-disable */
+ // Script schemes are never allowed, on any attribute.
+ if (normalizedValue.includes('javascript:') || normalizedValue.includes('vbscript:')) {
+ element.removeAttribute(attributeName);
+ continue;
+ }
+
+ /**
+ * For URL-bearing attributes, allow `data:` URIs only for raster
+ * images. Document-bearing types (`text/html`, `image/svg+xml`,
+ * `application/*`, etc.) can carry markup or script when navigated to
+ * or rendered, so they are stripped, while safe image types are kept so
+ * inline images keep working.
+ */
if (
- (attributeValue != null && attributeValue.toLowerCase().includes('javascript:')) ||
- (propertyValue != null && propertyValue.toLowerCase().includes('javascript:'))
+ urlAttributes.includes(lowerName) &&
+ normalizedValue.startsWith('data:') &&
+ !safeDataImageUri.test(normalizedValue)
) {
element.removeAttribute(attributeName);
}
- /* eslint-enable */
}
/**
@@ -149,7 +233,7 @@ const sanitizeElement = (element: any) => {
/* eslint-disable-next-line */
for (let i = 0; i < childElements.length; i++) {
- sanitizeElement(childElements[i]);
+ sanitizeElement(childElements[i], allowedAttributes, allowedAttributePrefixes);
}
};
@@ -175,8 +259,199 @@ const isSanitizerEnabled = (): boolean => {
return true;
};
-const allowedAttributes = ['class', 'id', 'href', 'src', 'name', 'slot'];
-const blockedTags = ['script', 'style', 'iframe', 'meta', 'link', 'object', 'embed'];
+/**
+ * Mirror known custom-element DOM properties onto attributes so they
+ * survive `cloneNode`. Call this on a DOM subtree before cloning it for
+ * rendering elsewhere (e.g. cloning slotted option content into an
+ * overlay).
+ *
+ * Only sets the attribute when the property holds a non-empty string
+ * and the attribute isn't already present, so existing attributes
+ * take precedence.
+ *
+ * @param root - The root element whose subtree (and itself) will be
+ * inspected.
+ */
+export const reflectPropertiesToAttributes = (root: Element): void => {
+ const candidates: Element[] = [];
+ if (root.tagName in elementPropsToReflect) {
+ candidates.push(root);
+ }
+ for (const tagName of Object.keys(elementPropsToReflect)) {
+ candidates.push(...Array.from(root.querySelectorAll(tagName.toLowerCase())));
+ }
+
+ for (const el of candidates) {
+ if (!(el.tagName in elementPropsToReflect)) {
+ continue;
+ }
+ const props = elementPropsToReflect[el.tagName];
+ for (const prop of props) {
+ const value = (el as unknown as Record)[prop];
+ if (typeof value === 'string' && value.length > 0 && !el.hasAttribute(prop)) {
+ el.setAttribute(prop, value);
+ }
+ }
+ }
+};
+
+/**
+ * Attribute allowlist for `sanitizeDOMString`. Intentionally narrow: this
+ * path sanitizes developer HTML strings rendered via `innerHTML` by
+ * `ion-toast`, `ion-loading`, the `ion-alert` message,
+ * `ion-refresher-content`, and `ion-infinite-scroll-content`, so the list
+ * is kept to the minimum those have always needed. Broadening it here would
+ * change sanitization output for all of those consumers, so the wider
+ * rich-content allowlist below is deliberately scoped to `sanitizeDOMTree`
+ * instead.
+ */
+const domStringAllowedAttributes = ['class', 'id', 'href', 'src', 'name', 'slot'];
+
+/**
+ * Attribute allowlist for `sanitizeDOMTree` (the select rich-content path).
+ * Covers global HTML attributes, the Ionic component presentational props
+ * that cloned rich content (e.g. `ion-select-option` markup) relies on, and
+ * the inert SVG presentation attributes used by inline icons.
+ *
+ * `aria-*` and `data-*` are allowed separately by prefix (refer
+ * `richContentAllowedAttributePrefixes`) since they are inert and not worth
+ * enumerating. URL-bearing names (`href`, `src`) are allowed here, but
+ * their values are still scrubbed for script schemes in `sanitizeElement`.
+ *
+ * Notably absent: `style`, event handlers (`on*`), and the
+ * form/navigation-hijack attributes (`formaction`, `action`, `target`),
+ * which are therefore stripped.
+ */
+const richContentAllowedAttributes = [
+ // Global / structural
+ 'class',
+ 'id',
+ 'slot',
+ 'name',
+ 'title',
+ 'alt',
+ 'lang',
+ 'dir',
+ 'role',
+ 'type',
+ 'value',
+ 'disabled',
+ 'width',
+ 'height',
+ 'href',
+ 'src',
+ // Ionic component presentational props
+ 'color',
+ 'size',
+ 'shape',
+ 'fill',
+ 'expand',
+ 'mode',
+ 'theme',
+ 'icon',
+ 'label',
+ 'label-placement',
+ 'justify',
+ 'inset',
+ 'lines',
+ 'ios',
+ 'md',
+ // SVG presentation attributes (compared lowercased, e.g. `viewBox`)
+ 'xmlns',
+ 'viewbox',
+ 'preserveaspectratio',
+ 'stroke',
+ 'stroke-width',
+ 'stroke-linecap',
+ 'stroke-linejoin',
+ 'stroke-opacity',
+ 'stroke-dasharray',
+ 'fill-rule',
+ 'fill-opacity',
+ 'clip-rule',
+ 'd',
+ 'points',
+ 'cx',
+ 'cy',
+ 'r',
+ 'rx',
+ 'ry',
+ 'x',
+ 'y',
+ 'x1',
+ 'y1',
+ 'x2',
+ 'y2',
+ 'transform',
+ 'opacity',
+];
+
+/**
+ * Attribute-name prefixes that are always safe to keep in the rich-content
+ * path. `aria-*` and `data-*` attributes cannot execute script or load
+ * resources, so they are allowed wholesale rather than enumerated by name.
+ */
+const richContentAllowedAttributePrefixes = ['aria-', 'data-'];
+
+/**
+ * Whether an attribute name (already lowercased) is safe to keep, by exact
+ * match or by an allowed prefix.
+ */
+const isAttributeAllowed = (
+ lowerName: string,
+ allowedAttributes: string[],
+ allowedAttributePrefixes: string[]
+): boolean => {
+ if (allowedAttributes.includes(lowerName)) {
+ return true;
+ }
+ return allowedAttributePrefixes.some((prefix) => lowerName.startsWith(prefix));
+};
+
+/**
+ * Matches ASCII control characters and whitespace (including the
+ * non-breaking space). Used to normalize attribute values before matching
+ * a URL scheme so entity-obfuscated payloads such as `java script:`
+ * (decoded by the parser to `java\tscript:`) can't smuggle a scheme past
+ * the check.
+ */
+// eslint-disable-next-line no-control-regex -- matching control characters is the point
+const controlCharactersAndWhitespace = /[\u0000-\u0020\u007f-\u00a0]/g;
+
+/**
+ * Attributes whose values are URLs. Their values are scheme-checked in
+ * `sanitizeElement` (e.g. `data:` filtering) beyond the script-scheme scrub
+ * applied to every attribute.
+ */
+const urlAttributes = ['href', 'src'];
+
+/**
+ * Matches a `data:` URI for a raster image type that cannot carry script.
+ * `image/svg+xml` is deliberately excluded (SVG can execute script), as are
+ * document types like `text/html` and `application/*`. The value is already
+ * lowercased and whitespace-stripped before this is tested.
+ */
+const safeDataImageUri = /^data:image\/(?:png|jpe?g|gif|webp|bmp|avif|x-icon|vnd\.microsoft\.icon)[;,]/;
+
+/**
+ * Tags removed entirely (with their subtree) before attribute sanitization.
+ * Exported so tests can assert the set without hardcoding it.
+ */
+export const blockedTags = ['script', 'style', 'iframe', 'meta', 'link', 'object', 'embed', 'base'];
+
+/**
+ * Properties on custom elements that frameworks (Vue, Angular) often
+ * set as DOM properties rather than attributes. `cloneNode` only copies
+ * attributes, so these values are lost when slotted content is cloned
+ * into an overlay. For each known custom element, we mirror the listed
+ * properties onto attributes so the cloned copy still has the data it
+ * needs to render.
+ *
+ * Keyed by uppercased tagName so the lookup matches `Element.tagName`.
+ */
+const elementPropsToReflect: Record = {
+ 'ION-ICON': ['icon', 'name', 'src', 'ios', 'md'],
+};
export class IonicSafeString {
constructor(public value: string) {}
diff --git a/core/src/utils/sanitization/test/sanitization.spec.ts b/core/src/utils/sanitization/test/sanitization.spec.ts
index 295dd306279..2ca069e387f 100644
--- a/core/src/utils/sanitization/test/sanitization.spec.ts
+++ b/core/src/utils/sanitization/test/sanitization.spec.ts
@@ -1,4 +1,4 @@
-import { IonicSafeString, sanitizeDOMString } from '..';
+import { blockedTags, IonicSafeString, reflectPropertiesToAttributes, sanitizeDOMString, sanitizeDOMTree } from '..';
describe('sanitizeDOMString', () => {
it('disable sanitizer', () => {
@@ -62,6 +62,258 @@ describe('sanitizeDOMString', () => {
)
).toEqual('Hello! Click me ');
});
+
+ it('strips rich-content attributes that are scoped to sanitizeDOMTree', () => {
+ /**
+ * Attributes only allowed by the wider sanitizeDOMTree (rich-content)
+ * allowlist must still be stripped here. This keeps the output unchanged
+ * for existing consumers (toast, loading, alert message, refresher and
+ * infinite-scroll content) that run their innerHTML through
+ * sanitizeDOMString.
+ */
+ expect(
+ sanitizeDOMString(
+ 'Hi '
+ )
+ ).toEqual('Hi ');
+ });
+});
+
+describe('sanitizeDOMTree', () => {
+ beforeEach(() => {
+ enableSanitizer(true);
+ });
+
+ it('should strip a blocked ';
+
+ sanitizeDOMTree(root);
+
+ expect(root.querySelector('script')).toBeNull();
+ expect(root.querySelector('p')).not.toBeNull();
+ });
+
+ it('should strip every blocked tag type', () => {
+ const root = document.createElement('div');
+ root.innerHTML = blockedTags.map((tag) => `<${tag}>${tag}>`).join('') + 'keep ';
+
+ sanitizeDOMTree(root);
+
+ for (const blocked of blockedTags) {
+ expect(root.querySelector(blocked)).toBeNull();
+ }
+ expect(root.querySelector('span')).not.toBeNull();
+ });
+
+ it('should strip blocked elements nested deep in the tree', () => {
+ const root = document.createElement('div');
+ root.innerHTML = '';
+
+ sanitizeDOMTree(root);
+
+ expect(root.querySelector('script')).toBeNull();
+ expect(root.querySelector('span')?.textContent).toBe('keep');
+ });
+
+ it('should remove on* event-handler attributes', () => {
+ const root = document.createElement('div');
+ root.innerHTML = 'click ';
+
+ sanitizeDOMTree(root);
+
+ const button = root.querySelector('button')!;
+ expect(button.hasAttribute('onclick')).toBe(false);
+ expect(button.hasAttribute('onmouseover')).toBe(false);
+ });
+
+ it('should strip javascript: URLs while keeping the element', () => {
+ const root = document.createElement('div');
+ root.innerHTML = 'link ';
+
+ sanitizeDOMTree(root);
+
+ const anchor = root.querySelector('a')!;
+ expect(anchor).not.toBeNull();
+ expect(anchor.hasAttribute('href')).toBe(false);
+ });
+
+ it('should preserve component attributes like size, color, and shape', () => {
+ const root = document.createElement('div');
+ root.innerHTML = 'button ';
+
+ sanitizeDOMTree(root);
+
+ const button = root.querySelector('ion-button')!;
+ expect(button.getAttribute('size')).toBe('small');
+ expect(button.getAttribute('color')).toBe('primary');
+ expect(button.getAttribute('shape')).toBe('round');
+ });
+
+ it('should strip the style attribute', () => {
+ const root = document.createElement('div');
+ root.innerHTML = 'text ';
+
+ sanitizeDOMTree(root);
+
+ expect(root.querySelector('span')!.hasAttribute('style')).toBe(false);
+ });
+
+ it('should strip form/navigation-hijack attributes', () => {
+ const root = document.createElement('div');
+ root.innerHTML = 'go ';
+
+ sanitizeDOMTree(root);
+
+ const button = root.querySelector('button')!;
+ expect(button.hasAttribute('formaction')).toBe(false);
+ expect(button.hasAttribute('action')).toBe(false);
+ expect(button.hasAttribute('target')).toBe(false);
+ });
+
+ it('should preserve inline SVG presentation attributes', () => {
+ const root = document.createElement('div');
+ root.innerHTML =
+ '' +
+ ' ';
+
+ sanitizeDOMTree(root);
+
+ const svg = root.querySelector('svg')!;
+ expect(svg.getAttribute('viewBox')).toBe('0 0 24 24');
+ expect(svg.getAttribute('width')).toBe('24');
+ const circle = root.querySelector('circle')!;
+ expect(circle.getAttribute('cx')).toBe('12');
+ expect(circle.getAttribute('r')).toBe('10');
+ expect(circle.getAttribute('fill')).toBe('red');
+ expect(circle.getAttribute('stroke-width')).toBe('2');
+ });
+
+ it('should preserve aria-* and data-* attributes', () => {
+ const root = document.createElement('div');
+ root.innerHTML = ' ';
+
+ sanitizeDOMTree(root);
+
+ const icon = root.querySelector('ion-icon')!;
+ expect(icon.getAttribute('aria-hidden')).toBe('true');
+ expect(icon.getAttribute('data-value')).toBe('star');
+ });
+
+ it('should strip namespaced attributes such as xlink:href', () => {
+ const root = document.createElement('div');
+ root.innerHTML = 'x ';
+
+ sanitizeDOMTree(root);
+
+ const anchor = root.querySelector('a')!;
+ expect(anchor.hasAttribute('xlink:href')).toBe(false);
+ });
+
+ it('should strip entity-obfuscated javascript: schemes', () => {
+ const root = document.createElement('div');
+ // The parser decodes to a tab, hiding the scheme from a naive
+ // substring check; normalization must still catch it.
+ root.innerHTML = 'link ';
+
+ sanitizeDOMTree(root);
+
+ expect(root.querySelector('a')!.hasAttribute('href')).toBe(false);
+ });
+
+ it('should strip vbscript: schemes', () => {
+ const root = document.createElement('div');
+ root.innerHTML = 'link ';
+
+ sanitizeDOMTree(root);
+
+ expect(root.querySelector('a')!.hasAttribute('href')).toBe(false);
+ });
+
+ it('should keep safe image data: URIs', () => {
+ const root = document.createElement('div');
+ const png = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNgAAAAAgAB';
+ root.innerHTML = ` `;
+
+ sanitizeDOMTree(root);
+
+ expect(root.querySelector('img')!.getAttribute('src')).toBe(png);
+ });
+
+ it('should strip document-bearing data: URIs', () => {
+ const root = document.createElement('div');
+ root.innerHTML =
+ 'html ' +
+ ' ';
+
+ sanitizeDOMTree(root);
+
+ expect(root.querySelector('a')!.hasAttribute('href')).toBe(false);
+ expect(root.querySelector('img')!.hasAttribute('src')).toBe(false);
+ });
+
+ it('should be a no-op when the sanitizer is disabled', () => {
+ enableSanitizer(false);
+ const root = document.createElement('div');
+ root.innerHTML = 'click ';
+
+ sanitizeDOMTree(root);
+
+ expect(root.querySelector('script')).not.toBeNull();
+ expect(root.querySelector('button')!.hasAttribute('onclick')).toBe(true);
+ });
+});
+
+describe('reflectPropertiesToAttributes', () => {
+ it('should reflect a known DOM property onto its attribute', () => {
+ const icon = document.createElement('ion-icon');
+ (icon as any).name = 'star';
+
+ reflectPropertiesToAttributes(icon);
+
+ expect(icon.getAttribute('name')).toBe('star');
+ });
+
+ it('should reflect properties on a nested element', () => {
+ const root = document.createElement('div');
+ const icon = document.createElement('ion-icon');
+ (icon as any).icon = 'logo-ionic';
+ root.appendChild(icon);
+
+ reflectPropertiesToAttributes(root);
+
+ expect(icon.getAttribute('icon')).toBe('logo-ionic');
+ });
+
+ it('should not overwrite an attribute that is already present', () => {
+ const icon = document.createElement('ion-icon');
+ icon.setAttribute('name', 'existing');
+ (icon as any).name = 'from-property';
+
+ reflectPropertiesToAttributes(icon);
+
+ expect(icon.getAttribute('name')).toBe('existing');
+ });
+
+ it('should ignore empty-string and non-string property values', () => {
+ const icon = document.createElement('ion-icon');
+ (icon as any).name = '';
+ (icon as any).icon = 42;
+
+ reflectPropertiesToAttributes(icon);
+
+ expect(icon.hasAttribute('name')).toBe(false);
+ expect(icon.hasAttribute('icon')).toBe(false);
+ });
+
+ it('should leave elements without reflected properties untouched', () => {
+ const div = document.createElement('div');
+ (div as any).name = 'value';
+
+ reflectPropertiesToAttributes(div);
+
+ expect(div.hasAttribute('name')).toBe(false);
+ });
});
const enableSanitizer = (enable = true) => {
diff --git a/core/src/utils/select-option-render.tsx b/core/src/utils/select-option-render.tsx
new file mode 100644
index 00000000000..ab99f4da81b
--- /dev/null
+++ b/core/src/utils/select-option-render.tsx
@@ -0,0 +1,171 @@
+import type { VNode } from '@stencil/core';
+import { h } from '@stencil/core';
+import { sanitizeDOMTree } from '@utils/sanitization';
+
+import type { RichContentOption as RichContentOpt } from '../components/select/select-interface';
+
+interface RichContentOption extends RichContentOpt {
+ /** Unique identifier for stable virtual DOM keys across re-renders. */
+ id: string;
+ /** The main label for the option as a string or an HTMLElement. */
+ label?: string | HTMLElement;
+}
+
+/**
+ * Converts a DOM node into a Stencil VNode (or text string) so the
+ * resulting tree is rendered through the component's normal render
+ * path. Rendering through Stencil ensures that scoped CSS classes
+ * (e.g. `sc-ion-action-sheet-ionic`) are applied to every element.
+ *
+ * Highly recommended to pre-sanitize the source DOM (see
+ * `getOptionContent` in select.tsx). This function performs pure
+ * structural conversion — no security filtering.
+ *
+ * Preserves attributes only — properties set imperatively on the source
+ * element (e.g. `input.value` after a user types) won't carry through.
+ * In practice this isn't a concern: interactive controls shouldn't
+ * appear in select-option rich content since they'd nest inside the
+ * overlay's button/radio/checkbox wrapper, which is invalid HTML and
+ * an accessibility issue.
+ *
+ * @param node - The DOM node to convert. Text nodes become strings,
+ * element nodes become VNodes, and any other node types are skipped.
+ * @param keyPrefix - String prefix used to build a stable VNode key,
+ * so Stencil's diff can preserve elements across re-renders.
+ * @param index - Position of this node among its siblings. Combined
+ * with `keyPrefix` to form the final unique key.
+ * @returns The converted VNode, a text string, or `null` if the node
+ * type isn't supported.
+ *
+ * @internal Exported only so it can be unit tested; not part of the
+ * public API.
+ */
+export const cloneToVNode = (node: Node, keyPrefix: string, index: number): VNode | string | null => {
+ if (node.nodeType === Node.TEXT_NODE) {
+ return node.textContent ?? '';
+ }
+
+ if (node.nodeType !== Node.ELEMENT_NODE) {
+ return null;
+ }
+
+ const el = node as Element;
+ const tag = el.tagName.toLowerCase();
+ const key = `${keyPrefix}-${index}`;
+
+ const attrs: Record = { key };
+ for (let i = 0; i < el.attributes.length; i++) {
+ const attr = el.attributes.item(i)!;
+ attrs[attr.name] = attr.value;
+ }
+
+ const children = Array.from(el.childNodes)
+ .map((child, i) => cloneToVNode(child, key, i))
+ .filter((c): c is VNode | string => c !== null);
+
+ return h(tag as any, attrs, children as any);
+};
+
+/**
+ * Renders cloned DOM content 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
+ * content to prevent accessibility issues.
+ *
+ * @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 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}`;
+
+ /**
+ * Do not remove. This is the only sanitization pass for callers that pass
+ * an `HTMLElement` straight to `renderOptionLabel` (e.g. vanilla JS)
+ * without going through `getOptionContent`, which sanitizes upstream.
+ * `cloneToVNode` does pure structural conversion and no security
+ * filtering, so dropping this call would reopen an XSS hole on the
+ * direct `HTMLElement` path.
+ */
+ sanitizeDOMTree(content);
+
+ return (
+
+ {Array.from(content.childNodes).map((child, i) => cloneToVNode(child, keyPrefix, i))}
+
+ );
+};
+
+/**
+ * Renders the label content for a select option within an overlay
+ * interface based on the presence of rich content.
+ *
+ * Span elements should be used when this content renders within buttons,
+ * depending on the select interface. Buttons can only have phrasing
+ * content to prevent accessibility issues.
+ *
+ * @param option - The content option data containing label, slots,
+ * and description.
+ * @param className - The base CSS class for the label element.
+ * @param useSpan - Whether to use a span element instead of a div for the label.
+ */
+export const renderOptionLabel = (
+ option: RichContentOption,
+ className: string,
+ useSpan = false
+): HTMLElement | string | undefined => {
+ const { id, label, startContent, endContent, description } = option;
+ const hasRichContent = !!startContent || !!endContent || !!description;
+ const Tag = useSpan ? 'span' : 'div';
+
+ // Render simple string label if there is no rich content to display
+ if (!hasRichContent && (typeof label === 'string' || !label)) {
+ return (
+
+ {label}
+
+ );
+ }
+
+ // Render the main label
+ const labelEl =
+ typeof label === 'string' || !label ? (
+ // Label is a simple string or undefined
+ {label}
+ ) : (
+ // Label is an HTMLElement with potential rich content
+ renderClonedContent(id, label, `${className}-text`, useSpan)
+ );
+
+ // No rich content, render just the label
+ if (!hasRichContent) {
+ return (
+
+ {labelEl}
+
+ );
+ }
+
+ // Render label with rich content (start, end, description)
+ return (
+
+ {startContent && renderClonedContent(id, startContent, 'select-option-start', useSpan)}
+
+ {labelEl}
+ {description && (
+
+ {description}
+
+ )}
+
+ {endContent && renderClonedContent(id, endContent, 'select-option-end', useSpan)}
+
+ );
+};
diff --git a/core/src/utils/test/select-option-render.spec.tsx b/core/src/utils/test/select-option-render.spec.tsx
new file mode 100644
index 00000000000..d6b1cb8dc07
--- /dev/null
+++ b/core/src/utils/test/select-option-render.spec.tsx
@@ -0,0 +1,170 @@
+import type { VNode } from '@stencil/core';
+
+import { cloneToVNode, renderOptionLabel } from '../select-option-render';
+
+/**
+ * `cloneToVNode` returns Stencil's internal VNode object, whose fields are
+ * name-mangled (`$tag$`, `$attrs$`, etc.). Casting to this shape keeps the
+ * assertions readable without depending on the public `VNode` type, which
+ * does not expose those runtime fields.
+ */
+interface RuntimeVNode {
+ $tag$: string | null;
+ $text$: string | null;
+ $attrs$: Record | null;
+ $children$: RuntimeVNode[] | null;
+ $key$: string | null;
+}
+
+const asVNode = (value: VNode | string | null): RuntimeVNode => value as unknown as RuntimeVNode;
+
+describe('cloneToVNode', () => {
+ describe('text nodes', () => {
+ it('should return the text content of a text node as a string', () => {
+ const node = document.createTextNode('hello world');
+
+ expect(cloneToVNode(node, 'prefix', 0)).toBe('hello world');
+ });
+
+ it('should return an empty string when text content is empty', () => {
+ const node = document.createTextNode('');
+
+ expect(cloneToVNode(node, 'prefix', 0)).toBe('');
+ });
+ });
+
+ describe('unsupported nodes', () => {
+ it('should return null for a comment node', () => {
+ const node = document.createComment('a comment');
+
+ expect(cloneToVNode(node, 'prefix', 0)).toBeNull();
+ });
+ });
+
+ describe('element nodes', () => {
+ it('should convert an element to a VNode with the lowercased tag name', () => {
+ const el = document.createElement('SPAN');
+
+ const vnode = asVNode(cloneToVNode(el, 'prefix', 0));
+
+ expect(vnode.$tag$).toBe('span');
+ });
+
+ it('should build a stable key from the prefix and index', () => {
+ const el = document.createElement('div');
+
+ const vnode = asVNode(cloneToVNode(el, 'prefix', 3));
+
+ expect(vnode.$key$).toBe('prefix-3');
+ expect(vnode.$attrs$?.key).toBe('prefix-3');
+ });
+
+ it('should copy all attributes from the source element', () => {
+ const el = document.createElement('span');
+ el.setAttribute('class', 'foo bar');
+ el.setAttribute('data-value', '42');
+ el.setAttribute('aria-label', 'label');
+
+ const vnode = asVNode(cloneToVNode(el, 'prefix', 0));
+
+ expect(vnode.$attrs$).toEqual({
+ key: 'prefix-0',
+ class: 'foo bar',
+ 'data-value': '42',
+ 'aria-label': 'label',
+ });
+ });
+
+ it('should recursively convert child element nodes', () => {
+ const el = document.createElement('div');
+ el.appendChild(document.createElement('span'));
+ el.appendChild(document.createElement('img'));
+
+ const vnode = asVNode(cloneToVNode(el, 'prefix', 0));
+
+ expect(vnode.$children$).toHaveLength(2);
+ expect(vnode.$children$?.[0].$tag$).toBe('span');
+ expect(vnode.$children$?.[1].$tag$).toBe('img');
+ });
+
+ it('should derive child keys from the parent key', () => {
+ const el = document.createElement('div');
+ el.appendChild(document.createElement('span'));
+
+ const vnode = asVNode(cloneToVNode(el, 'prefix', 2));
+
+ // Parent key is `prefix-2`, so the first child key is `prefix-2-0`
+ expect(vnode.$children$?.[0].$key$).toBe('prefix-2-0');
+ });
+
+ it('should preserve text child content', () => {
+ const el = document.createElement('span');
+ el.appendChild(document.createTextNode('inner text'));
+
+ const vnode = asVNode(cloneToVNode(el, 'prefix', 0));
+
+ // `cloneToVNode` returns the text as a string, which `h` wraps into a
+ // text VNode (`$tag$` null, content on `$text$`).
+ expect(vnode.$children$).toHaveLength(1);
+ expect(vnode.$children$?.[0].$tag$).toBeNull();
+ expect(vnode.$children$?.[0].$text$).toBe('inner text');
+ });
+
+ it('should filter out unsupported child nodes', () => {
+ const el = document.createElement('div');
+ el.appendChild(document.createElement('span'));
+ el.appendChild(document.createComment('skip me'));
+ el.appendChild(document.createTextNode('keep me'));
+
+ const vnode = asVNode(cloneToVNode(el, 'prefix', 0));
+
+ // The comment node is dropped, leaving the span and the text
+ expect(vnode.$children$).toHaveLength(2);
+ expect(vnode.$children$?.[0].$tag$).toBe('span');
+ expect(vnode.$children$?.[1].$text$).toBe('keep me');
+ });
+
+ it('should convert a deeply nested structure', () => {
+ const el = document.createElement('div');
+ const child = document.createElement('span');
+ const grandchild = document.createElement('strong');
+ grandchild.appendChild(document.createTextNode('deep'));
+ child.appendChild(grandchild);
+ el.appendChild(child);
+
+ const vnode = asVNode(cloneToVNode(el, 'prefix', 0));
+
+ const span = vnode.$children$?.[0];
+ const strong = span?.$children$?.[0];
+ expect(span?.$tag$).toBe('span');
+ expect(strong?.$tag$).toBe('strong');
+ expect(strong?.$children$?.[0].$text$).toBe('deep');
+ });
+
+ it('should produce no children for an empty element', () => {
+ const el = document.createElement('div');
+
+ const vnode = asVNode(cloneToVNode(el, 'prefix', 0));
+
+ // `h` normalizes an empty children array to `null`
+ expect(vnode.$children$).toBeNull();
+ });
+ });
+});
+
+describe('renderOptionLabel', () => {
+ it('should sanitize an HTMLElement label that bypassed ion-select', () => {
+ // Mirrors a vanilla JS caller passing DOM straight to an overlay, which
+ // never runs through `getOptionContent`'s `sanitizeDOMTree`.
+ const label = document.createElement('span');
+ label.innerHTML = 'hi ';
+
+ const result = asVNode(renderOptionLabel({ id: '1', label }, 'select-option') as unknown as VNode);
+
+ const anchor = result.$children$?.[0].$children$?.[0];
+ expect(anchor?.$tag$).toBe('a');
+ expect(anchor?.$attrs$?.href).toBeUndefined();
+ expect(anchor?.$attrs$?.onclick).toBeUndefined();
+ expect(anchor?.$attrs$?.class).toBe('safe');
+ });
+});
diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts
index 943c4de989f..66feafc701c 100644
--- a/packages/angular/src/directives/proxies.ts
+++ b/packages/angular/src/directives/proxies.ts
@@ -2365,14 +2365,14 @@ export declare interface IonSelectModal extends Components.IonSelectModal {}
@ProxyCmp({
- inputs: ['disabled', 'value']
+ inputs: ['description', 'disabled', 'justify', 'labelPlacement', 'mode', 'value']
})
@Component({
selector: 'ion-select-option',
changeDetection: ChangeDetectionStrategy.OnPush,
template: ' ',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
- inputs: ['disabled', 'value'],
+ inputs: ['description', 'disabled', 'justify', 'labelPlacement', 'mode', 'value'],
standalone: false
})
export class IonSelectOption {
diff --git a/packages/angular/standalone/src/directives/proxies.ts b/packages/angular/standalone/src/directives/proxies.ts
index 274560e5981..5507445158a 100644
--- a/packages/angular/standalone/src/directives/proxies.ts
+++ b/packages/angular/standalone/src/directives/proxies.ts
@@ -1917,14 +1917,14 @@ export declare interface IonSelectModal extends Components.IonSelectModal {}
@ProxyCmp({
defineCustomElementFn: defineIonSelectOption,
- inputs: ['disabled', 'value']
+ inputs: ['description', 'disabled', 'justify', 'labelPlacement', 'mode', 'value']
})
@Component({
selector: 'ion-select-option',
changeDetection: ChangeDetectionStrategy.OnPush,
template: ' ',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
- inputs: ['disabled', 'value'],
+ inputs: ['description', 'disabled', 'justify', 'labelPlacement', 'mode', 'value'],
})
export class IonSelectOption {
protected el: HTMLIonSelectOptionElement;
diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts
index cd24c2c6e0a..f62b14267a7 100644
--- a/packages/vue/src/proxies.ts
+++ b/packages/vue/src/proxies.ts
@@ -961,7 +961,10 @@ export const IonSelectModal: StencilVueComponent = /*@__PURE
export const IonSelectOption: StencilVueComponent = /*@__PURE__*/ defineContainer('ion-select-option', defineIonSelectOption, [
'disabled',
- 'value'
+ 'value',
+ 'description',
+ 'labelPlacement',
+ 'justify'
]);