From 71a8fc328f90bb6d9015a9457274914155111011 Mon Sep 17 00:00:00 2001 From: Brandy Smith <6577830+brandyscarney@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:14:19 -0400 Subject: [PATCH 01/12] fix(input): float label when start and end slot exist --- .../components/input/input.md.outline.scss | 13 +- core/src/components/input/input.scss | 44 ++-- core/src/components/input/input.tsx | 207 +++++++++--------- 3 files changed, 134 insertions(+), 130 deletions(-) diff --git a/core/src/components/input/input.md.outline.scss b/core/src/components/input/input.md.outline.scss index be54121d0e0..fd8028f4959 100644 --- a/core/src/components/input/input.md.outline.scss +++ b/core/src/components/input/input.md.outline.scss @@ -8,6 +8,7 @@ --border-radius: 4px; --padding-start: 16px; --padding-end: 16px; + --start-slot-adjustment: 0px; min-height: 56px; } @@ -90,7 +91,7 @@ * This makes the label sit above the input. */ :host(.label-floating.input-fill-outline) .label-text-wrapper { - @include transform(translateY(-32%), scale(#{$form-control-label-stacked-scale})); + @include transform(translate(var(--start-slot-adjustment), -32%), scale(#{$form-control-label-stacked-scale})); @include margin(0); /** @@ -100,16 +101,6 @@ max-width: calc((100% - var(--padding-start) - var(--padding-end) - #{$input-md-floating-label-padding * 2}) / #{$form-control-label-stacked-scale}); } -/** - * This ensures that the input does not - * overlap the floating label while still - * remaining visually centered. - */ -:host(.input-fill-outline.input-label-placement-stacked) input, -:host(.input-fill-outline.input-label-placement-floating) input { - @include margin(6px, 0, 6px, 0); -} - // Input Fill: Outline Outline Container // ---------------------------------------------------------------- diff --git a/core/src/components/input/input.scss b/core/src/components/input/input.scss index 476cb012bee..586cf64376a 100644 --- a/core/src/components/input/input.scss +++ b/core/src/components/input/input.scss @@ -259,6 +259,18 @@ line-height: normal; } +// Input Content (Label + Input) +// ---------------------------------------------------------------- + +.input-content { + display: flex; + position: relative; + + flex-grow: 1; + + width: 100%; +} + // Input Native Wrapper // ---------------------------------------------------------------- @@ -269,12 +281,24 @@ flex-grow: 1; - // ensure start/end slot content is vertically centered - align-items: center; - width: 100%; } +// Input Start/End Slots +// ---------------------------------------------------------------- + +.input-start-slot, +.input-end-slot { + display: flex; + + position: relative; + + flex-shrink: 0; + + // Ensure start/end slot content is vertically centered + align-items: center; +} + // Input Highlight // ---------------------------------------------------------------- @@ -532,10 +556,9 @@ * Floating: Label sits over the input when the input has no * value and is blurred. Label sits above the input and is scaled * down when the input is focused or has a value. - * */ -:host(.input-label-placement-stacked) .input-wrapper, -:host(.input-label-placement-floating) .input-wrapper { +:host(.input-label-placement-stacked) .input-content, +:host(.input-label-placement-floating) .input-content { flex-direction: column; align-items: start; } @@ -559,15 +582,6 @@ z-index: 2; } -/** - * Ensures the input does not - * overlap the label. - */ -:host(.input-label-placement-stacked) input, -:host(.input-label-placement-floating) input { - @include margin(1px, 0, 0, 0); -} - /** * This makes the label sit over the input * when the input is blurred and has no value. diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx index 89efdce43dd..3ca1fe70c63 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -466,6 +466,23 @@ export class Input implements ComponentInterface { this.notchController?.calculateNotchWidth(); } + /** + * Gets the width of the start slot, rounded to 1 decimal place. + * Only applies to inputs with `md` mode and `fill="outline"`. + */ + private getStartSlotAdjustment(): string { + const startSlot = this.el.querySelector('.input-start-slot') as HTMLElement | null; + if (!startSlot || !Build.isBrowser || getIonMode(this) !== 'md' || this.fill !== 'outline') { + return ''; + } + + // Round the width to a half pixel to ensure the label is + // placed properly when a start slot is added or removed. + const startSlotWidth = startSlot.getBoundingClientRect().width; + const roundedWidth = Math.round(startSlotWidth * 10) / 10; + return roundedWidth ? `-${roundedWidth}px` : '0px'; + } + disconnectedCallback() { if (Build.isBrowser) { document.dispatchEvent( @@ -811,37 +828,24 @@ export class Input implements ComponentInterface { * Renders the border container * when fill="outline". */ - private renderLabelContainer() { - const mode = getIonMode(this); - const hasOutlineFill = mode === 'md' && this.fill === 'outline'; - - if (hasOutlineFill) { - /** - * The outline fill has a special outline - * that appears around the input and the label. - * Certain stacked and floating label placements cause the - * label to translate up and create a "cut out" - * inside of that border by using the notch-spacer element. - */ - return [ -
-
-
- -
-
-
, - this.renderLabel(), - ]; - } + private renderOutlineDecorations() { + return [ +
, +
+ +
, +
, + ]; + } + private renderLabelContainer() { /** * If not using the outline style, * we can render just the label. @@ -850,7 +854,7 @@ export class Input implements ComponentInterface { } render() { - const { disabled, fill, readonly, shape, inputId, labelPlacement, el, hasFocus, clearInputIcon } = this; + const { disabled, fill, readonly, shape, inputId, labelPlacement, hasFocus, clearInputIcon } = this; const mode = getIonMode(this); const value = this.getValue(); const inItem = hostContext('ion-item', this.el); @@ -859,27 +863,14 @@ export class Input implements ComponentInterface { const clearIconData = clearInputIcon ?? defaultClearIcon; const hasValue = this.hasValue(); - const hasStartEndSlots = el.querySelector('[slot="start"], [slot="end"]') !== null; + const hasOutlineFill = mode === 'md' && fill === 'outline'; /** * 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 + * the input has a value or is focused. */ - const labelShouldFloat = - labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || hasStartEndSlots)); + const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus)); return ( {/** * htmlFor is needed so that clicking the label always focuses @@ -903,64 +895,71 @@ export class Input implements ComponentInterface { * since it comes before the input in the DOM. */}