diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts index 19948ca2d99..2fc119daa93 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -856,14 +856,14 @@ where the user's interaction is typing. @ProxyCmp({ defineCustomElementFn: undefined, - inputs: ['accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'color', 'debounce', 'disabled', 'enterkeyhint', 'inputmode', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'size', 'spellcheck', 'step', 'type', 'value'], + inputs: ['accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'size', 'spellcheck', 'step', 'type', 'value'], methods: ['setFocus', 'getInputElement'] }) @Component({ selector: 'ion-input', changeDetection: ChangeDetectionStrategy.OnPush, template: '', - inputs: ['accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'color', 'debounce', 'disabled', 'enterkeyhint', 'inputmode', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'size', 'spellcheck', 'step', 'type', 'value'] + inputs: ['accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'size', 'spellcheck', 'step', 'type', 'value'] }) export class IonInput { protected el: HTMLElement; diff --git a/core/api.txt b/core/api.txt index dc6419b9d50..36037a08324 100644 --- a/core/api.txt +++ b/core/api.txt @@ -533,10 +533,17 @@ ion-input,prop,autofocus,boolean,false,false,false ion-input,prop,clearInput,boolean,false,false,false ion-input,prop,clearOnEdit,boolean | undefined,undefined,false,false ion-input,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record | undefined,undefined,false,true +ion-input,prop,counter,boolean,false,false,false +ion-input,prop,counterFormatter,((inputLength: number, maxLength: number) => string) | undefined,undefined,false,false ion-input,prop,debounce,number | undefined,undefined,false,false ion-input,prop,disabled,boolean,false,false,false ion-input,prop,enterkeyhint,"done" | "enter" | "go" | "next" | "previous" | "search" | "send" | undefined,undefined,false,false +ion-input,prop,errorText,string | undefined,undefined,false,false +ion-input,prop,fill,"outline" | "solid" | undefined,undefined,false,false +ion-input,prop,helperText,string | undefined,undefined,false,false ion-input,prop,inputmode,"decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined,undefined,false,false +ion-input,prop,label,string | undefined,undefined,false,false +ion-input,prop,labelPlacement,"end" | "fixed" | "floating" | "stacked" | "start",'start',false,false ion-input,prop,max,number | string | undefined,undefined,false,false ion-input,prop,maxlength,number | undefined,undefined,false,false ion-input,prop,min,number | string | undefined,undefined,false,false @@ -548,6 +555,7 @@ ion-input,prop,pattern,string | undefined,undefined,false,false ion-input,prop,placeholder,string | undefined,undefined,false,false ion-input,prop,readonly,boolean,false,false,false ion-input,prop,required,boolean,false,false,false +ion-input,prop,shape,"round" | undefined,undefined,false,false ion-input,prop,size,number | undefined,undefined,false,false ion-input,prop,spellcheck,boolean,false,false,false ion-input,prop,step,string | undefined,undefined,false,false @@ -560,7 +568,14 @@ ion-input,event,ionChange,InputChangeEventDetail,true ion-input,event,ionFocus,FocusEvent,true ion-input,event,ionInput,InputInputEventDetail,true ion-input,css-prop,--background +ion-input,css-prop,--border-color +ion-input,css-prop,--border-radius +ion-input,css-prop,--border-style +ion-input,css-prop,--border-width ion-input,css-prop,--color +ion-input,css-prop,--highlight-color-focused +ion-input,css-prop,--highlight-color-invalid +ion-input,css-prop,--highlight-color-valid ion-input,css-prop,--padding-bottom ion-input,css-prop,--padding-end ion-input,css-prop,--padding-start diff --git a/core/src/components.d.ts b/core/src/components.d.ts index a2c2fdf2ab5..f25561a3066 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -1083,6 +1083,14 @@ export namespace Components { * The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). */ "color"?: Color; + /** + * If `true`, a character counter will display the ratio of characters used and the total character limit. Developers must also set the `maxlength` property for the counter to be calculated correctly. + */ + "counter": boolean; + /** + * A callback used to format the counter text. By default the counter text is set to "itemLength / maxLength". + */ + "counterFormatter"?: (inputLength: number, maxLength: number) => string; /** * Set the amount of time, in milliseconds, to wait to trigger the `ionInput` event after each keystroke. */ @@ -1095,14 +1103,34 @@ export namespace Components { * A hint to the browser for which enter key to display. Possible values: `"enter"`, `"done"`, `"go"`, `"next"`, `"previous"`, `"search"`, and `"send"`. */ "enterkeyhint"?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'; + /** + * Text that is placed under the input and displayed when an error is detected. + */ + "errorText"?: string; + /** + * The fill for the item. If `'solid'` the item will have a background. If `'outline'` the item will be transparent with a border. Only available in `md` mode. + */ + "fill"?: 'outline' | 'solid'; /** * Returns the native `` element used under the hood. */ "getInputElement": () => Promise; + /** + * Text that is placed under the input and displayed when no error is detected. + */ + "helperText"?: string; /** * A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`. */ "inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'; + /** + * The visible label associated with the input. + */ + "label"?: string; + /** + * Where to place the label relative to the input. `'start'`: The label will appear to the left of the input in LTR and to the right in RTL. `'end'`: The label will appear to the right of the input in LTR and to the left in RTL. `'floating'`: The label will appear smaller and above the input when the input is focused or it has a value. Otherwise it will appear on top of the input. `'stacked'`: The label will appear smaller and above the input regardless even when the input is blurred or has no value. `'fixed'`: The label has the same behavior as `'start'` except it also has a fixed width. Long text will be truncated with ellipses ("..."). + */ + "labelPlacement": 'start' | 'end' | 'floating' | 'stacked' | 'fixed'; /** * The maximum value, which must not be less than its minimum (min attribute) value. */ @@ -1151,6 +1179,10 @@ export namespace Components { * Sets focus on the native `input` in `ion-input`. Use this method instead of the global `input.focus()`. */ "setFocus": () => Promise; + /** + * The shape of the input. If "round" it will have an increased border radius. + */ + "shape"?: 'round'; /** * The initial size of the control. This value is in pixels unless the value of the type attribute is `"text"` or `"password"`, in which case it is an integer number of characters. This attribute applies only when the `type` attribute is set to `"text"`, `"search"`, `"tel"`, `"url"`, `"email"`, or `"password"`, otherwise it is ignored. */ @@ -1183,10 +1215,12 @@ export namespace Components { "color"?: Color; /** * If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`. + * @deprecated Use the `counter` property on `ion-input` or `ion-textarea` instead. */ "counter": boolean; /** * A callback used to format the counter text. By default the counter text is set to "itemLength / maxLength". + * @deprecated Use the `counterFormatter` property on `ion-input` or `ion-textarea` instead. */ "counterFormatter"?: CounterFormatter; /** @@ -4925,6 +4959,14 @@ declare namespace LocalJSX { * The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). */ "color"?: Color; + /** + * If `true`, a character counter will display the ratio of characters used and the total character limit. Developers must also set the `maxlength` property for the counter to be calculated correctly. + */ + "counter"?: boolean; + /** + * A callback used to format the counter text. By default the counter text is set to "itemLength / maxLength". + */ + "counterFormatter"?: (inputLength: number, maxLength: number) => string; /** * Set the amount of time, in milliseconds, to wait to trigger the `ionInput` event after each keystroke. */ @@ -4937,10 +4979,30 @@ declare namespace LocalJSX { * A hint to the browser for which enter key to display. Possible values: `"enter"`, `"done"`, `"go"`, `"next"`, `"previous"`, `"search"`, and `"send"`. */ "enterkeyhint"?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'; + /** + * Text that is placed under the input and displayed when an error is detected. + */ + "errorText"?: string; + /** + * The fill for the item. If `'solid'` the item will have a background. If `'outline'` the item will be transparent with a border. Only available in `md` mode. + */ + "fill"?: 'outline' | 'solid'; + /** + * Text that is placed under the input and displayed when no error is detected. + */ + "helperText"?: string; /** * A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`. */ "inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'; + /** + * The visible label associated with the input. + */ + "label"?: string; + /** + * Where to place the label relative to the input. `'start'`: The label will appear to the left of the input in LTR and to the right in RTL. `'end'`: The label will appear to the right of the input in LTR and to the left in RTL. `'floating'`: The label will appear smaller and above the input when the input is focused or it has a value. Otherwise it will appear on top of the input. `'stacked'`: The label will appear smaller and above the input regardless even when the input is blurred or has no value. `'fixed'`: The label has the same behavior as `'start'` except it also has a fixed width. Long text will be truncated with ellipses ("..."). + */ + "labelPlacement"?: 'start' | 'end' | 'floating' | 'stacked' | 'fixed'; /** * The maximum value, which must not be less than its minimum (min attribute) value. */ @@ -5005,6 +5067,10 @@ declare namespace LocalJSX { * If `true`, the user must fill in a value before submitting a form. */ "required"?: boolean; + /** + * The shape of the input. If "round" it will have an increased border radius. + */ + "shape"?: 'round'; /** * The initial size of the control. This value is in pixels unless the value of the type attribute is `"text"` or `"password"`, in which case it is an integer number of characters. This attribute applies only when the `type` attribute is set to `"text"`, `"search"`, `"tel"`, `"url"`, `"email"`, or `"password"`, otherwise it is ignored. */ @@ -5037,10 +5103,12 @@ declare namespace LocalJSX { "color"?: Color; /** * If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`. + * @deprecated Use the `counter` property on `ion-input` or `ion-textarea` instead. */ "counter"?: boolean; /** * A callback used to format the counter text. By default the counter text is set to "itemLength / maxLength". + * @deprecated Use the `counterFormatter` property on `ion-input` or `ion-textarea` instead. */ "counterFormatter"?: CounterFormatter; /** diff --git a/core/src/components/input/input.ios.scss b/core/src/components/input/input.ios.scss index 013f1e385bc..314a8f46630 100644 --- a/core/src/components/input/input.ios.scss +++ b/core/src/components/input/input.ios.scss @@ -2,12 +2,18 @@ @import "./input.ios.vars"; :host { + --border-width: #{$hairlines-width}; + --border-color: #{$item-ios-border-color}; + + font-size: $input-ios-font-size; +} + +// TODO FW-2764 Remove this +:host(.legacy-input) { --padding-top: #{$input-ios-padding-top}; --padding-end: #{$input-ios-padding-end}; --padding-bottom: #{$input-ios-padding-bottom}; --padding-start: #{$input-ios-padding-start}; - - font-size: $input-ios-font-size; } :host-context(.item-label-stacked), @@ -25,3 +31,20 @@ background-size: $input-ios-input-clear-icon-size; } + +// Input Wrapper +// ---------------------------------------------------------------- + +.input-wrapper { + min-height: 44px; +} + +/** + * Since the label sits on top of the element, + * the component needs to be taller otherwise the + * label will appear too close to the input text. + */ +:host(.input-label-placement-floating) .input-wrapper, +:host(.input-label-placement-stacked) .input-wrapper { + min-height: 56px; +} diff --git a/core/src/components/input/input.md.outline.scss b/core/src/components/input/input.md.outline.scss new file mode 100644 index 00000000000..f6465c846bc --- /dev/null +++ b/core/src/components/input/input.md.outline.scss @@ -0,0 +1,213 @@ +@import "./input.vars"; + +// Input Fill: Outline +// ---------------------------------------------------------------- + +:host(.input-fill-outline) { + --border-color: #{$background-color-step-300}; + --border-radius: 4px; + --padding-start: 16px; + --padding-end: 16px; +} + +:host(.input-fill-outline.input-shape-round) { + --border-radius: 28px; + --padding-start: 32px; + --padding-end: 32px; +} + +/** + * If the input has a validity state, the + * border should reflect that as a color. + */ +:host(.input-fill-outline.ion-touched.ion-valid), +:host(.input-fill-outline.ion-touched.ion-invalid) { + --border-color: var(--highlight-color); +} + +/** + * Border should be + * slightly darker on hover. + */ +@media (any-hover: hover) { + :host(.input-fill-outline:hover) { + --border-color: #{$background-color-step-750}; + } +} + +/** + * The border should get thicker + * and take on component color when + * the input is focused. + */ +:host(.input-fill-outline.has-focus) { + --border-width: 2px; + --border-color: var(--highlight-color); +} + +/** + * The bottom content should never have + * a border with the outline style. + */ +:host(.input-fill-outline) .input-bottom { + border-top: none; +} + +/** + * Outline inputs do not have a bottom border. + * Instead, they have a border that wraps the + * input + label. + */ +:host(.input-fill-outline) .input-wrapper { + border-bottom: none; +} + +:host(.input-fill-outline.input-label-placement-stacked) .label-text-wrapper, +:host(.input-fill-outline.input-label-placement-floating) .label-text-wrapper { + @include transform-origin(start, top); + + position: absolute; + + /** + * Label text should not extend + * beyond the bounds of the input. + */ + max-width: calc(100% - var(--padding-start) - var(--padding-end)); +} + +/** + * The label should appear on top of an outline + * container that overlaps it so it is always clickable. + */ +:host(.input-fill-outline) .label-text-wrapper, +:host(.input-fill-outline) .label-text-wrapper { + position: relative; + + z-index: 1; +} + +/** + * 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 { + @include transform(translateY(-32%), scale(#{$input-floating-label-scale})); + @include margin(0); + + /** + * Label text should not extend + * beyond the bounds of the input. + */ + max-width: calc((100% - var(--padding-start) - var(--padding-end) - #{$input-md-floating-label-padding * 2}) / #{$input-floating-label-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 +// ---------------------------------------------------------------- + +:host(.input-fill-outline) .input-outline-container { + @include position(0, 0, 0, 0); + + display: flex; + + position: absolute; + + width: 100%; + height: 100%; +} + +:host(.input-fill-outline) .input-outline-start, +:host(.input-fill-outline) .input-outline-end { + pointer-events: none; +} + +/** + * By default, each piece of the container should have + * a top and bottom border. This gives the appearance + * of a unified container with a border. + */ +:host(.input-fill-outline) .input-outline-start, +:host(.input-fill-outline) .input-outline-notch, +:host(.input-fill-outline) .input-outline-end { + border-top: var(--border-width) var(--border-style) var(--border-color); + border-bottom: var(--border-width) var(--border-style) var(--border-color); +} + +/** + * Ensures long labels do not cause the notch to flow + * out of bounds. + */ +:host(.input-fill-outline) .input-outline-notch { + max-width: calc(100% - var(--padding-start) - var(--padding-end)); +} + +/** + * This element ensures that the notch used + * the size of the scaled text so that the + * border cut out is the correct width. + * The text in this element should not + * be interactive. + */ +:host(.input-fill-outline) .notch-spacer { + /** + * We need $input-md-floating-label-padding of padding on the right. + * However, we also subtracted $input-md-floating-label-padding from + * the width of .input-outline-start + * to create space, so we need to take + * that into consideration here. + */ + @include padding(null, #{$input-md-floating-label-padding * 2}, null, null); + + font-size: calc(1em * #{$input-floating-label-scale}); + + opacity: 0; + pointer-events: none; +} + +:host(.input-fill-outline) .input-outline-start { + @include border-radius(var(--border-radius), 0px, 0px, var(--border-radius)); + @include border(null, null, null, var(--border-width) var(--border-style) var(--border-color)); + + /** + * There should be spacing between the translated text + * and .input-outline-start. However, we can't add this + * spacing onto the notch because it would cause the + * label to look like it is not aligned with the + * text input. Instead, we subtract a few pixels from + * this element. + */ + width: calc(var(--padding-start) - #{$input-md-floating-label-padding}); +} + +:host(.input-fill-outline) .input-outline-end { + @include border(null, var(--border-width) var(--border-style) var(--border-color), null, null); + @include border-radius(0px, var(--border-radius), var(--border-radius), 0px); + + /** + * The ending outline fragment + * should take up the remaining free space. + */ + flex-grow: 1; +} + +/** + * When the input either has focus or a value, + * there should be a "cut out" at the top for + * 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 { + border-top: none; +} diff --git a/core/src/components/input/input.md.scss b/core/src/components/input/input.md.scss index 51bc9ce6cb1..b604cbfa202 100644 --- a/core/src/components/input/input.md.scss +++ b/core/src/components/input/input.md.scss @@ -1,17 +1,25 @@ @import "./input"; @import "./input.md.vars"; - +@import "./input.md.solid.scss"; +@import "./input.md.outline.scss"; +@import "../item/item.md.vars.scss"; // Material Design Input // -------------------------------------------------- :host { + --border-width: 1px; + --border-color: #{$item-md-border-color}; + + font-size: $input-md-font-size; +} + +// TODO FW-2764 Remove this +:host(.legacy-input) { --padding-top: #{$input-md-padding-top}; --padding-end: #{$input-md-padding-end}; --padding-bottom: #{$input-md-padding-bottom}; --padding-start: #{$input-md-padding-start}; - - font-size: $input-md-font-size; } :host-context(.item-label-stacked), @@ -29,3 +37,80 @@ background-size: $input-md-input-clear-icon-size; } + +/** + * The input with no fill in item should + * have start padding applied on the input + * not the item. + */ +:host(.in-full-item:not(.input-fill-outline)), +:host(.in-full-item:not(.input-fill-solid)) { + --padding-start: #{$item-md-padding-start}; +} + +// Input Bottom +// ---------------------------------------------------------------- +/** + * If the input has a validity state, the + * border and label should reflect that as a color. + */ +:host(.ion-touched.ion-valid) .input-bottom, +:host(.ion-touched.ion-invalid) .input-bottom { + --border-color: var(--highlight-color); +} + +.input-bottom .counter { + letter-spacing: .0333333333em; +} + +// Input Wrapper +// ---------------------------------------------------------------- + +.input-wrapper { + min-height: 56px; +} + +// Input Label +// ---------------------------------------------------------------- + +/** + * When the input is focused the label should + * take on the highlight color. + */ +:host(.has-focus) .label-text-wrapper { + color: var(--highlight-color); +} + +:host(.ion-touched.ion-valid) .label-text-wrapper, +:host(.ion-touched.ion-invalid) .label-text-wrapper { + color: var(--highlight-color); +} + +// Input Highlight +// ---------------------------------------------------------------- + +.input-highlight { + @include position(null, null, -1px, 0); + + position: absolute; + + width: 100%; + height: 2px; + + transform: scale(0); + + transition: transform 200ms; + + background: var(--highlight-color); +} + +:host(.has-focus) .input-highlight { + transform: scale(1); +} + +// Input Shape Rounded +// ---------------------------------------------------------------- + +:host(.input-shape-round) { + --border-radius: 16px; +} diff --git a/core/src/components/input/input.md.solid.scss b/core/src/components/input/input.md.solid.scss new file mode 100644 index 00000000000..d7ee9c7da43 --- /dev/null +++ b/core/src/components/input/input.md.solid.scss @@ -0,0 +1,76 @@ +@import "./input.vars"; + +// Input Fill: Solid +// ---------------------------------------------------------------- + +:host(.input-fill-solid) { + --background: #{$background-color-step-50}; + --border-color: #{$background-color-step-500}; + --border-radius: 4px; + --padding-start: 16px; + --padding-end: 16px; +} + +/** + * The solid fill style has a border + * at the bottom of the input wrapper. + * As a result, the border on the "bottom + * content" is not needed. + */ +:host(.input-fill-solid) .input-wrapper { + border-bottom: var(--border-width) var(--border-style) var(--border-color); +} + +/** + * If the input has a validity state, the + * border should reflect that as a color. + */ +:host(.input-fill-solid.ion-touched.ion-valid) .input-wrapper, +:host(.input-fill-solid.ion-touched.ion-invalid) .input-wrapper { + --border-color: var(--highlight-color); +} + +:host(.input-fill-solid) .input-bottom { + border-top: none; +} + +/** + * Background and border should be + * slightly darker on hover. + */ +@media (any-hover: hover) { + :host(.input-fill-solid:hover) { + --background: #{$background-color-step-100}; + --border-color: #{$background-color-step-750}; + } +} + +/** + * Background and border should be + * much darker on focus. + */ +:host(.input-fill-solid.has-focus) { + --background: #{$background-color-step-150}; + --border-color: #{$background-color-step-750}; +} + +:host(.input-fill-solid) .input-wrapper { + /** + * Only the top left and top right borders should. + * have a radius when using a solid fill. + */ + @include border-radius(var(--border-radius), var(--border-radius), 0px, 0px); +} + +// 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 { + /** + * Label text should not extend + * beyond the bounds of the input. + */ + max-width: calc(100% / #{$input-floating-label-scale}); +} diff --git a/core/src/components/input/input.md.vars.scss b/core/src/components/input/input.md.vars.scss index b04a35040ff..42bbb195626 100644 --- a/core/src/components/input/input.md.vars.scss +++ b/core/src/components/input/input.md.vars.scss @@ -75,3 +75,6 @@ $input-md-inset-margin-bottom: ($item-md-padding-bottom * 0.5) !defau /// @prop - Margin start of the inset input $input-md-inset-margin-start: $item-md-padding-start !default; + +/// @prop - The amount of whitespace to display on either side of the floating label +$input-md-floating-label-padding: 4px !default; diff --git a/core/src/components/input/input.scss b/core/src/components/input/input.scss index b08914f1b1d..d8338c0fb5f 100644 --- a/core/src/components/input/input.scss +++ b/core/src/components/input/input.scss @@ -18,30 +18,48 @@ * @prop --placeholder-font-style: Font style of the input placeholder text * @prop --placeholder-font-weight: Font weight of the input placeholder text * @prop --placeholder-opacity: Opacity of the input placeholder text + * + * @prop --highlight-color-focused: The color of the highlight on the input when focused + * @prop --highlight-color-valid: The color of the highlight on the input when valid + * @prop --highlight-color-invalid: The color of the highlight on the input when invalid + * + * @prop --border-color: Color of the border below the input when using helper text, error text, or counter + * @prop --border-radius: Radius of the input + * @prop --border-style: Color of the border below the input when using helper text, error text, or counter + * @prop --border-width: Color of the border below the input when using helper text, error text, or counter */ --placeholder-color: initial; --placeholder-font-style: initial; --placeholder-font-weight: initial; --placeholder-opacity: .5; - --padding-top: 0; - --padding-end: 0; - --padding-bottom: 0; - --padding-start: 0; + --padding-top: 0px; + --padding-end: 0px; + --padding-bottom: 0px; + --padding-start: 0px; --background: transparent; --color: initial; + --border-style: solid; + --highlight-color-focused: #{ion-color(primary, base)}; + --highlight-color-valid: #{ion-color(success, base)}; + --highlight-color-invalid: #{ion-color(danger, base)}; - display: flex; - position: relative; + /** + * This is a private API that is used to switch + * out the highlight color based on the state + * of the component without having to write + * different selectors for different fill variants. + */ + --highlight-color: var(--highlight-color-focused); - flex: 1; - align-items: center; + display: block; + + position: relative; width: 100%; /* stylelint-disable-next-line all */ padding: 0 !important; - background: var(--background); color: var(--color); font-family: $font-family-base; @@ -49,24 +67,46 @@ z-index: $z-index-item-input; } -:host-context(ion-item:not(.item-label)) { +// TODO FW-2764 Remove this +:host(.legacy-input) { + display: flex; + + flex: 1; + align-items: center; + + background: var(--background); +} + +// TODO FW-2764 Remove this +:host(.legacy-input) .native-input { + @include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start)); + @include border-radius(var(--border-radius)); +} + +:host-context(ion-item:not(.item-label):not(.item-has-modern-input)) { --padding-start: 0; } -:host(.ion-color) { +// TODO FW-2764 Remove this +:host(.legacy-input.ion-color) { color: current-color(base); } +:host(.ion-color) { + --highlight-color-focused: #{current-color(base)}; +} + // Native Text Input // -------------------------------------------------- .native-input { - @include border-radius(var(--border-radius)); - @include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start)); + @include padding(0, 0, 0, 0); @include text-inherit(); display: inline-block; + position: relative; + flex: 1; width: 100%; @@ -82,6 +122,17 @@ box-sizing: border-box; appearance: none; + /** + * This ensures the input + * remains on top of any decoration + * that we render (particularly the + * outline border when fill="outline"). + * If we did not do this then Axe would + * be unable to determine the color + * contrast of the input. + */ + z-index: 1; + &::placeholder { color: var(--placeholder-color); @@ -105,10 +156,11 @@ } } -.native-input[disabled] { - opacity: .4; -} +:host(.legacy-input) .native-input[disabled], +:host([disabled]) { + opacity: 0.4; +} // Input Cover: Unfocused @@ -131,8 +183,13 @@ // Clear Input Icon // -------------------------------------------------- -.input-clear-icon { +// TODO FW-2764 Remove this +:host(.legacy-input) .input-clear-icon { @include margin(0); +} + +.input-clear-icon { + @include margin(auto); @include padding(0); @include background-position(center); @@ -191,3 +248,344 @@ opacity: 1; } + +// Input Wrapper +// ---------------------------------------------------------------- + +.input-wrapper { + @include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start)); + @include border-radius(var(--border-radius)); + + display: flex; + + position: relative; + + flex-grow: 1; + + align-items: stretch; + + height: 100%; + + transition: background-color 15ms linear; + + background: var(--background); +} + +// Input Native Wrapper +// ---------------------------------------------------------------- + +.native-wrapper { + display: flex; + + flex-grow: 1; + + width: 100%; +} + +// Input Highlight +// ---------------------------------------------------------------- + +:host(.ion-touched.ion-invalid) { + --highlight-color: var(--highlight-color-invalid); +} + +:host(.ion-touched.ion-valid) { + --highlight-color: var(--highlight-color-valid); +} + +// Input Bottom Content +// ---------------------------------------------------------------- + +.input-bottom { + /** + * The bottom content should take on the start and end + * padding so it is always aligned with either the label + * or the start of the text input. + */ + @include padding(5px, var(--padding-end), 0, var(--padding-start)); + + display: flex; + + justify-content: space-between; + + border-top: var(--border-width) var(--border-style) var(--border-color); + + font-size: 12px; +} + +/** + * If the input has a validity state, the + * border and label should reflect that as a color. + */ +:host(.ion-touched.ion-valid) .input-bottom, +:host(.ion-touched.ion-invalid) .input-bottom { + --border-color: var(--highlight-color); +} + +:host(.ion-touched.ion-valid) label, +:host(.ion-touched.ion-invalid) label { + color: var(--highlight-color); +} + +// Input Hint Text +// ---------------------------------------------------------------- + +/** + * Error text should only be shown when .ion-invalid is + * present on the input. Otherwise the helper text should + * be shown. + */ +.input-bottom .error-text { + display: none; + + color: var(--highlight-color-invalid); +} + +.input-bottom .helper-text { + display: block; +} + +:host(.ion-invalid) .input-bottom .error-text { + display: block; +} + +:host(.ion-invalid) .input-bottom .helper-text { + display: none; +} + +// Input Max Length Counter +// ---------------------------------------------------------------- + +.input-bottom .counter { + /** + * Counter should always be at + * the end of the container even + * when no helper/error texts are used. + */ + @include margin-horizontal(auto, null); + + color: #{$background-color-step-550}; + + white-space: nowrap; + + padding-inline-start: 16px; +} + +// Input Highlight +// ---------------------------------------------------------------- + +:host(.ion-touched.ion-invalid) { + --highlight-color: var(--highlight-color-invalid); +} + +:host(.ion-touched.ion-valid) { + --highlight-color: var(--highlight-color-valid); +} + +// Input Native +// ---------------------------------------------------------------- + +:host(.has-focus) input { + caret-color: var(--highlight-color); +} + +// Input Label +// ---------------------------------------------------------------- + +.label-text-wrapper { + /** + * This causes the label to take up + * the entire height of its container + * while still keeping the text centered. + */ + display: flex; + + align-items: center; + + /** + * Label text should not extend + * beyond the bounds of the input. + * However, we do not set the max + * width to 100% because then + * only the label would show and users + * would not be able to see what they are typing. + */ + max-width: 200px; + + transition: color 150ms cubic-bezier(.4, 0, .2, 1), transform 150ms cubic-bezier(.4, 0, .2, 1); + + /** + * This ensures that double tapping this text + * clicks the