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..7b0314e0bd8 100644 --- a/core/src/components/input/input.scss +++ b/core/src/components/input/input.scss @@ -189,7 +189,6 @@ // -------------------------------------------------- .input-clear-icon { - @include margin(auto); @include padding(0); @include background-position(center); @@ -259,6 +258,19 @@ line-height: normal; } +// Input Control (Label + Input) +// ---------------------------------------------------------------- + +.input-control { + 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 Containers +// ---------------------------------------------------------------- + +.input-start, +.input-end { + display: flex; + + position: relative; + + flex-shrink: 0; + + // Ensure start/end content is vertically centered + align-items: center; +} + // Input Highlight // ---------------------------------------------------------------- @@ -467,7 +491,7 @@ * Label is on the left of the input in LTR and * on the right in RTL. */ -:host(.input-label-placement-start) .input-wrapper { +:host(.input-label-placement-start) .input-control { flex-direction: row; } @@ -487,7 +511,7 @@ * Label is on the right of the input in LTR and * on the left in RTL. */ -:host(.input-label-placement-end) .input-wrapper { +:host(.input-label-placement-end) .input-control { flex-direction: row-reverse; } @@ -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-control, +:host(.input-label-placement-floating) .input-control { 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..b0fc69e7862 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -20,7 +20,7 @@ import { inheritAriaAttributes, debounceEvent, inheritAttributes, componentOnRea 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'; +import { closeCircle, closeCircleSharp } from 'ionicons/icons'; import { getIonMode } from '../../global/ionic-global'; import type { AutocompleteTypes, Color, TextFieldTypes } from '../../interface'; @@ -466,6 +466,26 @@ 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"`. + * In RTL mode, the adjustment is positive; in LTR mode, it's negative. + */ + private getStartSlotAdjustment(): string { + const startSlot = this.el.querySelector('.input-start') 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; + const isRTL = document.dir === 'rtl'; + const sign = isRTL ? '' : '-'; + return roundedWidth ? `${sign}${roundedWidth}px` : '0px'; + } + disconnectedCallback() { if (Build.isBrowser) { document.dispatchEvent( @@ -800,48 +820,31 @@ export class Input implements ComponentInterface { * otherwise, two clicks will be triggered. */ private onLabelClick = (ev: MouseEvent) => { - // Only stop propagation if the click was directly on the label - // and not on the input or other child elements - if (ev.target === ev.currentTarget) { - ev.stopPropagation(); - } + ev.stopPropagation(); }; /** * 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,36 +853,23 @@ 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); const shouldRenderHighlight = mode === 'md' && fill !== 'outline' && !inItem; - const defaultClearIcon = mode === 'ios' ? closeCircle : closeSharp; + const defaultClearIcon = mode === 'ios' ? closeCircle : closeCircleSharp; 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,46 +894,53 @@ export class Input implements ComponentInterface { * since it comes before the input in the DOM. */}