diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index 819ba2e5ad9..a461ef1003c 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -2,7 +2,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Component, Element, Event, Host, Method, Prop, State, Watch, h, forceUpdate } from '@stencil/core'; import type { LegacyFormController } from '@utils/forms'; import { createLegacyFormController } from '@utils/forms'; -import { findItemLabel, focusElement, getAriaLabel, renderHiddenInput, inheritAttributes } from '@utils/helpers'; +import { findItemLabel, focusElement, getAriaLabel, renderHiddenInput, inheritAttributes, raf } from '@utils/helpers'; import type { Attributes } from '@utils/helpers'; import { printIonWarning } from '@utils/logging'; import { actionSheetController, alertController, popoverController } from '@utils/overlays'; @@ -10,6 +10,7 @@ import type { OverlaySelect } from '@utils/overlays-interface'; import { isRTL } from '@utils/rtl'; import { createColorClasses, hostContext } from '@utils/theme'; import { watchForOptions } from '@utils/watch-options'; +import { win } from '@utils/window'; import { caretDownSharp, chevronExpand } from 'ionicons/icons'; import { getIonMode } from '../../global/ionic-global'; @@ -55,6 +56,8 @@ export class Select implements ComponentInterface { private legacyFormController!: LegacyFormController; private inheritedAttributes: Attributes = {}; private nativeWrapperEl: HTMLElement | undefined; + private notchSpacerEl: HTMLElement | undefined; + private notchVisibilityIO: IntersectionObserver | undefined; // This flag ensures we log the deprecation warning at most once. private hasLoggedDeprecationWarning = false; @@ -665,13 +668,13 @@ export class Select implements ComponentInterface { * was passed. */ private get labelText() { - const { el, label } = this; + const { label } = this; if (label !== undefined) { return label; } - const labelSlot = el.querySelector('[slot="label"]'); + const { labelSlot } = this; if (labelSlot !== null) { return labelSlot.textContent; @@ -740,6 +743,28 @@ export class Select implements ComponentInterface { ); } + componentDidRender() { + if (this.needsExplicitNotchWidth()) { + /** + * Run this the frame after + * the browser has re-painted the select. + * Otherwise, the label element may have a width + * of 0 and the IntersectionObserver will be used. + */ + raf(() => { + this.setNotchWidth(); + }); + } + } + + /** + * Gets any content passed into the `label` slot, + * not the definition. + */ + private get labelSlot() { + return this.el.querySelector('[slot="label"]'); + } + /** * Returns `true` if label content is provided * either by a prop or a content. If you want @@ -747,7 +772,126 @@ export class Select implements ComponentInterface { * the `labelText` getter instead. */ private get hasLabel() { - return this.label !== undefined || this.el.querySelector('[slot="label"]') !== null; + return this.label !== undefined || this.labelSlot !== null; + } + + private needsExplicitNotchWidth() { + if ( + /** + * If the notch is not being used + * then we do not need to set the notch width. + */ + this.notchSpacerEl === undefined || + /** + * If no label is being used, then we + * do not need to estimate the notch width. + */ + !this.hasLabel || + /** + * If the label property is being used + * then we can render the label text inside + * of the notch and let the browser + * determine the notch size for us. + */ + this.label !== undefined + ) { + return false; + } + + return true; + } + + /** + * When using a label prop we can render + * the label value inside of the notch and + * let the browser calculate the size of the notch. + * However, we cannot render the label slot in multiple + * places so we need to manually calculate the notch dimension + * based on the size of the slotted content. + * + * This function should only be used to set the notch width + * on slotted label content. The notch width for label prop + * content is automatically calculated based on the + * intrinsic size of the label text. + */ + private setNotchWidth() { + const { el, notchSpacerEl } = this; + + if (notchSpacerEl === undefined) { + return; + } + + if (!this.needsExplicitNotchWidth()) { + notchSpacerEl.style.removeProperty('width'); + return; + } + + const width = this.labelSlot!.scrollWidth; + if ( + /** + * If the computed width of the label is 0 + * and notchSpacerEl's offsetParent is null + * then that means the element is hidden. + * As a result, we need to wait for the element + * to become visible before setting the notch width. + * + * We do not check el.offsetParent because + * that can be null if ion-select has + * position: fixed applied to it. + * notchSpacerEl does not have position: fixed. + */ + width === 0 && + notchSpacerEl.offsetParent === null && + win !== undefined && + 'IntersectionObserver' in win + ) { + /** + * If there is an IO already attached + * then that will update the notch + * once the element becomes visible. + * As a result, there is no need to create + * another one. + */ + if (this.notchVisibilityIO !== undefined) { + return; + } + + const io = (this.notchVisibilityIO = new IntersectionObserver( + (ev) => { + /** + * If the element is visible then we + * can try setting the notch width again. + */ + if (ev[0].intersectionRatio === 1) { + this.setNotchWidth(); + io.disconnect(); + this.notchVisibilityIO = undefined; + } + }, + /** + * Set the root to be the select + * This causes the IO callback + * to be fired in WebKit as soon as the element + * is visible. If we used the default root value + * then WebKit would only fire the IO callback + * after any animations (such as a modal transition) + * finished, and there would potentially be a flicker. + */ + { threshold: 0.01, root: el } + )); + + io.observe(notchSpacerEl); + return; + } + + /** + * If the element is visible then we can set the notch width. + * The notch is only visible when the label is scaled, + * which is why we multiply the width by 0.75 as this is + * the same amount the label element is scaled by in the + * select CSS (See $select-floating-label-scale in select.vars.scss). + */ + notchSpacerEl.style.setProperty('width', `${width * 0.75}px`); } /** @@ -770,7 +914,7 @@ export class Select implements ComponentInterface {
- diff --git a/core/src/components/select/test/fill/select.e2e.ts b/core/src/components/select/test/fill/select.e2e.ts index 767191fbdfa..cfd282d7854 100644 --- a/core/src/components/select/test/fill/select.e2e.ts +++ b/core/src/components/select/test/fill/select.e2e.ts @@ -120,6 +120,7 @@ configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => { const select = page.locator('ion-select'); expect(await select.screenshot()).toMatchSnapshot(screenshot(`select-fill-outline-label-floating`)); }); + test('should not have visual regressions with shaped outline', async ({ page }) => { await page.setContent( ` @@ -167,3 +168,60 @@ configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => { }); }); }); + +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('select: label slot'), () => { + test('should render the notch correctly with a slotted label', async ({ page }) => { + await page.setContent( + ` + + +
My Label Content
+ Apple +
+ `, + config + ); + + const select = page.locator('ion-select'); + expect(await select.screenshot()).toMatchSnapshot(screenshot(`select-fill-outline-slotted-label`)); + }); + test('should render the notch correctly with a slotted label after the select was originally hidden', async ({ + page, + }) => { + await page.setContent( + ` + + +
My Label Content
+ Apple +
+ `, + config + ); + + const select = page.locator('ion-select'); + + await select.evaluate((el: HTMLIonSelectElement) => el.style.removeProperty('display')); + + expect(await select.screenshot()).toMatchSnapshot(screenshot(`select-fill-outline-hidden-slotted-label`)); + }); + }); +}); diff --git a/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-hidden-slotted-label-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-hidden-slotted-label-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..e189c85fe0e Binary files /dev/null and b/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-hidden-slotted-label-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-hidden-slotted-label-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-hidden-slotted-label-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..79d572bee45 Binary files /dev/null and b/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-hidden-slotted-label-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-hidden-slotted-label-md-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-hidden-slotted-label-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..0344c584fe7 Binary files /dev/null and b/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-hidden-slotted-label-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-slotted-label-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-slotted-label-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..e189c85fe0e Binary files /dev/null and b/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-slotted-label-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-slotted-label-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-slotted-label-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..79d572bee45 Binary files /dev/null and b/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-slotted-label-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-slotted-label-md-ltr-Mobile-Safari-linux.png b/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-slotted-label-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..0344c584fe7 Binary files /dev/null and b/core/src/components/select/test/fill/select.e2e.ts-snapshots/select-fill-outline-slotted-label-md-ltr-Mobile-Safari-linux.png differ