-
+
(this.notchSpacerEl = el)}>
{this.label}
diff --git a/core/src/components/input/test/fill/input.e2e.ts b/core/src/components/input/test/fill/input.e2e.ts
index 7688efdf711..39c0c1bef7d 100644
--- a/core/src/components/input/test/fill/input.e2e.ts
+++ b/core/src/components/input/test/fill/input.e2e.ts
@@ -180,3 +180,58 @@ configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => {
});
});
});
+
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+ test.describe(title('input: label slot'), () => {
+ test('should render the notch correctly with a slotted label', async ({ page }) => {
+ await page.setContent(
+ `
+
+
+ My Label Content
+
+ `,
+ config
+ );
+
+ const input = page.locator('ion-input');
+ expect(await input.screenshot()).toMatchSnapshot(screenshot(`input-fill-outline-slotted-label`));
+ });
+ test('should render the notch correctly with a slotted label after the input was originally hidden', async ({
+ page,
+ }) => {
+ await page.setContent(
+ `
+
+
+ My Label Content
+
+ `,
+ config
+ );
+
+ const input = page.locator('ion-input');
+
+ await input.evaluate((el: HTMLIonSelectElement) => el.style.removeProperty('display'));
+
+ expect(await input.screenshot()).toMatchSnapshot(screenshot(`input-fill-outline-hidden-slotted-label`));
+ });
+ });
+});
diff --git a/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-hidden-slotted-label-md-ltr-Mobile-Chrome-linux.png b/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-hidden-slotted-label-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..39518074a14
Binary files /dev/null and b/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-hidden-slotted-label-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-hidden-slotted-label-md-ltr-Mobile-Firefox-linux.png b/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-hidden-slotted-label-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..51ddf404110
Binary files /dev/null and b/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-hidden-slotted-label-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-hidden-slotted-label-md-ltr-Mobile-Safari-linux.png b/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-hidden-slotted-label-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..991042ab59f
Binary files /dev/null and b/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-hidden-slotted-label-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-slotted-label-md-ltr-Mobile-Chrome-linux.png b/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-slotted-label-md-ltr-Mobile-Chrome-linux.png
new file mode 100644
index 00000000000..39518074a14
Binary files /dev/null and b/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-slotted-label-md-ltr-Mobile-Chrome-linux.png differ
diff --git a/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-slotted-label-md-ltr-Mobile-Firefox-linux.png b/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-slotted-label-md-ltr-Mobile-Firefox-linux.png
new file mode 100644
index 00000000000..51ddf404110
Binary files /dev/null and b/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-slotted-label-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-slotted-label-md-ltr-Mobile-Safari-linux.png b/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-slotted-label-md-ltr-Mobile-Safari-linux.png
new file mode 100644
index 00000000000..991042ab59f
Binary files /dev/null and b/core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-slotted-label-md-ltr-Mobile-Safari-linux.png differ
diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx
index 39831d66329..b14c498988c 100644
--- a/core/src/components/select/select.tsx
+++ b/core/src/components/select/select.tsx
@@ -1,9 +1,8 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Method, Prop, State, Watch, h, forceUpdate } from '@stencil/core';
-import { win } from '@utils/browser';
-import type { LegacyFormController } from '@utils/forms';
-import { createLegacyFormController } from '@utils/forms';
-import { findItemLabel, focusElement, getAriaLabel, renderHiddenInput, inheritAttributes, raf } from '@utils/helpers';
+import type { LegacyFormController, NotchController } from '@utils/forms';
+import { createLegacyFormController, createNotchController } from '@utils/forms';
+import { findItemLabel, focusElement, getAriaLabel, renderHiddenInput, inheritAttributes } from '@utils/helpers';
import type { Attributes } from '@utils/helpers';
import { printIonWarning } from '@utils/logging';
import { actionSheetController, alertController, popoverController } from '@utils/overlays';
@@ -58,7 +57,8 @@ export class Select implements ComponentInterface {
private inheritedAttributes: Attributes = {};
private nativeWrapperEl: HTMLElement | undefined;
private notchSpacerEl: HTMLElement | undefined;
- private notchVisibilityIO: IntersectionObserver | undefined;
+
+ private notchController?: NotchController;
// This flag ensures we log the deprecation warning at most once.
private hasLoggedDeprecationWarning = false;
@@ -245,6 +245,11 @@ export class Select implements ComponentInterface {
const { el } = this;
this.legacyFormController = createLegacyFormController(el);
+ this.notchController = createNotchController(
+ el,
+ () => this.notchSpacerEl,
+ () => this.labelSlot
+ );
this.updateOverlayOptions();
this.emitStyle();
@@ -267,6 +272,11 @@ export class Select implements ComponentInterface {
this.mutationO.disconnect();
this.mutationO = undefined;
}
+
+ if (this.notchController) {
+ this.notchController.destroy();
+ this.notchController = undefined;
+ }
}
/**
@@ -746,17 +756,7 @@ 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();
- });
- }
+ this.notchController?.calculateNotchWidth();
}
/**
@@ -777,120 +777,6 @@ export class Select implements ComponentInterface {
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 either the label property is being
- * used or the label slot is not defined,
- * then we do not need to estimate the notch width.
- */
- this.label !== undefined ||
- this.labelSlot === null
- ) {
- 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`);
- }
-
/**
* Renders the border container
* when fill="outline".
diff --git a/core/src/utils/forms/index.ts b/core/src/utils/forms/index.ts
index d7c63744bdc..f219f3978a9 100644
--- a/core/src/utils/forms/index.ts
+++ b/core/src/utils/forms/index.ts
@@ -1 +1,2 @@
export * from './form-controller';
+export * from './notch-controller';
diff --git a/core/src/utils/forms/notch-controller.ts b/core/src/utils/forms/notch-controller.ts
new file mode 100644
index 00000000000..1507f1f3763
--- /dev/null
+++ b/core/src/utils/forms/notch-controller.ts
@@ -0,0 +1,177 @@
+import { win } from '@utils/browser';
+import { raf } from '@utils/helpers';
+
+type NotchElement = HTMLIonInputElement | HTMLIonSelectElement;
+
+/**
+ * A utility to calculate the size of an outline notch
+ * width relative to the content passed. This is used in
+ * components such as `ion-select` with `fill="outline"`
+ * where we need to pass slotted HTML content. This is not
+ * needed when rendering plaintext content because we can
+ * render the plaintext again hidden with `opacity: 0` inside
+ * of the notch. As a result we can rely on the intrinsic size
+ * of the element to correctly compute the notch width. We
+ * cannot do this with slotted content because we cannot project
+ * it into 2 places at once.
+ *
+ * @internal
+ * @param el: The host element
+ * @param getNotchSpacerEl: A function that returns a reference to the notch spacer element inside of the component template.
+ * @param getLabelSlot: A function that returns a reference to the slotted content.
+ */
+export const createNotchController = (
+ el: NotchElement,
+ getNotchSpacerEl: () => HTMLElement | undefined,
+ getLabelSlot: () => Element | null
+): NotchController => {
+ let notchVisibilityIO: IntersectionObserver | undefined;
+
+ const needsExplicitNotchWidth = () => {
+ const notchSpacerEl = getNotchSpacerEl();
+
+ if (
+ /**
+ * If the notch is not being used
+ * then we do not need to set the notch width.
+ */
+ notchSpacerEl === undefined ||
+ /**
+ * If either the label property is being
+ * used or the label slot is not defined,
+ * then we do not need to estimate the notch width.
+ */
+ el.label !== undefined ||
+ getLabelSlot() === null
+ ) {
+ return false;
+ }
+
+ return true;
+ };
+
+ const calculateNotchWidth = () => {
+ if (needsExplicitNotchWidth()) {
+ /**
+ * Run this the frame after
+ * the browser has re-painted the host element.
+ * Otherwise, the label element may have a width
+ * of 0 and the IntersectionObserver will be used.
+ */
+ raf(() => {
+ setNotchWidth();
+ });
+ }
+ };
+
+ /**
+ * 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.
+ */
+ const setNotchWidth = () => {
+ const notchSpacerEl = getNotchSpacerEl();
+
+ if (notchSpacerEl === undefined) {
+ return;
+ }
+
+ if (!needsExplicitNotchWidth()) {
+ notchSpacerEl.style.removeProperty('width');
+ return;
+ }
+
+ const width = getLabelSlot()!.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 the host element 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 (notchVisibilityIO !== undefined) {
+ return;
+ }
+
+ const io = (notchVisibilityIO = new IntersectionObserver(
+ (ev) => {
+ /**
+ * If the element is visible then we
+ * can try setting the notch width again.
+ */
+ if (ev[0].intersectionRatio === 1) {
+ setNotchWidth();
+ io.disconnect();
+ notchVisibilityIO = undefined;
+ }
+ },
+ /**
+ * Set the root to be the host element
+ * 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 host CSS.
+ * (For ion-select, see $select-floating-label-scale in select.vars.scss).
+ */
+ notchSpacerEl.style.setProperty('width', `${width * 0.75}px`);
+ };
+
+ const destroy = () => {
+ if (notchVisibilityIO) {
+ notchVisibilityIO.disconnect();
+ notchVisibilityIO = undefined;
+ }
+ };
+
+ return {
+ calculateNotchWidth,
+ destroy,
+ };
+};
+
+export type NotchController = {
+ calculateNotchWidth: () => void;
+ destroy: () => void;
+};