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 [
+
,
+
+
(this.notchSpacerEl = el)}>
+ {this.label}
+
+
,
+
,
+ ];
+ }
+ 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.
*/}
- {this.renderLabelContainer()}
-
+ {hasOutlineFill &&
{this.renderOutlineDecorations()}
}
+
- (this.nativeInput = input)}
- id={inputId}
- disabled={disabled}
- autoCapitalize={this.autocapitalize}
- autoComplete={this.autocomplete}
- autoCorrect={this.autocorrect ? 'on' : 'off'}
- autoFocus={this.autofocus}
- enterKeyHint={this.enterkeyhint}
- inputMode={this.inputmode}
- min={this.min}
- max={this.max}
- minLength={this.minlength}
- maxLength={this.maxlength}
- multiple={this.multiple}
- name={this.name}
- pattern={this.pattern}
- placeholder={this.placeholder || ''}
- readOnly={readonly}
- required={this.required}
- spellcheck={this.spellcheck}
- step={this.step}
- type={this.type}
- value={value}
- onInput={this.onInput}
- onChange={this.onChange}
- onBlur={this.onBlur}
- onFocus={this.onFocus}
- onKeyDown={this.onKeydown}
- onCompositionstart={this.onCompositionStart}
- onCompositionend={this.onCompositionEnd}
- aria-describedby={this.getHintTextID()}
- aria-invalid={this.isInvalid ? 'true' : undefined}
- aria-labelledby={this.getLabelledById()}
- {...this.inheritedAttributes}
- />
- {this.clearInput && !readonly && !disabled && (
- {
- /**
- * This prevents mobile browsers from
- * blurring the input when the clear
- * button is activated.
- */
- ev.preventDefault();
- }}
- onClick={this.clearTextInput}
- >
-
-
- )}
+
+
+ {this.renderLabelContainer()}
+
+ (this.nativeInput = input)}
+ id={inputId}
+ disabled={disabled}
+ autoCapitalize={this.autocapitalize}
+ autoComplete={this.autocomplete}
+ autoCorrect={this.autocorrect ? 'on' : 'off'}
+ autoFocus={this.autofocus}
+ enterKeyHint={this.enterkeyhint}
+ inputMode={this.inputmode}
+ min={this.min}
+ max={this.max}
+ minLength={this.minlength}
+ maxLength={this.maxlength}
+ multiple={this.multiple}
+ name={this.name}
+ pattern={this.pattern}
+ placeholder={this.placeholder || ''}
+ readOnly={readonly}
+ required={this.required}
+ spellcheck={this.spellcheck}
+ step={this.step}
+ type={this.type}
+ value={value}
+ onInput={this.onInput}
+ onChange={this.onChange}
+ onBlur={this.onBlur}
+ onFocus={this.onFocus}
+ onKeyDown={this.onKeydown}
+ onCompositionstart={this.onCompositionStart}
+ onCompositionend={this.onCompositionEnd}
+ aria-describedby={this.getHintTextID()}
+ aria-invalid={this.isInvalid ? 'true' : undefined}
+ aria-labelledby={this.getLabelledById()}
+ {...this.inheritedAttributes}
+ />
+ {this.clearInput && !readonly && !disabled && (
+ {
+ /**
+ * This prevents mobile browsers from
+ * blurring the input when the clear
+ * button is activated.
+ */
+ ev.preventDefault();
+ }}
+ onClick={this.clearTextInput}
+ >
+
+
+ )}
+
+
+
{shouldRenderHighlight &&
}
From 5e9872647843b5c7d45a7d897e179971e39d5b2e Mon Sep 17 00:00:00 2001
From: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
Date: Thu, 30 Jul 2026 12:14:34 -0400
Subject: [PATCH 02/12] test(input): update slot test
---
.../src/components/input/test/slot/index.html | 34 ++++++++++++-------
.../components/input/test/slot/input.e2e.ts | 22 ++++++++++--
2 files changed, 40 insertions(+), 16 deletions(-)
diff --git a/core/src/components/input/test/slot/index.html b/core/src/components/input/test/slot/index.html
index 8482f938f6a..9547fa3bdd8 100644
--- a/core/src/components/input/test/slot/index.html
+++ b/core/src/components/input/test/slot/index.html
@@ -53,42 +53,42 @@
Label Slot
No Fill / Start Label
-
+
Email *
Solid / Start Label
-
+
Email *
Outline / Start Label
-
+
Email *
No Fill / Floating Label
-
+
Email *
Solid / Floating Label
-
+
Email *
Outline / Floating Label
-
+
Email *
@@ -98,7 +98,7 @@
Start/End Slots
No Fill / Start Label
-
+
@@ -108,7 +108,7 @@ No Fill / Start Label
Solid / Start Label
-
+
@@ -118,7 +118,7 @@ Solid / Start Label
Outline / Start Label
-
+
@@ -128,7 +128,7 @@ Outline / Start Label
No Fill / Floating Label
-
+
@@ -138,7 +138,7 @@ No Fill / Floating Label
Solid / Floating Label
-
+
@@ -148,7 +148,7 @@ Solid / Floating Label
Outline / Floating Label
-
+
@@ -161,7 +161,7 @@ Async Slot Content
Outline / Async Label
-
+
@@ -173,6 +173,7 @@
Outline / Async Decorations
Add Slotted Content
Update Slotted Content
Remove Slotted Content
+ Add Value to All
@@ -242,6 +243,13 @@ Outline / Async Decorations
slottedContent.textContent = 'This is my really really really long text';
}
};
+
+ const addValueToAll = () => {
+ const inputs = document.querySelectorAll('ion-input');
+ inputs.forEach((input) => {
+ input.value = 'hi@ionic.io';
+ });
+ };