diff --git a/BREAKING.md b/BREAKING.md index 8715719907a..0fda8c7579b 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -229,6 +229,39 @@ 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. +**Floating Label Behavior** + +Floating labels no longer automatically float when the select contains slotted content. Labels float only when the select is focused or has a value. Additionally, when using a floating label, the placeholder is only visible when the select is focused. + +**Internal DOM Structure Changes** + +The internal DOM structure has been reorganized to support floating labels with slotted content. This changes the structure and location of several exposed shadow parts. + +Added: +- `.select-start` — `part="start"` +- `.select-control` — `part="control"` +- `.select-end` — `part="end"` + +Removed: +- `.select-wrapper-inner` — `part="inner"` + +Restructured: +- `.label-text-wrapper` remains `part="label"` but moved from `.select-wrapper` into `.select-control` +- `.native-wrapper` remains `part="container"` but moved from `.select-wrapper-inner` into `.select-control` +- Start slot moved from `.select-wrapper-inner` into `.select-start` (`part="start"`) +- End slot moved from `.select-wrapper-inner` into `.select-end` (`part="end"`) +- `.select-icon` remains `part="icon"` but its location depends on the label state: + - With a start/end label, the icon is inside `.select-control` + - With a floating/stacked label, the icon is inside `.select-end` + +Update selectors that target the exposed shadow parts to account for the new structure: + +If you currently target `part="inner"`, that part has been removed. Update those styles to target the new parts as appropriate. + +If you target `part="label"`, `part="container"`, or `part="icon"`, the part names remain unchanged, but their position in the shadow DOM has changed. This may affect styles that depend on the relationship or layout of these parts. + +Use the new `part="start"`, `part="control"`, and `part="end"` parts to target the new structural wrappers. +

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-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 index 6070403f4a4..b5a0393cd61 100644 Binary files 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 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 index 2a48eadd256..4dfecb1acac 100644 Binary files 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 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-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 index 8a8e68c7521..a7cb6e001c6 100644 Binary files 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 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 index 3cf41fcf865..79ab75f3422 100644 Binary files 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 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-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 index 46cfe39151f..6b038fc70d6 100644 Binary files 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 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 index e14857880ae..456a9a150eb 100644 Binary files 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 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-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 index 27763acc007..a52875e8075 100644 Binary files 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 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 index 0327ee6b7e7..c45fcfb9279 100644 Binary files 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 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-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 index b0db37f939f..1a4dbccfede 100644 Binary files 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 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 index dd2424d3c81..0beb2aef0ac 100644 Binary files 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 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-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 index daab103e13c..0c2d0873b1a 100644 Binary files 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 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 index a1fecfdc9da..6bd6a65a1bb 100644 Binary files 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 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-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 index 6bcf0597a7b..0dafd48fb8d 100644 Binary files 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 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 index 06c7beebf6a..b1f1b3029b2 100644 Binary files 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 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-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 index 28e4b16fc74..f72bf051bc6 100644 Binary files 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 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 index 736785f7f5d..63e140d28d1 100644 Binary files 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 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-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 index 8630aae6dc3..75eff4f6a22 100644 Binary files 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 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-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 index 1e127cf49ec..ec71371a7ec 100644 Binary files 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 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-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 index 9f5229da38b..15e1b07b4bd 100644 Binary files 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 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-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 index 3d8d71847f9..c0e695f3981 100644 Binary files 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 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/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..1567d425704 100644 --- a/core/src/components/select/select.md.outline.scss +++ b/core/src/components/select/select.md.outline.scss @@ -9,6 +9,15 @@ --padding-start: 16px; --padding-end: 16px; + /** + * The measured width of the start container, as a negative offset + * in LTR and a positive one in RTL. Written to the host by the + * start container controller, so renaming it here requires renaming + * it there too. Declared with a default so the label is positioned + * correctly before the first measurement runs. + */ + --internal-start-container-adjustment: 0px; + min-height: 56px; } @@ -82,10 +91,11 @@ position: absolute; /** - * Label text should not extend - * beyond the bounds of the select. + * While resting this is the width of the select, which already + * excludes the start and end slots. Once the label floats the + * containing block becomes the wrapper and the rule below takes over. */ - max-width: calc(100% - var(--padding-start) - var(--padding-end)); + max-width: 100%; } /** @@ -99,11 +109,20 @@ z-index: 1; } +/** + * The resting label is positioned relative to the control so it can size to + * the space between the start and end slots. When floating, the label needs + * to escape the control and position itself relative to the wrapper instead. + */ +:host(.select-fill-outline:not(.label-floating)) .select-control { + position: relative; +} + /** * 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(--internal-start-container-adjustment, 0px), -32%), scale(#{$form-control-label-stacked-scale})); @include margin(0); /** @@ -197,13 +216,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 +231,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..38614c46804 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; + flex: 1; - overflow: hidden; -} + 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 @@ -390,6 +365,13 @@ button { align-items: center; + /** + * Allows the label to shrink below its intrinsic content width. + * Without this the label keeps its full width and can push the + * select and end slot outside the wrapper. + */ + min-width: 0; + /** * Label text should not extend * beyond the bounds of the select. @@ -410,6 +392,18 @@ button { pointer-events: none; } +/** + * When start/end slots are added or removed, we temporarily + * disable the label transition to prevent the floating label + * from animating as its position is adjusted. + * + * This class is applied by the start container controller, so + * renaming it here requires renaming it there too. + */ +:host(.skip-label-transition) .label-text-wrapper { + transition: none; +} + /** * We need to use two elements instead of * one. The .label-text-wrapper is responsible @@ -448,6 +442,14 @@ button { .native-wrapper { display: flex; + /** + * Give up space before the label does. Flexbox would otherwise take + * from both in proportion to their size and truncate the shorter + * label first. The factor is this large because the label truncates + * as soon as it loses any width at all, even a fraction of a pixel. + */ + flex-shrink: 100000; + align-items: center; /** @@ -554,9 +556,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 control is + * stacked vertically under the label container. This line + * ensures that the control fills all the remaining height + * of the component. + */ + flex-grow: 1; + align-items: start; } @@ -583,15 +594,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 +623,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 +651,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..c870291ec4c 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -1,8 +1,14 @@ 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 type { NotchController, StartContainerController } from '@utils/forms'; +import { + compareOptions, + createNotchController, + createStartContainerController, + isOptionSelected, + checkInvalidState, +} from '@utils/forms'; import { focusVisibleElement, renderHiddenInput, inheritAttributes } from '@utils/helpers'; import type { Attributes } from '@utils/helpers'; import { printIonWarning } from '@utils/logging'; @@ -10,6 +16,8 @@ import { actionSheetController, alertController, popoverController, modalControl import type { OverlaySelect } from '@utils/overlays-interface'; import { isRTL } from '@utils/rtl'; import { reflectPropertiesToAttributes, sanitizeDOMTree } from '@utils/sanitization'; +import { createSlotMutationController } from '@utils/slot-mutation-controller'; +import type { SlotMutationController } from '@utils/slot-mutation-controller'; import { createColorClasses, hostContext } from '@utils/theme'; import { watchForOptions } from '@utils/watch-options'; import { caretDownSharp, chevronExpand } from 'ionicons/icons'; @@ -54,7 +62,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', @@ -72,10 +82,13 @@ export class Select implements ComponentInterface { private focusEl?: HTMLButtonElement; private mutationO?: MutationObserver; private inheritedAttributes: Attributes = {}; + private slotMutationController?: SlotMutationController; private nativeWrapperEl: HTMLElement | undefined; private notchSpacerEl: HTMLElement | undefined; private validationObserver?: MutationObserver; private notchController?: NotchController; + private startContainerController?: StartContainerController; + private startContainerEl: HTMLElement | undefined; private customHTMLEnabled = config.get('innerHTMLTemplatesEnabled', ENABLE_HTML_CONTENT_DEFAULT); @Element() el!: HTMLIonSelectElement; @@ -329,12 +342,28 @@ export class Select implements ComponentInterface { async connectedCallback() { const { el } = this; + this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => { + this.startContainerController?.calculateStartContainerWidth(); + + forceUpdate(this); + }); + this.notchController = createNotchController( el, () => this.notchSpacerEl, () => this.labelSlot ); + this.startContainerController = createStartContainerController( + el, + () => this.startContainerEl, + () => { + return Build.isBrowser && getIonMode(this) === 'md' && this.fill === 'outline'; + } + ); + + this.startContainerController.calculateStartContainerWidth(); + this.updateOverlayOptions(); this.emitStyle(); @@ -416,11 +445,21 @@ export class Select implements ComponentInterface { this.mutationO = undefined; } + if (this.slotMutationController) { + this.slotMutationController.destroy(); + this.slotMutationController = undefined; + } + if (this.notchController) { this.notchController.destroy(); this.notchController = undefined; } + if (this.startContainerController) { + this.startContainerController.destroy(); + this.startContainerController = undefined; + } + // Clean up validation observer to prevent memory leaks. if (this.validationObserver) { this.validationObserver.disconnect(); @@ -1022,6 +1061,7 @@ export class Select implements ComponentInterface { componentDidRender() { this.notchController?.calculateNotchWidth(); + this.startContainerController?.calculateStartContainerWidth(); } /** @@ -1043,57 +1083,37 @@ export class Select implements ComponentInterface { } /** - * Stops propagation when the label is clicked, - * otherwise, two clicks will be triggered. + * Stops propagation for re-dispatched clicks (when already expanded) to + * prevent double-click events. Allows initial button clicks (from keyboard + * or mouse) and slotted content clicks to propagate so event delegation + * works for parent handlers. */ 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) { + if (ev.target === this.focusEl && this.isExpanded) { 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 [ -
-
-
- + private renderOutlineContainer() { + return ( +
+
+
+ -
-
, - this.renderLabel(), - ]; - } - - /** - * If not using the outline style, - * we can render just the label. - */ - return this.renderLabel(); +
+
+
+ ); } /** @@ -1329,30 +1349,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 (