Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 2 additions & 11 deletions core/src/components/input/input.md.outline.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
--border-radius: 4px;
--padding-start: 16px;
--padding-end: 16px;
--start-slot-adjustment: 0px;

min-height: 56px;
}
Expand Down Expand Up @@ -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);

/**
Expand All @@ -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
// ----------------------------------------------------------------

Expand Down
48 changes: 31 additions & 17 deletions core/src/components/input/input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@
// --------------------------------------------------

.input-clear-icon {
@include margin(auto);
@include padding(0);
@include background-position(center);

Expand Down Expand Up @@ -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
// ----------------------------------------------------------------

Expand All @@ -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
// ----------------------------------------------------------------

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
Expand All @@ -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.
Expand Down
184 changes: 91 additions & 93 deletions core/src/components/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the icon for Material Design per the spec:

Image


import { getIonMode } from '../../global/ionic-global';
import type { AutocompleteTypes, Color, TextFieldTypes } from '../../interface';
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 [
<div class="input-outline-container">
<div class="input-outline-start"></div>
<div
class={{
'input-outline-notch': true,
'input-outline-notch-hidden': !this.hasLabel,
}}
>
<div class="notch-spacer" aria-hidden="true" ref={(el) => (this.notchSpacerEl = el)}>
{this.label}
</div>
</div>
<div class="input-outline-end"></div>
</div>,
this.renderLabel(),
];
}
private renderOutlineDecorations() {
return [
<div class="input-outline-start"></div>,
<div
class={{
'input-outline-notch': true,
'input-outline-notch-hidden': !this.hasLabel,
}}
>
<div class="notch-spacer" aria-hidden="true" ref={(el) => (this.notchSpacerEl = el)}>
{this.label}
</div>
</div>,
<div class="input-outline-end"></div>,
];
}

private renderLabelContainer() {
/**
* If not using the outline style,
* we can render just the label.
Expand All @@ -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 (
<Host
Expand All @@ -895,6 +885,7 @@ export class Input implements ComponentInterface {
'in-item-color': hostContext('ion-item.ion-color', this.el),
'input-disabled': disabled,
})}
style={{ '--start-slot-adjustment': this.getStartSlotAdjustment() }}
>
{/**
* htmlFor is needed so that clicking the label always focuses
Expand All @@ -903,46 +894,53 @@ export class Input implements ComponentInterface {
* since it comes before the input in the DOM.
*/}
<label class="input-wrapper" htmlFor={inputId} onClick={this.onLabelClick}>
{this.renderLabelContainer()}
<div class="native-wrapper" onClick={this.onLabelClick}>
{hasOutlineFill && <div class="input-outline-container">{this.renderOutlineDecorations()}</div>}
<div class="input-start">
<slot name="start"></slot>
<input
class="native-input"
ref={(input) => (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}
/>
</div>
<div class="input-control">
{this.renderLabelContainer()}
<div class="native-wrapper" onClick={this.onLabelClick}>
<input
class="native-input"
ref={(input) => (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}
/>
</div>
</div>
<div class="input-end">
{this.clearInput && !readonly && !disabled && (
<button
aria-label="reset"
Expand Down

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This 1px shift was caused by the removal of this rule:

/**
* 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);
}

However, this is actually more aligned than before:

Image

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This 1px shift was caused by the removal of this rule:

/**
* 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);
}

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This 1px shift was caused by the removal of this rule:

/**
* 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);
}

Loading
Loading