From bfad5a4ba720fa4146a93e8e0223e9faa7bc316c Mon Sep 17 00:00:00 2001 From: Amanda Johnston <90629384+amandaejohnston@users.noreply.github.com> Date: Mon, 27 Nov 2023 09:44:25 -0600 Subject: [PATCH 1/6] refactor(utils): update slotMutationController to allow multiple slot names (#28396) Issue number: Internal --------- ## What is the current behavior? The internal `slotMutationController` utility can only watch one slot name at a time. ## What is the new behavior? Added the option to specify multiple slot names, any of which will trigger the callback. This change is made in preparation for upcoming `start` and `end` slots for several form control components; it boosts performance by preventing us from needing to add multiple `createSlotMutationController` calls to each component. ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information --- core/src/utils/slot-mutation-controller.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/src/utils/slot-mutation-controller.ts b/core/src/utils/slot-mutation-controller.ts index 663b781a367..3827c28f9fa 100644 --- a/core/src/utils/slot-mutation-controller.ts +++ b/core/src/utils/slot-mutation-controller.ts @@ -6,18 +6,20 @@ import { raf } from '@utils/helpers'; * This is not needed for components using native slots in the Shadow DOM. * @internal * @param el The host element to observe - * @param slotName mutationCallback will fire when nodes on this slot change + * @param slotName mutationCallback will fire when nodes on these slot(s) change * @param mutationCallback The callback to fire whenever the slotted content changes */ export const createSlotMutationController = ( el: HTMLElement, - slotName: string, + slotName: string | string[], mutationCallback: () => void ): SlotMutationController => { let hostMutationObserver: MutationObserver | undefined; let slottedContentMutationObserver: MutationObserver | undefined; if (win !== undefined && 'MutationObserver' in win) { + const slots = Array.isArray(slotName) ? slotName : [slotName]; + hostMutationObserver = new MutationObserver((entries) => { for (const entry of entries) { for (const node of entry.addedNodes) { @@ -25,7 +27,7 @@ export const createSlotMutationController = ( * Check to see if the added node * is our slotted content. */ - if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).slot === slotName) { + if (node.nodeType === Node.ELEMENT_NODE && slots.includes((node as HTMLElement).slot)) { /** * If so, we want to watch the slotted * content itself for changes. This lets us From 3648520e09b930f8861d2d19ec6efc415a94bc18 Mon Sep 17 00:00:00 2001 From: Amanda Johnston <90629384+amandaejohnston@users.noreply.github.com> Date: Mon, 27 Nov 2023 09:47:28 -0600 Subject: [PATCH 2/6] feat(input): add start and end slots (#28397) Issue number: Part of #26297 --------- ## What is the current behavior? With the modern form control syntax, it is not possible to add icon buttons or other decorators to the sides of `ion-input`, as you can with `ion-item`. ## What is the new behavior? `start` and `end` slots added. While making this change, I also tweaked the CSS selectors responsible for translating the label above the input with `"stacked"` and `"floating"` placements, merging this logic into a single class managed by the TSX. I needed to add a new class for whether slot content is present, so the selectors were getting unwieldy otherwise. Docs PR TBA; I plan on knocking out all three components at once when the features are all complete, to make dev builds easier to manage. ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information --------- Co-authored-by: ionitron --- .../components/input/input.md.outline.scss | 8 +- core/src/components/input/input.md.solid.scss | 4 +- core/src/components/input/input.scss | 18 +- core/src/components/input/input.tsx | 44 ++++- .../src/components/input/test/a11y/index.html | 10 +- .../src/components/input/test/slot/index.html | 185 ++++++++++++++++-- .../components/input/test/slot/input.e2e.ts | 68 +++++++ ...l-floating-ios-ltr-Mobile-Chrome-linux.png | Bin 0 -> 2938 bytes ...-floating-ios-ltr-Mobile-Firefox-linux.png | Bin 0 -> 3122 bytes ...l-floating-ios-ltr-Mobile-Safari-linux.png | Bin 0 -> 2573 bytes ...l-floating-ios-rtl-Mobile-Chrome-linux.png | Bin 0 -> 2956 bytes ...-floating-ios-rtl-Mobile-Firefox-linux.png | Bin 0 -> 3279 bytes ...l-floating-ios-rtl-Mobile-Safari-linux.png | Bin 0 -> 2579 bytes ...el-floating-md-ltr-Mobile-Chrome-linux.png | Bin 0 -> 2804 bytes ...l-floating-md-ltr-Mobile-Firefox-linux.png | Bin 0 -> 2889 bytes ...el-floating-md-ltr-Mobile-Safari-linux.png | Bin 0 -> 2443 bytes ...el-floating-md-rtl-Mobile-Chrome-linux.png | Bin 0 -> 2817 bytes ...l-floating-md-rtl-Mobile-Firefox-linux.png | Bin 0 -> 2829 bytes ...el-floating-md-rtl-Mobile-Safari-linux.png | Bin 0 -> 2447 bytes ...abel-start-ios-ltr-Mobile-Chrome-linux.png | Bin 0 -> 3226 bytes ...bel-start-ios-ltr-Mobile-Firefox-linux.png | Bin 0 -> 3302 bytes ...abel-start-ios-ltr-Mobile-Safari-linux.png | Bin 0 -> 2896 bytes ...abel-start-ios-rtl-Mobile-Chrome-linux.png | Bin 0 -> 3249 bytes ...bel-start-ios-rtl-Mobile-Firefox-linux.png | Bin 0 -> 3123 bytes ...abel-start-ios-rtl-Mobile-Safari-linux.png | Bin 0 -> 2917 bytes ...label-start-md-ltr-Mobile-Chrome-linux.png | Bin 0 -> 3126 bytes ...abel-start-md-ltr-Mobile-Firefox-linux.png | Bin 0 -> 3036 bytes ...label-start-md-ltr-Mobile-Safari-linux.png | Bin 0 -> 2710 bytes ...label-start-md-rtl-Mobile-Chrome-linux.png | Bin 0 -> 3089 bytes ...abel-start-md-rtl-Mobile-Firefox-linux.png | Bin 0 -> 3037 bytes ...label-start-md-rtl-Mobile-Safari-linux.png | Bin 0 -> 2709 bytes 31 files changed, 307 insertions(+), 30 deletions(-) create mode 100644 core/src/components/input/test/slot/input.e2e.ts create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-floating-ios-ltr-Mobile-Chrome-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-floating-ios-ltr-Mobile-Firefox-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-floating-ios-ltr-Mobile-Safari-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-floating-ios-rtl-Mobile-Chrome-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-floating-ios-rtl-Mobile-Firefox-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-floating-ios-rtl-Mobile-Safari-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-floating-md-ltr-Mobile-Chrome-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-floating-md-ltr-Mobile-Firefox-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-floating-md-ltr-Mobile-Safari-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-floating-md-rtl-Mobile-Chrome-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-floating-md-rtl-Mobile-Firefox-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-floating-md-rtl-Mobile-Safari-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-start-ios-ltr-Mobile-Chrome-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-start-ios-ltr-Mobile-Firefox-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-start-ios-ltr-Mobile-Safari-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-start-ios-rtl-Mobile-Chrome-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-start-ios-rtl-Mobile-Firefox-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-start-ios-rtl-Mobile-Safari-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-start-md-ltr-Mobile-Chrome-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-start-md-ltr-Mobile-Firefox-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-start-md-ltr-Mobile-Safari-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-start-md-rtl-Mobile-Chrome-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-start-md-rtl-Mobile-Firefox-linux.png create mode 100644 core/src/components/input/test/slot/input.e2e.ts-snapshots/input-slots-label-start-md-rtl-Mobile-Safari-linux.png diff --git a/core/src/components/input/input.md.outline.scss b/core/src/components/input/input.md.outline.scss index 4a1abfbbdcb..ea202b74ab6 100644 --- a/core/src/components/input/input.md.outline.scss +++ b/core/src/components/input/input.md.outline.scss @@ -89,9 +89,7 @@ /** * This makes the label sit above the input. */ -:host(.has-focus.input-fill-outline.input-label-placement-floating) .label-text-wrapper, -:host(.has-value.input-fill-outline.input-label-placement-floating) .label-text-wrapper, -:host(.input-fill-outline.input-label-placement-stacked) .label-text-wrapper { +:host(.label-floating.input-fill-outline) .label-text-wrapper { @include transform(translateY(-32%), scale(#{$form-control-label-stacked-scale})); @include margin(0); @@ -216,8 +214,6 @@ * the floating/stacked label. We simulate this "cut out" * by removing the top border from the notch fragment. */ -:host(.has-focus.input-fill-outline.input-label-placement-floating) .input-outline-notch, -:host(.has-value.input-fill-outline.input-label-placement-floating) .input-outline-notch, -:host(.input-fill-outline.input-label-placement-stacked) .input-outline-notch { +:host(.label-floating.input-fill-outline) .input-outline-notch { border-top: none; } diff --git a/core/src/components/input/input.md.solid.scss b/core/src/components/input/input.md.solid.scss index 5765f764ba0..98cde92880a 100644 --- a/core/src/components/input/input.md.solid.scss +++ b/core/src/components/input/input.md.solid.scss @@ -67,9 +67,7 @@ // Input Label // ---------------------------------------------------------------- -:host(.input-fill-solid.input-label-placement-stacked) .label-text-wrapper, -:host(.has-focus.input-fill-solid.input-label-placement-floating) .label-text-wrapper, -:host(.has-value.input-fill-solid.input-label-placement-floating) .label-text-wrapper { +:host(.label-floating.input-fill-solid.input-label-placement-floating) .label-text-wrapper { /** * Label text should not extend * beyond the bounds of the input. diff --git a/core/src/components/input/input.scss b/core/src/components/input/input.scss index d8aab456d53..f236b942668 100644 --- a/core/src/components/input/input.scss +++ b/core/src/components/input/input.scss @@ -322,6 +322,9 @@ flex-grow: 1; + // ensure start/end slot content is vertically centered + align-items: center; + width: 100%; } @@ -641,9 +644,7 @@ /** * This makes the label sit above the input. */ -:host(.input-label-placement-stacked) .label-text-wrapper, -:host(.has-focus.input-label-placement-floating) .label-text-wrapper, -:host(.has-value.input-label-placement-floating) .label-text-wrapper { +:host(.label-floating) .label-text-wrapper { @include transform(translateY(50%), scale(#{$form-control-label-stacked-scale})); /** @@ -652,3 +653,14 @@ */ max-width: calc(100% / #{$form-control-label-stacked-scale}); } + +// Start/End Slots +// ---------------------------------------------------------------- + +::slotted([slot="start"]) { + margin-inline-end: $form-control-label-margin; +} + +::slotted([slot="end"]) { + margin-inline-start: $form-control-label-margin; +} \ No newline at end of file diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx index 00214022426..28ee6816a6c 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -26,6 +26,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) + * @slot start - Content to display at the leading edge of the input. (EXPERIMENTAL) + * @slot end - Content to display at the trailing edge of the input. (EXPERIMENTAL) */ @Component({ tag: 'ion-input', @@ -369,7 +371,7 @@ export class Input implements ComponentInterface { const { el } = this; this.legacyFormController = createLegacyFormController(el); - this.slotMutationController = createSlotMutationController(el, 'label', () => forceUpdate(this)); + this.slotMutationController = createSlotMutationController(el, ['label', 'start', 'end'], () => forceUpdate(this)); this.notchController = createNotchController( el, () => this.notchSpacerEl, @@ -697,18 +699,42 @@ export class Input implements ComponentInterface { } private renderInput() { - const { disabled, fill, readonly, shape, inputId, labelPlacement } = this; + const { disabled, fill, readonly, shape, inputId, labelPlacement, el, hasFocus } = this; const mode = getIonMode(this); const value = this.getValue(); const inItem = hostContext('ion-item', this.el); const shouldRenderHighlight = mode === 'md' && fill !== 'outline' && !inItem; + const hasValue = this.hasValue(); + const hasStartEndSlots = el.querySelector('[slot="start"], [slot="end"]') !== null; + + /** + * If the label is stacked, it should always sit above the input. + * For floating labels, the label should move above the input if + * the input has a value, is focused, 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 input 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 || hasStartEndSlots)); + return ( -