feat(input): add ionic theme styles and size property (#29380)
Issue number: internal --------- ## What is the current behavior? The input uses `md` styles on the `ionic` theme. ## What is the new behavior? Adds the following for the `ionic` theme: - The `--text-color-invalid` CSS variable - The `size` property with support for the `large` size - The `outline` fill styles for the input - The `round` shape styles for the input - The helper and error text styles - The `focused`, `disabled` and `hover` styles - The clear button styles ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information The outline fill & stacked label were the main priority here so some screenshots may look plain. Floating label will be added in future work.
@ -910,7 +910,7 @@ 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,labelPlacement,"end" | "fixed" | "floating" | "stacked" | "start" | undefined,undefined,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
|
||||
@ -922,7 +922,8 @@ 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,true
|
||||
ion-input,prop,required,boolean,false,false,false
|
||||
ion-input,prop,shape,"round" | undefined,undefined,false,false
|
||||
ion-input,prop,shape,"rectangular" | "round" | "soft" | undefined,undefined,false,false
|
||||
ion-input,prop,size,"large" | "medium" | "xlarge" | undefined,'medium',false,false
|
||||
ion-input,prop,spellcheck,boolean,false,false,false
|
||||
ion-input,prop,step,string | undefined,undefined,false,false
|
||||
ion-input,prop,theme,"ios" | "md" | "ionic",undefined,false,false
|
||||
@ -988,6 +989,9 @@ ion-input,css-prop,--placeholder-font-weight,md
|
||||
ion-input,css-prop,--placeholder-opacity,ionic
|
||||
ion-input,css-prop,--placeholder-opacity,ios
|
||||
ion-input,css-prop,--placeholder-opacity,md
|
||||
ion-input,css-prop,--text-color-invalid,ionic
|
||||
ion-input,css-prop,--text-color-invalid,ios
|
||||
ion-input,css-prop,--text-color-invalid,md
|
||||
|
||||
ion-input-password-toggle,shadow
|
||||
ion-input-password-toggle,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
|
||||
|
||||
22
core/src/components.d.ts
vendored
@ -1435,9 +1435,9 @@ export namespace Components {
|
||||
*/
|
||||
"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 ("...").
|
||||
* 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 ("..."). Defaults to "stacked" for the ionic theme, or "start" for all other themes. In the ionic theme, only the values "stacked" and "floating" are supported.
|
||||
*/
|
||||
"labelPlacement": 'start' | 'end' | 'floating' | 'stacked' | 'fixed';
|
||||
"labelPlacement"?: 'start' | 'end' | 'floating' | 'stacked' | 'fixed';
|
||||
/**
|
||||
* The maximum value, which must not be less than its minimum (min attribute) value.
|
||||
*/
|
||||
@ -1487,9 +1487,13 @@ export namespace Components {
|
||||
*/
|
||||
"setFocus": () => Promise<void>;
|
||||
/**
|
||||
* The shape of the input. If "round" it will have an increased border radius.
|
||||
* Set to `"soft"` for an input with slightly rounded corners, `"round"` for an input with fully rounded corners, or `"rectangular"` for an input without rounded corners. Defaults to `"round"` for the ionic theme, and `undefined` for all other themes. Only applies when the fill is set to `"solid"` or `"outline"`.
|
||||
*/
|
||||
"shape"?: 'round';
|
||||
"shape"?: 'soft' | 'round' | 'rectangular';
|
||||
/**
|
||||
* The size of the input. If "large", it will have an increased height. By default the size is medium. This property only applies to the `"ionic"` theme.
|
||||
*/
|
||||
"size"?: 'medium' | 'large' | 'xlarge';
|
||||
/**
|
||||
* If `true`, the element will have its spelling and grammar checked.
|
||||
*/
|
||||
@ -6715,7 +6719,7 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"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 ("...").
|
||||
* 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 ("..."). Defaults to "stacked" for the ionic theme, or "start" for all other themes. In the ionic theme, only the values "stacked" and "floating" are supported.
|
||||
*/
|
||||
"labelPlacement"?: 'start' | 'end' | 'floating' | 'stacked' | 'fixed';
|
||||
/**
|
||||
@ -6779,9 +6783,13 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"required"?: boolean;
|
||||
/**
|
||||
* The shape of the input. If "round" it will have an increased border radius.
|
||||
* Set to `"soft"` for an input with slightly rounded corners, `"round"` for an input with fully rounded corners, or `"rectangular"` for an input without rounded corners. Defaults to `"round"` for the ionic theme, and `undefined` for all other themes. Only applies when the fill is set to `"solid"` or `"outline"`.
|
||||
*/
|
||||
"shape"?: 'round';
|
||||
"shape"?: 'soft' | 'round' | 'rectangular';
|
||||
/**
|
||||
* The size of the input. If "large", it will have an increased height. By default the size is medium. This property only applies to the `"ionic"` theme.
|
||||
*/
|
||||
"size"?: 'medium' | 'large' | 'xlarge';
|
||||
/**
|
||||
* If `true`, the element will have its spelling and grammar checked.
|
||||
*/
|
||||
|
||||
157
core/src/components/input/input.ionic.outline.scss
Normal file
@ -0,0 +1,157 @@
|
||||
@import "./input.vars";
|
||||
@import "../../foundations/ionic.vars";
|
||||
|
||||
// Input Fill: Outline (Ionic Theme)
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
:host(.input-fill-outline) {
|
||||
--border-radius: #{$ionic-border-radius-100};
|
||||
--padding-start: 12px;
|
||||
--padding-end: 12px;
|
||||
}
|
||||
|
||||
:host(.input-fill-outline.input-size-large) {
|
||||
--padding-start: 16px;
|
||||
--padding-end: 16px;
|
||||
}
|
||||
|
||||
:host(.input-fill-outline.input-size-xlarge) {
|
||||
--padding-start: 20px;
|
||||
--padding-end: 20px;
|
||||
}
|
||||
|
||||
/**
|
||||
* The bottom content should never have
|
||||
* a border with the outline style.
|
||||
*/
|
||||
:host(.input-fill-outline) .input-bottom {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
:host(.input-fill-outline.input-shape-round) .input-bottom,
|
||||
:host(.input-fill-outline.input-label-placement-floating) .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-horizontal(var(--padding-start), var(--padding-end));
|
||||
}
|
||||
|
||||
:host(.input-fill-outline) .input-wrapper {
|
||||
/**
|
||||
* For the ionic theme, the padding needs to sit on the
|
||||
* native wrapper instead, so that it sits within the
|
||||
* outline container but does not always affect the
|
||||
* label text.
|
||||
*/
|
||||
@include padding(0);
|
||||
|
||||
/**
|
||||
* Outline inputs do not have a bottom border.
|
||||
* Instead, they have a border that wraps the
|
||||
* input + label.
|
||||
*/
|
||||
border-bottom: none;
|
||||
|
||||
/**
|
||||
* Do not show a background on the input wrapper as
|
||||
* this includes the label, instead we apply the
|
||||
* background to the native wrapper.
|
||||
*/
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:host(.input-fill-outline.input-shape-round) .label-text-wrapper {
|
||||
@include padding(null, var(--padding-end), null, var(--padding-start));
|
||||
}
|
||||
|
||||
:host(.input-fill-outline.input-label-placement-stacked) .label-text-wrapper {
|
||||
@include transform-origin(start, top);
|
||||
|
||||
/**
|
||||
* Label text should not extend
|
||||
* beyond the bounds of the input.
|
||||
*/
|
||||
max-width: calc(100% - var(--padding-start) - var(--padding-end));
|
||||
}
|
||||
|
||||
:host(.input-fill-outline) .label-text-wrapper {
|
||||
/**
|
||||
* The label should appear on top of an outline
|
||||
* container that overlaps it so it is always clickable.
|
||||
*/
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host(.input-fill-outline) .native-wrapper {
|
||||
@include border-radius(inherit);
|
||||
@include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start));
|
||||
|
||||
min-height: 40px;
|
||||
|
||||
/**
|
||||
* Apply the background to the native input
|
||||
* wrapper to only style the input.
|
||||
*/
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
// Input Fill: Outline, Outline Container
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
:host(.input-fill-outline) .input-outline {
|
||||
@include position(0, 0, 0, 0);
|
||||
@include border-radius(var(--border-radius));
|
||||
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
border: var(--border-width) var(--border-style) var(--border-color);
|
||||
}
|
||||
|
||||
// Input Fill: Outline, Label Placement: Stacked
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
// This makes the label sit above the input.
|
||||
:host(.label-floating.input-fill-outline.input-label-placement-stacked) .label-text-wrapper {
|
||||
@include margin(0);
|
||||
@include padding(4px, null);
|
||||
}
|
||||
|
||||
// Start/End Slots
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
:host(.input-fill-outline) ::slotted([slot="start"]) {
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
|
||||
:host(.input-fill-outline) ::slotted([slot="end"]) {
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
|
||||
// Input Shapes
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.input-fill-outline.input-shape-soft) {
|
||||
--border-radius: #{$ionic-border-radius-200};
|
||||
}
|
||||
|
||||
:host(.input-fill-outline.input-shape-round) {
|
||||
--border-radius: #{$ionic-border-radius-full};
|
||||
}
|
||||
|
||||
:host(.input-fill-outline.input-shape-rectangular) {
|
||||
--border-radius: #{$ionic-border-radius-0};
|
||||
}
|
||||
|
||||
// Input Focus
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
:host(.input-fill-outline.has-focus) {
|
||||
--border-width: #{tokens.$ionic-border-size-050};
|
||||
}
|
||||
229
core/src/components/input/input.ionic.scss
Normal file
@ -0,0 +1,229 @@
|
||||
@use "../../foundations/ionic.vars.scss" as tokens;
|
||||
@import "./input";
|
||||
@import "./input.ionic.vars";
|
||||
@import "./input.ionic.outline.scss";
|
||||
|
||||
// Ionic Input
|
||||
// --------------------------------------------------
|
||||
|
||||
:host {
|
||||
--color: #{tokens.$ionic-color-neutral-1000};
|
||||
--border-width: #{tokens.$ionic-border-size-025};
|
||||
--border-color: #{tokens.$ionic-color-neutral-300};
|
||||
--highlight-color-valid: #{tokens.$ionic-color-success-base};
|
||||
--highlight-color-invalid: #{tokens.$ionic-color-danger-base};
|
||||
--placeholder-color: #{tokens.$ionic-color-neutral-800};
|
||||
--placeholder-opacity: 1;
|
||||
--text-color-invalid: #{tokens.$ionic-color-danger-800};
|
||||
|
||||
font-size: tokens.$ionic-font-size-350;
|
||||
}
|
||||
|
||||
// Ionic Input Sizes
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.input-size-medium) .native-wrapper {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
:host(.input-size-large) .native-wrapper {
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
:host(.input-size-xlarge) .native-wrapper {
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
// Target area
|
||||
// --------------------------------------------------
|
||||
:host .native-wrapper::after {
|
||||
@include position(50%, 0, null, 0);
|
||||
|
||||
position: absolute;
|
||||
|
||||
height: 100%;
|
||||
min-height: 48px;
|
||||
|
||||
transform: translateY(-50%);
|
||||
|
||||
content: "";
|
||||
|
||||
// Cursor should match the native input when hovering over the target area.
|
||||
cursor: text;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
::slotted([slot="start"]),
|
||||
::slotted([slot="end"]),
|
||||
.input-clear-icon {
|
||||
/**
|
||||
* The target area has a z-index of 1, so the slotted elements
|
||||
* should be higher. Otherwise, the slotted elements will not
|
||||
* be interactable. This is especially important for the clear
|
||||
* button, which should be clickable.
|
||||
*/
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
// Input Clear Button
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
.input-clear-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
color: #{tokens.$ionic-color-neutral-500};
|
||||
}
|
||||
|
||||
.input-clear-icon:focus-visible {
|
||||
@include border-radius(tokens.$ionic-border-radius-100);
|
||||
|
||||
outline: #{tokens.$ionic-border-size-050} solid #{tokens.$ionic-state-focus-1};
|
||||
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.input-clear-icon ion-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/**
|
||||
* The clear button should be visible if the native input
|
||||
* OR any other interactive elements in the component (the
|
||||
* clear button, slotted buttons, etc.) are focused. If we
|
||||
* only looked at the native input, tabbing to the clear
|
||||
* button would immediately hide it.
|
||||
*
|
||||
* Note that the clear button also requires the native input
|
||||
* to have any value, but this is not specific to the ionic
|
||||
* theme, so it is handled elsewhere.
|
||||
*/
|
||||
:host(:not(:focus-within)) .input-clear-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Input Label
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
.label-text-wrapper {
|
||||
color: tokens.$ionic-color-neutral-1000;
|
||||
|
||||
font-size: tokens.$ionic-font-size-300;
|
||||
font-weight: tokens.$ionic-font-weight-medium;
|
||||
|
||||
line-height: tokens.$ionic-line-height-500;
|
||||
}
|
||||
|
||||
:host(.label-floating) .label-text-wrapper {
|
||||
@include transform(none);
|
||||
}
|
||||
|
||||
// Input Bottom Content
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
.input-bottom {
|
||||
@include padding(7px, 0);
|
||||
|
||||
font-weight: tokens.$ionic-font-weight-medium;
|
||||
}
|
||||
|
||||
.input-bottom .helper-text,
|
||||
.input-bottom .counter {
|
||||
color: tokens.$ionic-color-neutral-800;
|
||||
}
|
||||
|
||||
:host(.has-focus.ion-valid) .helper-text {
|
||||
color: tokens.$ionic-color-success-900;
|
||||
}
|
||||
|
||||
:host(.ion-touched.ion-invalid) .error-text {
|
||||
color: var(--text-color-invalid);
|
||||
}
|
||||
|
||||
:host(.has-focus.ion-valid),
|
||||
:host(.ion-touched.ion-invalid) {
|
||||
--border-width: #{tokens.$ionic-border-size-025};
|
||||
}
|
||||
|
||||
// Input Hover
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
@media (any-hover: hover) {
|
||||
:host(:hover) {
|
||||
--border-color: #{tokens.$ionic-color-neutral-600};
|
||||
}
|
||||
}
|
||||
|
||||
// Input - Disabled
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
:host(.input-disabled) {
|
||||
// color for the text within the input
|
||||
--color: #{tokens.$ionic-color-neutral-400};
|
||||
--background: #{tokens.$ionic-color-neutral-100};
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:host(.input-disabled:not(.ion-valid)) .input-bottom .helper-text,
|
||||
:host(.input-disabled) .input-bottom .counter,
|
||||
:host(.input-disabled) .label-text-wrapper {
|
||||
color: tokens.$ionic-color-neutral-400;
|
||||
}
|
||||
|
||||
:host(.input-disabled.has-focus.ion-valid) {
|
||||
--border-color: rgba(#{tokens.$ionic-color-success-base-rgb}, 0.6);
|
||||
}
|
||||
|
||||
:host(.input-disabled.ion-touched.ion-invalid) {
|
||||
--border-color: rgba(#{tokens.$ionic-color-danger-base-rgb}, 0.6);
|
||||
}
|
||||
|
||||
:host(.input-disabled.ion-color) {
|
||||
--border-color: #{current-color(base, 0.6)};
|
||||
}
|
||||
|
||||
:host(.input-disabled.has-focus.ion-valid) .input-bottom .helper-text,
|
||||
:host(.input-disabled.ion-touched.ion-invalid) .error-text,
|
||||
:host(.input-disabled.ion-color) .input-bottom .helper-text,
|
||||
:host(.input-disabled.ion-color) .helper-text {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
// Input - Readonly
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
:host(.input-readonly) {
|
||||
--background: #{tokens.$ionic-color-neutral-100};
|
||||
}
|
||||
|
||||
// Input Highlight
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
.input-highlight {
|
||||
@include position(null, null, -1px, 0);
|
||||
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
height: tokens.$ionic-border-size-050;
|
||||
|
||||
transform: scale(0);
|
||||
|
||||
transition: transform 200ms;
|
||||
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
// Input Focus
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
:host(.has-focus) {
|
||||
--border-color: #{tokens.$ionic-color-primary-base};
|
||||
}
|
||||
|
||||
:host(.has-focus) .input-highlight {
|
||||
transform: scale(1);
|
||||
}
|
||||
2
core/src/components/input/input.ionic.vars.scss
Normal file
@ -0,0 +1,2 @@
|
||||
// Ionic Input
|
||||
// --------------------------------------------------
|
||||
@ -23,6 +23,7 @@
|
||||
* @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 --text-color-invalid: The color of the error text on the input when invalid. Only applies to ionic theme.
|
||||
*
|
||||
* @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. A large radius may display unevenly when using fill="outline"; if needed, use shape="round" instead or increase --padding-start.
|
||||
|
||||
@ -4,6 +4,7 @@ import type { NotchController } from '@utils/forms';
|
||||
import { createNotchController } from '@utils/forms';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import { inheritAriaAttributes, debounceEvent, inheritAttributes, componentOnReady } from '@utils/helpers';
|
||||
import { printIonWarning } from '@utils/logging';
|
||||
import { createSlotMutationController } from '@utils/slot-mutation-controller';
|
||||
import type { SlotMutationController } from '@utils/slot-mutation-controller';
|
||||
import { createColorClasses, hostContext } from '@utils/theme';
|
||||
@ -29,7 +30,7 @@ import { getCounterText } from './input.utils';
|
||||
styleUrls: {
|
||||
ios: 'input.ios.scss',
|
||||
md: 'input.md.scss',
|
||||
ionic: 'input.md.scss',
|
||||
ionic: 'input.ionic.scss',
|
||||
},
|
||||
scoped: true,
|
||||
})
|
||||
@ -186,8 +187,12 @@ export class Input implements ComponentInterface {
|
||||
* `"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 ("...").
|
||||
*
|
||||
* Defaults to "stacked" for the ionic theme, or "start" for all other themes.
|
||||
*
|
||||
* In the ionic theme, only the values "stacked" and "floating" are supported.
|
||||
*/
|
||||
@Prop() labelPlacement: 'start' | 'end' | 'floating' | 'stacked' | 'fixed' = 'start';
|
||||
@Prop({ mutable: true }) labelPlacement?: 'start' | 'end' | 'floating' | 'stacked' | 'fixed';
|
||||
|
||||
/**
|
||||
* The maximum value, which must not be less than its minimum (min attribute) value.
|
||||
@ -242,9 +247,12 @@ export class Input implements ComponentInterface {
|
||||
@Prop() required = false;
|
||||
|
||||
/**
|
||||
* The shape of the input. If "round" it will have an increased border radius.
|
||||
* Set to `"soft"` for an input with slightly rounded corners, `"round"` for an input with fully
|
||||
* rounded corners, or `"rectangular"` for an input without rounded corners.
|
||||
* Defaults to `"round"` for the ionic theme, and `undefined` for all other themes.
|
||||
* Only applies when the fill is set to `"solid"` or `"outline"`.
|
||||
*/
|
||||
@Prop() shape?: 'round';
|
||||
@Prop() shape?: 'soft' | 'round' | 'rectangular';
|
||||
|
||||
/**
|
||||
* If `true`, the element will have its spelling and grammar checked.
|
||||
@ -257,6 +265,12 @@ export class Input implements ComponentInterface {
|
||||
*/
|
||||
@Prop() step?: string;
|
||||
|
||||
/**
|
||||
* The size of the input. If "large", it will have an increased height. By default the
|
||||
* size is medium. This property only applies to the `"ionic"` theme.
|
||||
*/
|
||||
@Prop() size?: 'medium' | 'large' | 'xlarge' = 'medium';
|
||||
|
||||
/**
|
||||
* The type of control to display. The default type is text.
|
||||
*/
|
||||
@ -344,6 +358,10 @@ export class Input implements ComponentInterface {
|
||||
...inheritAriaAttributes(this.el),
|
||||
...inheritAttributes(this.el, ['tabindex', 'title', 'data-form-type']),
|
||||
};
|
||||
|
||||
if (this.labelPlacement === undefined) {
|
||||
this.labelPlacement = getIonTheme(this) === 'ionic' ? ionicThemeDefaultLabelPlacement : 'start';
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
@ -473,6 +491,55 @@ export class Input implements ComponentInterface {
|
||||
return typeof this.value === 'number' ? this.value.toString() : (this.value || '').toString();
|
||||
}
|
||||
|
||||
private getLabelPlacement() {
|
||||
const theme = getIonTheme(this);
|
||||
const { el, labelPlacement } = this;
|
||||
|
||||
if (theme === 'ionic' && labelPlacement !== 'stacked' && labelPlacement !== 'floating') {
|
||||
printIonWarning(
|
||||
`The "${labelPlacement}" label placement is not supported in the ${theme} theme. The default value of "${ionicThemeDefaultLabelPlacement}" will be used instead.`,
|
||||
el
|
||||
);
|
||||
return ionicThemeDefaultLabelPlacement;
|
||||
}
|
||||
|
||||
return labelPlacement;
|
||||
}
|
||||
|
||||
// TODO(FW-6201): Remove this method when size is supported in ios and md
|
||||
private getSize() {
|
||||
const theme = getIonTheme(this);
|
||||
const { size } = this;
|
||||
if (theme !== 'ionic' && (size === 'large' || size === 'xlarge')) {
|
||||
printIonWarning(`The "${size}" size is not supported in the ${theme} theme.`);
|
||||
// Fallback to medium size, which is the default size for all themes.
|
||||
return 'medium';
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
private getShape() {
|
||||
const theme = getIonTheme(this);
|
||||
const { shape } = this;
|
||||
// TODO(ROU-5475): Remove the check for `soft` when the shape is supported in ios and md.
|
||||
if ((theme === 'ios' && shape === 'round') || (theme !== 'ionic' && shape === 'soft')) {
|
||||
printIonWarning(`The "${shape}" shape is not supported in the ${theme} theme.`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (shape !== undefined) {
|
||||
return shape;
|
||||
}
|
||||
|
||||
// TODO(FW-6229): Update this when the default shape has been decided.
|
||||
if (theme !== 'ionic') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Fallback to round shape, which is the default shape for the ionic theme.
|
||||
return 'round';
|
||||
}
|
||||
|
||||
private onInput = (ev: InputEvent | Event) => {
|
||||
const input = ev.target as HTMLInputElement | null;
|
||||
if (input) {
|
||||
@ -655,9 +722,9 @@ export class Input implements ComponentInterface {
|
||||
*/
|
||||
private renderLabelContainer() {
|
||||
const theme = getIonTheme(this);
|
||||
const hasOutlineFill = theme === 'md' && this.fill === 'outline';
|
||||
const hasOutlineFill = this.fill === 'outline';
|
||||
|
||||
if (hasOutlineFill) {
|
||||
if (hasOutlineFill && theme === 'md') {
|
||||
/**
|
||||
* The outline fill has a special outline
|
||||
* that appears around the input and the label.
|
||||
@ -685,8 +752,9 @@ export class Input implements ComponentInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* If not using the outline style,
|
||||
* we can render just the label.
|
||||
* If not using the outline style, OR if using the
|
||||
* ionic theme, just render the label. For the ionic
|
||||
* theme, the outline will be rendered elsewhere.
|
||||
*/
|
||||
return this.renderLabel();
|
||||
}
|
||||
@ -714,11 +782,14 @@ export class Input implements ComponentInterface {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { disabled, fill, readonly, shape, inputId, labelPlacement, el, hasFocus, inputClearIcon } = this;
|
||||
const { disabled, fill, readonly, inputId, el, hasFocus, clearInput, inputClearIcon } = this;
|
||||
const theme = getIonTheme(this);
|
||||
const value = this.getValue();
|
||||
const size = this.getSize();
|
||||
const shape = this.getShape();
|
||||
const inItem = hostContext('ion-item', this.el);
|
||||
const shouldRenderHighlight = theme === 'md' && fill !== 'outline' && !inItem;
|
||||
const shouldRenderHighlight = (theme === 'md' || theme === 'ionic') && fill !== 'outline' && !inItem;
|
||||
const labelPlacement = this.getLabelPlacement();
|
||||
|
||||
const hasValue = this.hasValue();
|
||||
const hasStartEndSlots = el.querySelector('[slot="start"], [slot="end"]') !== null;
|
||||
@ -752,10 +823,12 @@ export class Input implements ComponentInterface {
|
||||
'label-floating': labelShouldFloat,
|
||||
[`input-fill-${fill}`]: fill !== undefined,
|
||||
[`input-shape-${shape}`]: shape !== undefined,
|
||||
[`input-size-${size}`]: true,
|
||||
[`input-label-placement-${labelPlacement}`]: true,
|
||||
'in-item': inItem,
|
||||
'in-item-color': hostContext('ion-item.ion-color', this.el),
|
||||
'input-disabled': disabled,
|
||||
'input-readonly': readonly,
|
||||
})}
|
||||
>
|
||||
{/**
|
||||
@ -767,6 +840,18 @@ export class Input implements ComponentInterface {
|
||||
<label class="input-wrapper" htmlFor={inputId}>
|
||||
{this.renderLabelContainer()}
|
||||
<div class="native-wrapper">
|
||||
{
|
||||
/**
|
||||
* For the ionic theme, we render the outline container here
|
||||
* instead of higher up, so it can be positioned relative to
|
||||
* the native wrapper instead of the <label> element or the
|
||||
* entire component. This allows the label text to be positioned
|
||||
* above the outline, while staying within the bounds of the
|
||||
* <label> element, ensuring that clicking the label text
|
||||
* focuses the input.
|
||||
*/
|
||||
theme === 'ionic' && fill === 'outline' && <div class="input-outline"></div>
|
||||
}
|
||||
<slot name="start"></slot>
|
||||
<input
|
||||
class="native-input"
|
||||
@ -802,7 +887,7 @@ export class Input implements ComponentInterface {
|
||||
onCompositionend={this.onCompositionEnd}
|
||||
{...this.inheritedAttributes}
|
||||
/>
|
||||
{this.clearInput && !readonly && !disabled && (
|
||||
{clearInput && !readonly && !disabled && (
|
||||
<button
|
||||
aria-label="reset"
|
||||
type="button"
|
||||
@ -840,3 +925,4 @@ export class Input implements ComponentInterface {
|
||||
}
|
||||
|
||||
let inputIds = 0;
|
||||
const ionicThemeDefaultLabelPlacement = 'stacked';
|
||||
|
||||
@ -131,3 +131,90 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
configs({ modes: ['ionic-md'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('input: clear button in ionic theme, visual checks'), () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input
|
||||
label="Label"
|
||||
label-placement="stacked"
|
||||
clear-input="true"
|
||||
value="Text"
|
||||
></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await input.evaluate((el: HTMLIonInputElement) => el.setFocus());
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-with-clear-button`));
|
||||
});
|
||||
|
||||
test('should not have visual regressions when clear button is focused', async ({ page, pageUtils }) => {
|
||||
// extra padding around input ensures focus ring doesn't get cut off at screenshot edges
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
#container {
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="container">
|
||||
<ion-input
|
||||
label="Label"
|
||||
label-placement="stacked"
|
||||
clear-input="true"
|
||||
value="Text"
|
||||
></ion-input>
|
||||
</div>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await input.evaluate((el: HTMLIonInputElement) => el.setFocus());
|
||||
await page.waitForChanges();
|
||||
|
||||
await pageUtils.pressKeys('Tab');
|
||||
|
||||
const container = page.locator('#container');
|
||||
await expect(container).toHaveScreenshot(screenshot(`input-clear-button-focused`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
configs({ modes: ['ionic-md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('input: clear button in ionic theme, functionality checks'), () => {
|
||||
test('should show clear button when any part of input is focused', async ({ page, pageUtils }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input
|
||||
label="Label"
|
||||
label-placement="stacked"
|
||||
clear-input="true"
|
||||
value="Text"
|
||||
></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
const clearButton = input.locator('.input-clear-icon');
|
||||
|
||||
await expect(clearButton).not.toBeVisible();
|
||||
|
||||
await input.evaluate((el: HTMLIonInputElement) => el.setFocus());
|
||||
await expect(clearButton).toBeVisible();
|
||||
|
||||
// ensure blurring native input doesn't immediately hide clear button
|
||||
await pageUtils.pressKeys('Tab');
|
||||
await expect(clearButton).toBeFocused();
|
||||
await expect(clearButton).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@ -49,58 +49,159 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content id="content" class="ion-padding">
|
||||
<h2>Underline</h2>
|
||||
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>No Hint</h2>
|
||||
<ion-input label="Email"></ion-input>
|
||||
<ion-input placeholder="Placeholder" label="Email"></ion-input>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Helper Hint</h2>
|
||||
<ion-input label="Email" helper-text="Enter your email"></ion-input>
|
||||
<ion-input placeholder="Placeholder" label="Email" helper-text="Helper message"></ion-input>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Error Hint</h2>
|
||||
<ion-input
|
||||
placeholder="Placeholder"
|
||||
class="ion-touched ion-invalid"
|
||||
label="Email"
|
||||
error-text="Please enter a valid email"
|
||||
error-text="Helper message"
|
||||
></ion-input>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Custom Error Color</h2>
|
||||
<ion-input
|
||||
placeholder="Placeholder"
|
||||
class="ion-touched ion-invalid custom-error-color"
|
||||
label="Email"
|
||||
error-text="Please enter a valid email"
|
||||
error-text="Helper message"
|
||||
></ion-input>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Counter</h2>
|
||||
<ion-input label="Email" counter="true" maxlength="100"></ion-input>
|
||||
<ion-input placeholder="Placeholder" label="Email" counter="true" maxlength="100"></ion-input>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Custom Counter</h2>
|
||||
<ion-input id="custom-counter" label="Email" counter="true" maxlength="100"></ion-input>
|
||||
<ion-input
|
||||
placeholder="Placeholder"
|
||||
id="custom-counter"
|
||||
label="Email"
|
||||
counter="true"
|
||||
maxlength="100"
|
||||
></ion-input>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Counter with Helper</h2>
|
||||
<ion-input label="Email" counter="true" maxlength="100" helper-text="Enter an email"></ion-input>
|
||||
<ion-input
|
||||
placeholder="Placeholder"
|
||||
label="Email"
|
||||
counter="true"
|
||||
maxlength="100"
|
||||
helper-text="Enter an email"
|
||||
></ion-input>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Counter with Error</h2>
|
||||
<ion-input
|
||||
placeholder="Placeholder"
|
||||
class="ion-touched ion-invalid"
|
||||
label="Email"
|
||||
counter="true"
|
||||
maxlength="100"
|
||||
error-text="Please enter a valid email"
|
||||
error-text="Helper message"
|
||||
></ion-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Outline</h2>
|
||||
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>No Hint</h2>
|
||||
<ion-input placeholder="Placeholder" fill="outline" label="Email"></ion-input>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Helper Hint</h2>
|
||||
<ion-input placeholder="Placeholder" fill="outline" label="Email" helper-text="Helper message"></ion-input>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Error Hint</h2>
|
||||
<ion-input
|
||||
placeholder="Placeholder"
|
||||
fill="outline"
|
||||
class="ion-touched ion-invalid"
|
||||
label="Email"
|
||||
error-text="Helper message"
|
||||
></ion-input>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Custom Error Color</h2>
|
||||
<ion-input
|
||||
placeholder="Placeholder"
|
||||
fill="outline"
|
||||
class="ion-touched ion-invalid custom-error-color"
|
||||
label="Email"
|
||||
error-text="Helper message"
|
||||
></ion-input>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Counter</h2>
|
||||
<ion-input
|
||||
placeholder="Placeholder"
|
||||
fill="outline"
|
||||
label="Email"
|
||||
counter="true"
|
||||
maxlength="100"
|
||||
></ion-input>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Custom Counter</h2>
|
||||
<ion-input
|
||||
placeholder="Placeholder"
|
||||
fill="outline"
|
||||
id="custom-counter"
|
||||
label="Email"
|
||||
counter="true"
|
||||
maxlength="100"
|
||||
></ion-input>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Counter with Helper</h2>
|
||||
<ion-input
|
||||
placeholder="Placeholder"
|
||||
fill="outline"
|
||||
label="Email"
|
||||
counter="true"
|
||||
maxlength="100"
|
||||
helper-text="Enter an email"
|
||||
></ion-input>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Counter with Error</h2>
|
||||
<ion-input
|
||||
placeholder="Placeholder"
|
||||
fill="outline"
|
||||
class="ion-touched ion-invalid"
|
||||
label="Email"
|
||||
counter="true"
|
||||
maxlength="100"
|
||||
error-text="Helper message"
|
||||
></ion-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
configs({ directions: ['ltr'], modes: ['md', 'ios'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('input: bottom content'), () => {
|
||||
test('entire input component should render correctly with no fill', async ({ page }) => {
|
||||
await page.setContent(
|
||||
@ -36,6 +36,31 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
});
|
||||
});
|
||||
|
||||
configs({ directions: ['ltr'], modes: ['ionic-md'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('input: bottom content'), () => {
|
||||
test('entire input component should render correctly with no fill', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input value="hi@ionic.io" label="Email" helper-text="Enter an email" maxlength="20" counter="true"></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-full-bottom-no-fill`));
|
||||
});
|
||||
test('entire input component should render correctly with outline fill', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input fill="outline" value="hi@ionic.io" label="Email" helper-text="Enter an email" maxlength="20" counter="true"></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-full-bottom-outline`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Rendering is the same across modes
|
||||
*/
|
||||
@ -53,7 +78,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
/**
|
||||
* Rendering is the same across modes
|
||||
*/
|
||||
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
configs({ modes: ['md', 'ionic-md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('input: hint text'), () => {
|
||||
test.describe('input: hint text functionality', () => {
|
||||
test('helper text should be visible initially', async ({ page }) => {
|
||||
@ -80,12 +105,13 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
|
||||
await expect(errorText).toBeVisible();
|
||||
await expect(errorText).toHaveText('my error');
|
||||
});
|
||||
test('error text should change when variable is customized', async ({ page }) => {
|
||||
test('error text and highlight should change when variable is customized', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-input.custom-input {
|
||||
--highlight-color-invalid: purple;
|
||||
--text-color-invalid: purple; /* ionic only */
|
||||
}
|
||||
</style>
|
||||
<ion-input class="ion-invalid ion-touched custom-input" label="my label" error-text="my error"></ion-input>
|
||||
@ -93,8 +119,8 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
|
||||
config
|
||||
);
|
||||
|
||||
const errorText = page.locator('ion-input .error-text');
|
||||
await expect(errorText).toHaveScreenshot(screenshot(`input-error-custom-color`));
|
||||
const bottomEl = page.locator('ion-input .input-bottom');
|
||||
await expect(bottomEl).toHaveScreenshot(screenshot(`input-error-custom-color`));
|
||||
});
|
||||
});
|
||||
test.describe('input: hint text rendering', () => {
|
||||
|
||||
|
After Width: | Height: | Size: 1013 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 967 B |
|
After Width: | Height: | Size: 995 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 945 B |
|
After Width: | Height: | Size: 1022 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 965 B |
|
Before Width: | Height: | Size: 873 B After Width: | Height: | Size: 990 B |
|
Before Width: | Height: | Size: 822 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 751 B After Width: | Height: | Size: 868 B |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
@ -44,38 +44,17 @@ configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => {
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-fill-solid-label-floating`));
|
||||
});
|
||||
test('should not have visual regressions with shaped solid', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input
|
||||
shape="round"
|
||||
fill="solid"
|
||||
label="Email"
|
||||
value="hi@ionic.io"
|
||||
helper-text="Enter your email"
|
||||
maxlength="20"
|
||||
counter="true"
|
||||
></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-fill-shaped-solid`));
|
||||
});
|
||||
test('padding and border radius should be customizable', async ({ page }) => {
|
||||
test('padding should be customizable', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-input {
|
||||
--border-radius: 10px !important;
|
||||
--padding-start: 50px !important;
|
||||
--padding-end: 50px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-input
|
||||
shape="round"
|
||||
fill="solid"
|
||||
label="Email"
|
||||
label-placement="floating"
|
||||
@ -89,7 +68,7 @@ configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => {
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-fill-shaped-solid-custom`));
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-fill-solid-custom`));
|
||||
});
|
||||
});
|
||||
test.describe('input: fill outline', () => {
|
||||
@ -130,41 +109,23 @@ configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => {
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-fill-outline-label-floating`));
|
||||
});
|
||||
test('should not have visual regressions with shaped outline', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input
|
||||
shape="round"
|
||||
fill="outline"
|
||||
label="Email"
|
||||
value="hi@ionic.io"
|
||||
helper-text="Enter your email"
|
||||
maxlength="20"
|
||||
counter="true"
|
||||
></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-fill-shaped-outline`));
|
||||
});
|
||||
test('padding and border radius should be customizable', async ({ page }) => {
|
||||
test('padding should be customizable', async ({ page }) => {
|
||||
/**
|
||||
* Requires padding at the top to prevent the label
|
||||
* from being clipped by the top of the input.
|
||||
*/
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-input {
|
||||
--border-radius: 10px !important;
|
||||
--padding-start: 50px !important;
|
||||
--padding-end: 50px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-input
|
||||
shape="round"
|
||||
fill="outline"
|
||||
label="Email"
|
||||
label-placement="floating"
|
||||
value="hi@ionic.io"
|
||||
helper-text="Enter your email"
|
||||
maxlength="20"
|
||||
@ -175,7 +136,7 @@ configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => {
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-fill-shaped-outline-custom`));
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-fill-outline-custom`));
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -248,3 +209,42 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
configs({ modes: ['ionic-md'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('input: ionic theme fill'), () => {
|
||||
test('should not have visual regressions with outline fill and stacked label placement', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input
|
||||
fill="outline"
|
||||
label="Email"
|
||||
label-placement="stacked"
|
||||
placeholder="example@ionic.io"
|
||||
></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-fill-outline-label-stacked`));
|
||||
});
|
||||
|
||||
test('should not have visual regressions with outline fill and large size', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input
|
||||
fill="outline"
|
||||
label="Email"
|
||||
label-placement="stacked"
|
||||
placeholder="example@ionic.io"
|
||||
size="large"
|
||||
></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-fill-outline-label-stacked-size-large`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
@ -344,3 +344,118 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
configs({ directions: ['ltr'], modes: ['ionic-md'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('input: highlights'), () => {
|
||||
test.describe('input: no fill', () => {
|
||||
test('should render valid state correctly', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input
|
||||
value="hi@ionic.io"
|
||||
class="ion-valid has-focus"
|
||||
label="Email"
|
||||
error-text="Please enter a valid email"
|
||||
helper-text="Enter an email"
|
||||
counter="true"
|
||||
maxlength="20"
|
||||
></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-no-fill-valid`));
|
||||
});
|
||||
test('should render invalid state correctly', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input
|
||||
value="hi@ionic.io"
|
||||
class="ion-touched ion-invalid"
|
||||
label="Email"
|
||||
error-text="Please enter a valid email"
|
||||
helper-text="Enter an email"
|
||||
counter="true"
|
||||
maxlength="20"
|
||||
></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-no-fill-invalid`));
|
||||
});
|
||||
});
|
||||
test.describe('input: outline', () => {
|
||||
test('should render valid state correctly', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input
|
||||
fill="outline"
|
||||
value="hi@ionic.io"
|
||||
class="ion-valid has-focus"
|
||||
label="Email"
|
||||
error-text="Please enter a valid email"
|
||||
helper-text="Enter an email"
|
||||
counter="true"
|
||||
maxlength="20"
|
||||
></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-outline-valid`));
|
||||
});
|
||||
test('should render invalid state correctly', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input
|
||||
fill="outline"
|
||||
value="hi@ionic.io"
|
||||
class="ion-touched ion-invalid"
|
||||
label="Email"
|
||||
error-text="Please enter a valid email"
|
||||
helper-text="Enter an email"
|
||||
counter="true"
|
||||
maxlength="20"
|
||||
></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-outline-invalid`));
|
||||
});
|
||||
test('should render custom highlight correctly', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-input.custom {
|
||||
--highlight-color-valid: purple;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<ion-input
|
||||
fill="outline"
|
||||
value="hi@ionic.io"
|
||||
class="custom has-focus ion-valid"
|
||||
label="Email"
|
||||
error-text="Please enter a valid email"
|
||||
helper-text="Enter an email"
|
||||
counter="true"
|
||||
maxlength="20"
|
||||
></ion-input>
|
||||
</div>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const container = page.locator('.container');
|
||||
await expect(container).toHaveScreenshot(screenshot(`input-outline-custom-highlight`));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
102
core/src/components/input/test/shape/index.html
Normal file
@ -0,0 +1,102 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Input - Shape</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(250px, 1fr));
|
||||
grid-row-gap: 20px;
|
||||
grid-column-gap: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
|
||||
color: #6f7378;
|
||||
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Input - Shape</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content id="content" class="ion-padding">
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>Default Shape</h2>
|
||||
<ion-input
|
||||
fill="outline"
|
||||
label="Label"
|
||||
placeholder="Placeholder"
|
||||
helper-text="Helper message"
|
||||
counter="true"
|
||||
maxlength="999"
|
||||
></ion-input>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>Soft Shape</h2>
|
||||
<ion-input
|
||||
fill="outline"
|
||||
shape="soft"
|
||||
label="Label"
|
||||
placeholder="Placeholder"
|
||||
helper-text="Helper message"
|
||||
counter="true"
|
||||
maxlength="999"
|
||||
></ion-input>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>Round Shape</h2>
|
||||
<ion-input
|
||||
fill="outline"
|
||||
shape="round"
|
||||
label="Label"
|
||||
placeholder="Placeholder"
|
||||
helper-text="Helper message"
|
||||
counter="true"
|
||||
maxlength="999"
|
||||
></ion-input>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>Rectangular Shape</h2>
|
||||
<ion-input
|
||||
fill="outline"
|
||||
shape="rectangular"
|
||||
label="Label"
|
||||
placeholder="Placeholder"
|
||||
helper-text="Helper message"
|
||||
counter="true"
|
||||
maxlength="999"
|
||||
></ion-input>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
196
core/src/components/input/test/shape/input.e2e.ts
Normal file
@ -0,0 +1,196 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs({ modes: ['md'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('input: shape'), () => {
|
||||
/**
|
||||
* Solid fill is only available in the md theme
|
||||
*/
|
||||
test.describe('solid fill', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input
|
||||
shape="round"
|
||||
fill="solid"
|
||||
label="Email"
|
||||
value="hi@ionic.io"
|
||||
helper-text="Enter your email"
|
||||
maxlength="20"
|
||||
counter="true"
|
||||
></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-shape-round-fill-solid`));
|
||||
});
|
||||
test('border radius should be customizable', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-input {
|
||||
--border-radius: 10px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-input
|
||||
shape="round"
|
||||
fill="solid"
|
||||
label="Email"
|
||||
label-placement="floating"
|
||||
value="hi@ionic.io"
|
||||
helper-text="Enter your email"
|
||||
maxlength="20"
|
||||
counter="true"
|
||||
></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-shape-round-fill-solid-custom`));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
configs({ modes: ['ionic-md', 'md'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('input: shape'), () => {
|
||||
test.describe('round shape', () => {
|
||||
test.describe('outline fill', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input
|
||||
shape="round"
|
||||
fill="outline"
|
||||
label="Email"
|
||||
value="hi@ionic.io"
|
||||
helper-text="Enter your email"
|
||||
maxlength="20"
|
||||
counter="true"
|
||||
></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-shape-round-fill-outline`));
|
||||
});
|
||||
|
||||
test('border radius should be customizable', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-input {
|
||||
--border-radius: 10px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-input
|
||||
shape="round"
|
||||
fill="outline"
|
||||
label="Email"
|
||||
value="hi@ionic.io"
|
||||
helper-text="Enter your email"
|
||||
maxlength="20"
|
||||
counter="true"
|
||||
></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-shape-round-fill-outline-custom`));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
configs({ modes: ['ionic-md'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('input: shape'), () => {
|
||||
// TODO(ROU-5475): Add the `md` theme once the `soft` shape is available
|
||||
// in the `md` theme by combining these tests with the above tests.
|
||||
test.describe('soft shape', () => {
|
||||
test.describe('outline fill', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input
|
||||
shape="soft"
|
||||
fill="outline"
|
||||
label="Email"
|
||||
value="hi@ionic.io"
|
||||
helper-text="Enter your email"
|
||||
maxlength="20"
|
||||
counter="true"
|
||||
></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-shape-soft-fill-outline`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Rectangular shape is only available in the ionic theme
|
||||
* TODO(FW-6098): Add test for rectangular shape in md
|
||||
* by combining these tests with the above tests
|
||||
*/
|
||||
test.describe('rectangular shape', () => {
|
||||
test.describe('outline fill', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-input
|
||||
shape="rectangular"
|
||||
fill="outline"
|
||||
label="Email"
|
||||
value="hi@ionic.io"
|
||||
helper-text="Enter your email"
|
||||
maxlength="20"
|
||||
counter="true"
|
||||
></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-shape-rectangular-fill-outline`));
|
||||
});
|
||||
|
||||
test('border radius should be customizable', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-input {
|
||||
--border-radius: 10px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-input
|
||||
shape="rectangular"
|
||||
fill="outline"
|
||||
label="Email"
|
||||
value="hi@ionic.io"
|
||||
helper-text="Enter your email"
|
||||
maxlength="20"
|
||||
counter="true"
|
||||
></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await expect(input).toHaveScreenshot(screenshot(`input-shape-rectangular-fill-outline-custom`));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 5.0 KiB |