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 [
-
-
-
-
(this.notchSpacerEl = el)}>
- {this.label}
-
+ private renderOutlineContainer() {
+ return (
+
+
+
+
(this.notchSpacerEl = el)}>
+ {this.label}
-
-
,
- 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 (