diff --git a/BREAKING.md b/BREAKING.md index 8715719907a..1732c39d08e 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -229,6 +229,15 @@ When using `interface="action-sheet"`, `ion-select` no longer assigns the `selec Previously, the `selected` role was assigned only to the option matching the select's current value. Because the dismiss role mirrors the tapped button, this surfaced in just one case: re-selecting the already-selected option dismissed the action sheet with `role: "selected"` in `ionActionSheetDidDismiss`. Tapping any other option changed the value and dismissed with `role: ""`. Now that the role is no longer assigned, both cases dismiss with `role: undefined`. Apps that inspected this role to detect that a value was chosen, such as reading `role` from the underlying action sheet's `onDidDismiss` result, should listen for `ion-select`'s `ionChange` event instead, which emits the selected value when the selection changes. +**Internal DOM Structure Changes** + +The component's internal DOM structure has been restructured to support floating labels with slotted start and end content. Additionally, the structure of the component has been reorganized, with some elements now grouped differently than before. The `inner` wrapper element has been removed, and its content has been split across separate wrapper elements for the start slot, control, and end slot. This may introduce breaking changes for developers who rely on the component's internal DOM structure or apply custom styling to internal elements. + +Developers who previously styled `ion-select::part(inner)` should migrate to targeting the updated component structure using the following CSS parts instead: +- `ion-select::part(start)` - Target the start slot wrapper +- `ion-select::part(control)` - Target the control wrapper containing the label and native select. When the label is not floating or stacked, this part also contains the dropdown icon. +- `ion-select::part(end)` - Target the end slot wrapper. When the label is floating or stacked, this part also contains the dropdown icon. +

Framework Specific

Angular

diff --git a/core/api.txt b/core/api.txt index 17361852da1..5a79c3903aa 100644 --- a/core/api.txt +++ b/core/api.txt @@ -1758,12 +1758,14 @@ 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,control +ion-select,part,end ion-select,part,error-text ion-select,part,helper-text ion-select,part,icon -ion-select,part,inner ion-select,part,label ion-select,part,placeholder +ion-select,part,start ion-select,part,supporting-text ion-select,part,text ion-select,part,wrapper diff --git a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-ltr-Mobile-Chrome-linux.png index c83b2553e29..31412cdac0a 100644 Binary files a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-ltr-Mobile-Firefox-linux.png index fae3f804e7c..4c89bb4c4e8 100644 Binary files a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-ltr-Mobile-Safari-linux.png b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-ltr-Mobile-Safari-linux.png index 014f9df08d8..0c8f43adb3d 100644 Binary files a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-rtl-Mobile-Chrome-linux.png index 7ad6ab8de1c..1b7ace9824e 100644 Binary files a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-rtl-Mobile-Chrome-linux.png and b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-rtl-Mobile-Firefox-linux.png index d310360e579..e82f6d4ff06 100644 Binary files a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-rtl-Mobile-Firefox-linux.png and b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-rtl-Mobile-Safari-linux.png b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-rtl-Mobile-Safari-linux.png index 42ed59625a9..20dd9fc48b1 100644 Binary files a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-rtl-Mobile-Safari-linux.png and b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-ios-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-ltr-Mobile-Chrome-linux.png b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-ltr-Mobile-Chrome-linux.png index deb26c17eee..9d8fccc2723 100644 Binary files a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-ltr-Mobile-Firefox-linux.png b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-ltr-Mobile-Firefox-linux.png index e97bc4cd58f..8766cb4f88b 100644 Binary files a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-ltr-Mobile-Safari-linux.png b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-ltr-Mobile-Safari-linux.png index af3aa06548b..325ea391a4a 100644 Binary files a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-ltr-Mobile-Safari-linux.png and b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-rtl-Mobile-Chrome-linux.png b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-rtl-Mobile-Chrome-linux.png index fbe205946b8..fd9545cabfa 100644 Binary files a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-rtl-Mobile-Firefox-linux.png b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-rtl-Mobile-Firefox-linux.png index 8d4ad7c0fd1..0bc53dc0b4f 100644 Binary files a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-rtl-Mobile-Safari-linux.png b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-rtl-Mobile-Safari-linux.png index 66a9bf7715c..6db8ab3f9e9 100644 Binary files a/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-rtl-Mobile-Safari-linux.png and b/core/src/components/item/test/alignment/item.e2e.ts-snapshots/item-alignment-diff-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/select.ios.scss b/core/src/components/select/select.ios.scss index 5b5e2f913ee..0f50b148af6 100644 --- a/core/src/components/select/select.ios.scss +++ b/core/src/components/select/select.ios.scss @@ -19,14 +19,6 @@ color: #{$text-color-step-350}; } -// Select Inner Wrapper -// ---------------------------------------------------------------- - -:host(.select-label-placement-stacked) .select-wrapper-inner, -:host(.select-label-placement-floating) .select-wrapper-inner { - width: calc(100% - $select-ios-icon-size - $select-icon-margin-start); -} - // Select: Disabled // ---------------------------------------------------------------- // The select and label should use the diff --git a/core/src/components/select/select.md.outline.scss b/core/src/components/select/select.md.outline.scss index ca801e265bb..d7b720f7f86 100644 --- a/core/src/components/select/select.md.outline.scss +++ b/core/src/components/select/select.md.outline.scss @@ -8,6 +8,7 @@ --border-radius: 4px; --padding-start: 16px; --padding-end: 16px; + --start-slot-adjustment: 0px; min-height: 56px; } @@ -103,7 +104,7 @@ * This makes the label sit above the select. */ :host(.label-floating.select-fill-outline) .label-text-wrapper { - @include transform(translateY(-32%), scale(#{$form-control-label-stacked-scale})); + @include transform(translate(var(--start-slot-adjustment), -32%), scale(#{$form-control-label-stacked-scale})); @include margin(0); /** @@ -197,13 +198,8 @@ :host(.select-fill-outline) .select-outline-start { @include border(null, null, null, var(--border-width) var(--border-style) var(--border-color)); -} - -:host(.select-fill-outline) .select-outline-start { @include border-radius(var(--border-radius), 0px, 0px, var(--border-radius)); -} -:host(.select-fill-outline) .select-outline-start { /** * There should be spacing between the translated text * and .select-outline-start. However, we can't add this @@ -217,13 +213,8 @@ :host(.select-fill-outline) .select-outline-end { @include border(null, var(--border-width) var(--border-style) var(--border-color), null, null); -} - -:host(.select-fill-outline) .select-outline-end { @include border-radius(0px, var(--border-radius), var(--border-radius), 0px); -} -:host(.select-fill-outline) .select-outline-end { /** * The ending outline fragment * should take up the remaining free space. diff --git a/core/src/components/select/select.md.scss b/core/src/components/select/select.md.scss index 220c263c7f5..fb194b464e9 100644 --- a/core/src/components/select/select.md.scss +++ b/core/src/components/select/select.md.scss @@ -118,14 +118,6 @@ --border-radius: 16px; } -// Select Inner Wrapper -// ---------------------------------------------------------------- - -:host(.select-label-placement-stacked) .select-wrapper-inner, -:host(.select-label-placement-floating) .select-wrapper-inner { - width: calc(100% - $select-md-icon-size - $select-icon-margin-start); -} - // Select: Disabled // ---------------------------------------------------------------- // The select and label should use the diff --git a/core/src/components/select/select.scss b/core/src/components/select/select.scss index 98e9364271e..c3485351b0b 100644 --- a/core/src/components/select/select.scss +++ b/core/src/components/select/select.scss @@ -154,36 +154,6 @@ button { color: inherit; } -/** - * The select icon should be centered with - * the entire container not just the control - * with floating/stacked labels. - */ -:host(.select-label-placement-stacked) .select-icon, -:host(.select-label-placement-floating) .select-icon { - position: absolute; - - height: 100%; -} - -/** - * This positions the icon at the correct - * edge of the component with LTR and RTL - * text directions. The position mixin cannot be - * used here because the icon is in the Shadow DOM. - */ -:host(.select-ltr.select-label-placement-stacked) .select-icon, -:host(.select-ltr.select-label-placement-floating) .select-icon { - // stylelint-disable-next-line property-disallowed-list - right: var(--padding-end, 0); -} - -:host(.select-rtl.select-label-placement-stacked) .select-icon, -:host(.select-rtl.select-label-placement-floating) .select-icon { - // stylelint-disable-next-line property-disallowed-list - left: var(--padding-start, 0); -} - // Select Text // -------------------------------------------------- .select-text { @@ -237,7 +207,7 @@ button { flex-grow: 1; - align-items: center; + align-items: stretch; justify-content: space-between; height: inherit; @@ -270,23 +240,28 @@ button { transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1); } -.select-wrapper-inner { +// The control must inherit specific flex properties +// so that it will be placed properly when justify +// or label-placement are set on the host +.select-control { display: flex; - align-items: center; + position: relative; - overflow: hidden; -} + flex: 1; + + flex-direction: inherit; + + align-items: center; + justify-content: inherit; -:host(.select-label-placement-stacked) .select-wrapper-inner, -:host(.select-label-placement-floating) .select-wrapper-inner { /** - * When using a stacked/floating label, the inner wrapper is - * stacked vertically under the label container. This line - * ensures that the inner wrapper fills all the remaining height - * of the component. + * The min-width is set to 0 to allow flex items to shrink below + * their content size. This enables the select text to collapse + * in width. We avoid overflow: hidden here because it will clip + * the floating label. */ - flex-grow: 1; + min-width: 0; } // Select Highlight @@ -554,9 +529,18 @@ button { * down when the select is focused or has a value. * */ -:host(.select-label-placement-stacked) .select-wrapper, -:host(.select-label-placement-floating) .select-wrapper { +:host(.select-label-placement-stacked) .select-control, +:host(.select-label-placement-floating) .select-control { flex-direction: column; + + /** + * When using a stacked/floating label, the inner wrapper is + * stacked vertically under the label container. This line + * ensures that the inner wrapper fills all the remaining height + * of the component. + */ + flex-grow: 1; + align-items: start; } @@ -583,15 +567,11 @@ button { } /** - * Ensures the select does not - * overlap the label. - * Also ensure that the native wrapper - * takes up the remaining available height and width. + * Ensures that the native wrapper takes up the + * remaining available height and width. */ :host(.select-label-placement-stacked) .native-wrapper, :host(.select-label-placement-floating) .native-wrapper { - @include margin(1px, 0, 0, 0); - flex-grow: 1; width: 100%; @@ -616,27 +596,16 @@ button { * The placeholder should be hidden when the label * is on top of the select. This prevents the label * from overlapping any placeholder value. - * - * TODO(FW-5592): Remove :not(.label-floating) piece */ -:host(.select-label-placement-floating:not(.label-floating)) .native-wrapper .select-placeholder { +:host(.select-label-placement-floating) .native-wrapper .select-placeholder { opacity: 0; } /** - * We don't use .label-floating here because that would - * also include the case where the label is floating due - * to content in the start/end slot. We want the opacity - * to remain at the default in this case, since the select - * isn't being actively interacted with. - * - * TODO(FW-5592): Change entire selector to: - * :host(.label-floating.select-label-placement-floating) .native-wrapper .select-placeholder + * Show the placeholder when the label is floating. */ -:host(.select-expanded.select-label-placement-floating) .native-wrapper .select-placeholder, -:host(.has-focus.select-label-placement-floating) .native-wrapper .select-placeholder, -:host(.has-value.select-label-placement-floating) .native-wrapper .select-placeholder { - opacity: 1; +:host(.label-floating.select-label-placement-floating) .native-wrapper .select-placeholder { + opacity: var(--placeholder-opacity); } /** @@ -655,6 +624,17 @@ button { // Start/End Slots // ---------------------------------------------------------------- +.select-start, +.select-end { + display: flex; + + position: relative; + + flex-shrink: 0; + + align-items: center; +} + ::slotted([slot="start"]), ::slotted([slot="end"]) { /** * Prevent the slots from shrinking when the label and/or diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index 8941f0d35d9..6222ebd2fbb 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -54,7 +54,9 @@ import type { * @part error-text - Supporting text displayed beneath the select when the select is invalid and touched. * @part bottom - The container element for helper text, error text, and counter. * @part wrapper - The clickable label element that wraps the entire form field (label text, slots, selected values or placeholder, and toggle icons). - * @part inner - The inner element of the wrapper that manages the slots, selected values or placeholder, and toggle icons. + * @part start - The wrapper element for the content in the start slot. + * @part control - The wrapper element containing the label and native select control. When the label is not floating or stacked, this part also contains the dropdown icon. + * @part end - The wrapper element for the content in the end slot. When the label is floating or stacked, this part also contains the dropdown icon. */ @Component({ tag: 'ion-select', @@ -1043,57 +1045,37 @@ export class Select implements ComponentInterface { } /** - * Stops propagation when the label is clicked, - * otherwise, two clicks will be triggered. + * Stops propagation when the label is clicked, otherwise, + * two clicks will be triggered. Allows clicks on the native + * wrapper to propagate so the select can open. */ private onLabelClick = (ev: MouseEvent) => { - // Only stop propagation if the click was directly on the label - // and not on the input or other child elements - if (ev.target === ev.currentTarget) { + const target = ev.target as HTMLElement; + const nativeWrapper = this.el.shadowRoot?.querySelector('.native-wrapper'); + + if (!nativeWrapper?.contains(target)) { ev.stopPropagation(); } }; /** - * Renders the border container - * when fill="outline". + * Renders the outline border with a notch for the label. */ - private renderLabelContainer() { - const mode = getIonMode(this); - const hasOutlineFill = mode === 'md' && this.fill === 'outline'; - - if (hasOutlineFill) { - /** - * The outline fill has a special outline - * that appears around the select and the label. - * Certain stacked and floating label placements cause the - * label to translate up and create a "cut out" - * inside of that border by using the notch-spacer element. - */ - return [ -
-
-
- -
-
-
, - this.renderLabel(), - ]; - } - - /** - * If not using the outline style, - * we can render just the label. - */ - return this.renderLabel(); + private renderOutlineDecorations() { + return [ +
, +
+ +
, +
, + ]; } /** @@ -1306,6 +1288,19 @@ export class Select implements ComponentInterface { ); } + private getStartSlotAdjustment(): string { + const startSlot = this.el.shadowRoot?.querySelector('.select-start') as HTMLElement | null; + if (!startSlot || !Build.isBrowser || getIonMode(this) !== 'md' || this.fill !== 'outline') { + return ''; + } + + const startSlotWidth = startSlot.getBoundingClientRect().width; + const roundedWidth = Math.round(startSlotWidth * 10) / 10; + const isRTL = document.dir === 'rtl'; + const sign = isRTL ? '' : '-'; + return roundedWidth ? `${sign}${roundedWidth}px` : '0px'; + } + render() { const { disabled, @@ -1329,30 +1324,12 @@ export class Select implements ComponentInterface { const shouldRenderHighlight = mode === 'md' && fill !== 'outline' && !inItem; const hasValue = this.hasValue(); - const hasStartEndSlots = el.querySelector('[slot="start"], [slot="end"]') !== null; + const hasOutlineFill = mode === 'md' && fill === 'outline'; renderHiddenInput(true, el, name, parseValue(value), disabled); - /** - * If the label is stacked, it should always sit above the select. - * For floating labels, the label should move above the select if - * the select has a value, is open, or has anything in either - * the start or end slot. - * - * If there is content in the start slot, the label would overlap - * it if not forced to float. This is also applied to the end slot - * because with the default or solid fills, the select is not - * vertically centered in the container, but the label is. This - * causes the slots and label to appear vertically offset from each - * other when the label isn't floating above the input. This doesn't - * apply to the outline fill, but this was not accounted for to keep - * things consistent. - * - * TODO(FW-5592): Remove hasStartEndSlots condition - */ const labelShouldFloat = - labelPlacement === 'stacked' || - (labelPlacement === 'floating' && (hasValue || hasFocus || isExpanded || hasStartEndSlots)); + labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || isExpanded)); return (