From a45395cc02b2617b80e6c2389fa745e7c20540fc Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 15 Jun 2023 15:23:41 -0400 Subject: [PATCH] feat(input): add experimental label slot (#27650) Issue number: resolves #27061 --------- ## What is the current behavior? Input does not accept custom HTML labels ## What is the new behavior? - Input accepts custom HTML labels as an experimental feature. We marked this as experimental because it makes use of "scoped slots" which is an emulated version of Web Component slots. As a result, there may be instances where the slot behavior does not exactly match the native slot behavior. Note to reviewers: This is a combination of previously reviewed PRs. The implementation is complete, so feel free to bikeshed. ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information Docs PR: https://github.com/ionic-team/ionic-docs/pull/2997 --------- Co-authored-by: Brandy Carney --- core/src/components.d.ts | 4 +- .../components/input/input.md.outline.scss | 10 + core/src/components/input/input.scss | 13 +- core/src/components/input/input.tsx | 76 +++++++- .../src/components/input/test/a11y/index.html | 1 + .../components/input/test/fill/input.e2e.ts | 68 +++++++ ...otted-label-md-ltr-Mobile-Chrome-linux.png | Bin 0 -> 10772 bytes ...tted-label-md-ltr-Mobile-Firefox-linux.png | Bin 0 -> 3730 bytes ...otted-label-md-ltr-Mobile-Safari-linux.png | Bin 0 -> 10630 bytes ...otted-label-md-ltr-Mobile-Chrome-linux.png | Bin 0 -> 10772 bytes ...tted-label-md-ltr-Mobile-Firefox-linux.png | Bin 0 -> 3730 bytes ...otted-label-md-ltr-Mobile-Safari-linux.png | Bin 0 -> 10630 bytes core/src/components/input/test/input.spec.ts | 54 ++++++ .../input/test/label-placement/input.e2e.ts | 77 +++++--- ...async-label-md-ltr-Mobile-Chrome-linux.png | Bin 0 -> 6698 bytes ...sync-label-md-ltr-Mobile-Firefox-linux.png | Bin 0 -> 2425 bytes ...async-label-md-ltr-Mobile-Safari-linux.png | Bin 0 -> 6393 bytes ...ot-truncate-md-ltr-Mobile-Chrome-linux.png | Bin 0 -> 7553 bytes ...t-truncate-md-ltr-Mobile-Firefox-linux.png | Bin 0 -> 3605 bytes ...ot-truncate-md-ltr-Mobile-Safari-linux.png | Bin 0 -> 7197 bytes ...el-truncate-md-ltr-Mobile-Chrome-linux.png | Bin 0 -> 7553 bytes ...l-truncate-md-ltr-Mobile-Firefox-linux.png | Bin 0 -> 3605 bytes ...el-truncate-md-ltr-Mobile-Safari-linux.png | Bin 0 -> 7197 bytes ...long-label-ios-ltr-Mobile-Chrome-linux.png | Bin 9748 -> 0 bytes ...ong-label-ios-ltr-Mobile-Firefox-linux.png | Bin 4360 -> 0 bytes ...long-label-ios-ltr-Mobile-Safari-linux.png | Bin 9130 -> 0 bytes ...long-label-ios-rtl-Mobile-Chrome-linux.png | Bin 9693 -> 0 bytes ...ong-label-ios-rtl-Mobile-Firefox-linux.png | Bin 4404 -> 0 bytes ...long-label-ios-rtl-Mobile-Safari-linux.png | Bin 9083 -> 0 bytes ...-long-label-md-ltr-Mobile-Chrome-linux.png | Bin 9947 -> 0 bytes ...long-label-md-ltr-Mobile-Firefox-linux.png | Bin 4236 -> 0 bytes ...-long-label-md-ltr-Mobile-Safari-linux.png | Bin 9299 -> 0 bytes ...-long-label-md-rtl-Mobile-Chrome-linux.png | Bin 9891 -> 0 bytes ...long-label-md-rtl-Mobile-Firefox-linux.png | Bin 4047 -> 0 bytes ...-long-label-md-rtl-Mobile-Safari-linux.png | Bin 9233 -> 0 bytes ...long-label-ios-ltr-Mobile-Chrome-linux.png | Bin 9709 -> 0 bytes ...ong-label-ios-ltr-Mobile-Firefox-linux.png | Bin 4375 -> 0 bytes ...long-label-ios-ltr-Mobile-Safari-linux.png | Bin 9111 -> 0 bytes ...long-label-ios-rtl-Mobile-Chrome-linux.png | Bin 9677 -> 0 bytes ...ong-label-ios-rtl-Mobile-Firefox-linux.png | Bin 4345 -> 0 bytes ...long-label-ios-rtl-Mobile-Safari-linux.png | Bin 9119 -> 0 bytes ...-long-label-md-ltr-Mobile-Chrome-linux.png | Bin 9917 -> 0 bytes ...long-label-md-ltr-Mobile-Firefox-linux.png | Bin 4226 -> 0 bytes ...-long-label-md-ltr-Mobile-Safari-linux.png | Bin 9294 -> 0 bytes ...-long-label-md-rtl-Mobile-Chrome-linux.png | Bin 9930 -> 0 bytes ...long-label-md-rtl-Mobile-Firefox-linux.png | Bin 4062 -> 0 bytes ...-long-label-md-rtl-Mobile-Safari-linux.png | Bin 9285 -> 0 bytes .../src/components/input/test/slot/index.html | 139 ++++++++++++++ core/src/components/select/select.tsx | 148 ++------------- core/src/utils/forms/index.ts | 1 + core/src/utils/forms/notch-controller.ts | 177 ++++++++++++++++++ core/src/utils/slot-mutation-controller.ts | 118 ++++++++++++ 52 files changed, 721 insertions(+), 165 deletions(-) create mode 100644 core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-hidden-slotted-label-md-ltr-Mobile-Chrome-linux.png create mode 100644 core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-hidden-slotted-label-md-ltr-Mobile-Firefox-linux.png create mode 100644 core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-hidden-slotted-label-md-ltr-Mobile-Safari-linux.png create mode 100644 core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-slotted-label-md-ltr-Mobile-Chrome-linux.png create mode 100644 core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-slotted-label-md-ltr-Mobile-Firefox-linux.png create mode 100644 core/src/components/input/test/fill/input.e2e.ts-snapshots/input-fill-outline-slotted-label-md-ltr-Mobile-Safari-linux.png create mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-async-label-md-ltr-Mobile-Chrome-linux.png create mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-async-label-md-ltr-Mobile-Firefox-linux.png create mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-async-label-md-ltr-Mobile-Safari-linux.png create mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-label-slot-truncate-md-ltr-Mobile-Chrome-linux.png create mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-label-slot-truncate-md-ltr-Mobile-Firefox-linux.png create mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-label-slot-truncate-md-ltr-Mobile-Safari-linux.png create mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-label-truncate-md-ltr-Mobile-Chrome-linux.png create mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-label-truncate-md-ltr-Mobile-Firefox-linux.png create mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-label-truncate-md-ltr-Mobile-Safari-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-end-long-label-ios-ltr-Mobile-Chrome-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-end-long-label-ios-ltr-Mobile-Firefox-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-end-long-label-ios-ltr-Mobile-Safari-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-end-long-label-ios-rtl-Mobile-Chrome-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-end-long-label-ios-rtl-Mobile-Firefox-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-end-long-label-ios-rtl-Mobile-Safari-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-end-long-label-md-ltr-Mobile-Chrome-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-end-long-label-md-ltr-Mobile-Firefox-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-end-long-label-md-ltr-Mobile-Safari-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-end-long-label-md-rtl-Mobile-Chrome-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-end-long-label-md-rtl-Mobile-Firefox-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-end-long-label-md-rtl-Mobile-Safari-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-start-long-label-ios-ltr-Mobile-Chrome-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-start-long-label-ios-ltr-Mobile-Firefox-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-start-long-label-ios-ltr-Mobile-Safari-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-start-long-label-ios-rtl-Mobile-Chrome-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-start-long-label-ios-rtl-Mobile-Firefox-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-start-long-label-ios-rtl-Mobile-Safari-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-start-long-label-md-ltr-Mobile-Chrome-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-start-long-label-md-ltr-Mobile-Firefox-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-start-long-label-md-ltr-Mobile-Safari-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-start-long-label-md-rtl-Mobile-Chrome-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-start-long-label-md-rtl-Mobile-Firefox-linux.png delete mode 100644 core/src/components/input/test/label-placement/input.e2e.ts-snapshots/input-placement-start-long-label-md-rtl-Mobile-Safari-linux.png create mode 100644 core/src/components/input/test/slot/index.html create mode 100644 core/src/utils/forms/notch-controller.ts create mode 100644 core/src/utils/slot-mutation-controller.ts diff --git a/core/src/components.d.ts b/core/src/components.d.ts index c17d097daf4..b65f10b1791 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -1214,7 +1214,7 @@ export namespace Components { */ "inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'; /** - * The visible label associated with the input. + * The visible label associated with the input. Use this if you need to render a plaintext label. The `label` property will take priority over the `label` slot if both are used. */ "label"?: string; /** @@ -5248,7 +5248,7 @@ declare namespace LocalJSX { */ "inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'; /** - * The visible label associated with the input. + * The visible label associated with the input. Use this if you need to render a plaintext label. The `label` property will take priority over the `label` slot if both are used. */ "label"?: string; /** diff --git a/core/src/components/input/input.md.outline.scss b/core/src/components/input/input.md.outline.scss index 30367cf8077..4efbd2fcdd1 100644 --- a/core/src/components/input/input.md.outline.scss +++ b/core/src/components/input/input.md.outline.scss @@ -172,6 +172,16 @@ opacity: 0; pointer-events: none; + + /** + * The spacer currently inherits + * border-box sizing from the Ionic reset styles. + * However, we do not want to include padding in + * the calculation of the element dimensions. + * This code can be removed if input is updated + * to use the Shadow DOM. + */ + box-sizing: content-box; } :host(.input-fill-outline) .input-outline-start { diff --git a/core/src/components/input/input.scss b/core/src/components/input/input.scss index ab0cf76b37c..4a18956e01b 100644 --- a/core/src/components/input/input.scss +++ b/core/src/components/input/input.scss @@ -463,7 +463,8 @@ * works on block-level elements. A flex item is * considered blockified (https://www.w3.org/TR/css-display-3/#blockify). */ -.label-text { +.label-text, +::slotted([slot="label"]) { text-overflow: ellipsis; white-space: nowrap; @@ -471,6 +472,16 @@ overflow: hidden; } +/** + * If no label text is placed into the slot + * then the element should be hidden otherwise + * there will be additional margins added. + */ +.label-text-wrapper-hidden, +.input-outline-notch-hidden { + display: none; +} + .input-wrapper input { /** * When the floating label appears on top of the diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx index e76bf1dafc6..7e138469ab5 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -1,10 +1,12 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, h } from '@stencil/core'; -import type { LegacyFormController } from '@utils/forms'; -import { createLegacyFormController } from '@utils/forms'; +import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, forceUpdate, h } from '@stencil/core'; +import type { LegacyFormController, NotchController } from '@utils/forms'; +import { createLegacyFormController, createNotchController } from '@utils/forms'; import type { Attributes } from '@utils/helpers'; import { inheritAriaAttributes, debounceEvent, findItemLabel, inheritAttributes } from '@utils/helpers'; import { printIonWarning } from '@utils/logging'; +import { createSlotMutationController } from '@utils/slot-mutation-controller'; +import type { SlotMutationController } from '@utils/slot-mutation-controller'; import { createColorClasses, hostContext } from '@utils/theme'; import { closeCircle, closeSharp } from 'ionicons/icons'; @@ -16,6 +18,8 @@ import { getCounterText } from './input.utils'; /** * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use. + * + * @slot label - The label text to associate with the input. Use the `labelPlacement` property to control where the label is placed relative to the input. Use this if you need to render a label with custom HTML. (EXPERIMENTAL) */ @Component({ tag: 'ion-input', @@ -31,6 +35,9 @@ export class Input implements ComponentInterface { private inheritedAttributes: Attributes = {}; private isComposing = false; private legacyFormController!: LegacyFormController; + private slotMutationController?: SlotMutationController; + private notchController?: NotchController; + private notchSpacerEl: HTMLElement | undefined; // This flag ensures we log the deprecation warning at most once. private hasLoggedDeprecationWarning = false; @@ -165,6 +172,10 @@ export class Input implements ComponentInterface { /** * The visible label associated with the input. + * + * Use this if you need to render a plaintext label. + * + * The `label` property will take priority over the `label` slot if both are used. */ @Prop() label?: string; @@ -353,6 +364,12 @@ export class Input implements ComponentInterface { const { el } = this; this.legacyFormController = createLegacyFormController(el); + this.slotMutationController = createSlotMutationController(el, 'label', () => forceUpdate(this)); + this.notchController = createNotchController( + el, + () => this.notchSpacerEl, + () => this.labelSlot + ); this.emitStyle(); this.debounceChanged(); @@ -369,6 +386,10 @@ export class Input implements ComponentInterface { this.originalIonInput = this.ionInput; } + componentDidRender() { + this.notchController?.calculateNotchWidth(); + } + disconnectedCallback() { if (Build.isBrowser) { document.dispatchEvent( @@ -377,6 +398,16 @@ export class Input implements ComponentInterface { }) ); } + + if (this.slotMutationController) { + this.slotMutationController.destroy(); + this.slotMutationController = undefined; + } + + if (this.notchController) { + this.notchController.destroy(); + this.notchController = undefined; + } } /** @@ -578,17 +609,37 @@ export class Input implements ComponentInterface { private renderLabel() { const { label } = this; - if (label === undefined) { - return; - } return ( -
-
{this.label}
+
+ {label === undefined ? :
{label}
}
); } + /** + * 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 + * to get the plaintext value of the label use + * the `labelText` getter instead. + */ + private get hasLabel() { + return this.label !== undefined || this.labelSlot !== null; + } + /** * Renders the border container * when fill="outline". @@ -608,8 +659,13 @@ export class Input implements ComponentInterface { return [
-
-