Skip to content

fix(select): support floating labels with slotted content - #31326

Open
brandyscarney wants to merge 32 commits into
major-9.0from
FW-6471-select
Open

fix(select): support floating labels with slotted content#31326
brandyscarney wants to merge 32 commits into
major-9.0from
FW-6471-select

Conversation

@brandyscarney

@brandyscarney brandyscarney commented Aug 4, 2026

Copy link
Copy Markdown
Member

Issue number: resolves #30402


What is the current behavior?

Selects with a floating label and a start or end slot always display the label in the floated state, regardless of whether the select contains a value:

without value with value
without-value with-value

What is the new behavior?

  • The floating label now behaves consistently regardless of whether start or end slots are present:
    • It overlays the select when the field is empty.
    • It floats when the select is expanded or contains a value.
    • It displays the placeholder when it is focused or expanded but does not contain a value.
  • Updates the placeholder opacity when floating so it will use the correct value of --placeholder-opacity instead of 1, matching the other select label placements.
  • Start slot content is now always positioned to the left of both the label and the select.
  • Additional screenshot tests have been added to verify these behavior and layout changes.
  • A follow-up ticket has been created to address the remaining UI differences between our leading/trailing content implementation and the md specification.

Does this introduce a breaking change?

  • Yes
  • No

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.

Other information

Preview

@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ionic-framework Ready Ready Preview Aug 7, 2026 9:44pm

Request Review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed these screenshots from select-slots to select-slot to match the folder name.


configs().forEach(({ title, screenshot, config }) => {
test.describe(title('select: start and end slots (visual checks)'), () => {
test.describe(title('select: slot'), () => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was updated to match the folder name, following how we title other tests.

test('should not have visual regressions with a floating label when expanded', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/30402',

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this bug and fixed it while I was cleaning up the styles so I added a test for it here.

@brandyscarney brandyscarney Aug 5, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After updating all of the screenshots I found that this is technically covered by this one: https://github.com/ionic-team/ionic-framework/pull/31326/changes?#diff-5a03488c7650116b1b96323c5b1ec6fb3d652e44fd7c78d9a162a22076fcafe2

I could remove this test if desired and add the issue number on that test.

test('should not have visual regressions with a floating label when expanded', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/30402',

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this bug and fixed it while I was cleaning up the styles so I added a test for it here.

}

/* Hide the backdrop for action sheets */
ion-action-sheet.select-action-sheet ion-backdrop {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed all of the selects here to use an action sheet with a clear backdrop so that you could see the states better when interacting with them.

@brandyscarney
brandyscarney marked this pull request as ready for review August 7, 2026 14:35
@brandyscarney
brandyscarney requested a review from a team as a code owner August 7, 2026 14:35
@brandyscarney
brandyscarney requested a review from thetaPC August 7, 2026 14:35

@ShaneK ShaneK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking really great! I noticed a few issues and a few nits, hopefully I explained things well enough that it's not too bad to fix up!

Comment thread core/src/components/select/select.tsx Outdated
const target = ev.target as HTMLElement;
const nativeWrapper = this.el.shadowRoot?.querySelector('.native-wrapper');

if (!nativeWrapper?.contains(target)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clicking a slotted ion-button still fires its own handler, but a listener on document gets nothing. On major-9.0 both fire once.

Slotted nodes are never inside .native-wrapper, so they always take the stopPropagation() branch. That's the case onClick avoids stopping, so React's onClick on slotted content stops working.

@brandyscarney brandyscarney Aug 7, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Input fix: 9b59417

Textarea fix: 58708ca

Select fix: b5a1d65

await expect(select).not.toHaveClass(/select-expanded/);
await expect(select).toHaveScreenshot(screenshot(`select-slot-label-floating-value`));
});
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could should not open select when slotted buttons are clicked come back? It's the only coverage for onLabelClick, and this PR rewrites that handler.

Comment on lines +1329 to +1330
const isRTL = document.dir === 'rtl';
const sign = isRTL ? '' : '-';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const isRTL = document.dir === 'rtl';
const sign = isRTL ? '' : '-';
const sign = isRTL(this.el) ? '' : '-';

Using document.dir here misses a dir set on the host. With dir="rtl" on the select in an LTR page it renders RTL but the offset comes out -48px instead of 48px, so the label lands about 96px off its notch.

select-rtl-host-dir

return '';
}

const startSlotWidth = startSlot.getBoundingClientRect().width;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The offset only refreshes when the slot mutation fires, so a start slot that changes size without a DOM change keeps the old value. Widening slotted content with CSS leaves the real width at 176px and the offset still at -36.4px, which leaves the label mid-field.

A ResizeObserver on .select-start would cover it. Might be worth a short doc comment on this method too, it's doing a bit more than the name suggests.

select-stale-start-slot

Comment thread core/src/components/select/select.tsx Outdated
*/
this.skipLabelTransition = true;

requestAnimationFrame(() => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't cancelled on disconnect, so it can set state after the component is gone. Storing the handle and clearing it next to the slot controller would fix it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Input fix: 3fcf658

Textarea fix: 6278723

Select fix: 9a7b423

Comment thread core/src/components/select/select.scss
Comment thread core/src/components/select/select.scss Outdated

configs().forEach(({ title, screenshot, config }) => {
test.describe(title('select: start and end slots (visual checks)'), () => {
test.describe(title('select: slot'), () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new screenshots cover start and floating but not stacked, which goes through the same restructure. Worth adding here, or a follow-up card if you'd rather keep this one tight.

Comment thread core/src/components/select/test/slot/index.html Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: core @ionic/core package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants