diff --git a/core/src/components/input/input.ionic.outline.scss b/core/src/components/input/input.ionic.outline.scss index fd0ceadac49..22981cc50b8 100644 --- a/core/src/components/input/input.ionic.outline.scss +++ b/core/src/components/input/input.ionic.outline.scss @@ -1,142 +1,6 @@ @use "../../themes/ionic/ionic.globals.scss" as globals; -// Input Fill: Outline (Ionic Theme) -// ---------------------------------------------------------------- - :host(.input-fill-outline) { - --border-radius: #{globals.$ion-border-radius-100}; - --padding-start: #{globals.$ion-space-300}; - --padding-end: #{globals.$ion-space-300}; -} - -:host(.input-fill-outline.input-size-large) { - --padding-start: #{globals.$ion-space-400}; - --padding-end: #{globals.$ion-space-400}; -} - -:host(.input-fill-outline.input-size-xlarge) { - --padding-start: #{globals.$ion-space-500}; - --padding-end: #{globals.$ion-space-500}; -} - -/** - * 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-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 globals.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-label-placement-stacked) .label-text-wrapper { - @include globals.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 globals.border-radius(inherit); - @include globals.padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start)); - - min-height: globals.$ion-scale-1000; - - /** - * 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 globals.position(0, 0, 0, 0); - @include globals.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 globals.margin(0); - @include globals.padding(globals.$ion-space-100, null); -} - -// Start/End Slots -// ---------------------------------------------------------------- - -:host(.input-fill-outline) ::slotted([slot="start"]) { - margin-inline-end: globals.$ion-space-200; -} - -:host(.input-fill-outline) ::slotted([slot="end"]) { - margin-inline-start: globals.$ion-space-200; -} - -// Input Shapes -// -------------------------------------------------- - -:host(.input-fill-outline.input-shape-soft) { - --border-radius: #{globals.$ion-border-radius-200}; -} - -:host(.input-fill-outline.input-shape-round) { - --border-radius: #{globals.$ion-border-radius-400}; -} - -:host(.input-fill-outline.input-shape-rectangular) { - --border-radius: #{globals.$ion-border-radius-0}; -} - -// Input Focus -// ---------------------------------------------------------------- - -:host(.input-fill-outline.has-focus) { - --border-width: #{globals.$ion-border-size-050}; + --background: #{globals.$ion-primitives-base-white}; + --border-color: #{globals.$ion-primitives-neutral-500}; } diff --git a/core/src/components/input/input.ionic.scss b/core/src/components/input/input.ionic.scss index 9db34044095..631a441f330 100644 --- a/core/src/components/input/input.ionic.scss +++ b/core/src/components/input/input.ionic.scss @@ -1,26 +1,128 @@ @use "../../themes/ionic/ionic.globals.scss" as globals; @use "./input.common"; @forward "./input.ionic.outline.scss"; - +@forward "./input.ionic.solid.scss"; // Ionic Input // -------------------------------------------------- :host { --color: #{globals.$ion-primitives-neutral-1200}; --border-width: #{globals.$ion-border-size-025}; - --border-color: #{globals.$ion-primitives-neutral-500}; --highlight-color-valid: #{globals.$ion-semantics-success-900}; - --highlight-color-invalid: #{globals.$ion-semantics-danger-800}; + --highlight-color-invalid: #{globals.$ion-border-danger-default}; --placeholder-color: #{globals.$ion-primitives-neutral-800}; --placeholder-opacity: 1; - --background: #{globals.$ion-primitives-base-white}; @include globals.typography(globals.$ion-body-md-regular); } +// Input Outline Container +// ---------------------------------------------------------------- + +.input-outline { + @include globals.position(0, 0, 0, 0); + @include globals.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 Label Placement: Stacked +// ---------------------------------------------------------------- + +// This makes the label sit above the input. +:host(.label-floating.input-label-placement-stacked) .label-text-wrapper { + @include globals.margin(0); + @include globals.padding(globals.$ion-space-100, null); +} + +.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 globals.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; +} + +.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-label-placement-stacked) .label-text-wrapper { + @include globals.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)); +} + +/** + * The bottom content should never have + * a border with the outline style. + */ +.input-bottom { + border-top: none; +} + +.native-wrapper { + @include globals.border-radius(inherit); + @include globals.padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start)); + + min-height: globals.$ion-scale-1000; + + /** + * Apply the background to the native input + * wrapper to only style the input. + */ + background: var(--background); +} + // Ionic Input Sizes // -------------------------------------------------- +:host(.input-size-medium) { + --padding-start: #{globals.$ion-space-300}; + --padding-end: #{globals.$ion-space-300}; +} + +:host(.input-size-large) { + --padding-start: #{globals.$ion-space-400}; + --padding-end: #{globals.$ion-space-400}; +} + +:host(.input-size-xlarge) { + --padding-start: #{globals.$ion-space-500}; + --padding-end: #{globals.$ion-space-500}; +} + :host(.input-size-medium) .native-wrapper { min-height: globals.$ion-scale-1000; } @@ -33,10 +135,25 @@ min-height: globals.$ion-scale-1400; } +// Input Shapes +// -------------------------------------------------- + +:host(.input-shape-soft) { + --border-radius: #{globals.$ion-soft-xl}; +} + +:host(.input-shape-round) { + --border-radius: #{globals.$ion-round-xl}; +} + +:host(.input-shape-rectangular) { + --border-radius: #{globals.$ion-rectangular-xl}; +} + // Ionic Input Password Toggle Sizes // -------------------------------------------------- -:host ion-input-password-toggle { +:host(.input-size-medium) ion-input-password-toggle { --size: #{globals.$ion-scale-1000}; } @@ -50,7 +167,8 @@ // Target area // -------------------------------------------------- -:host .native-wrapper::after { + +.native-wrapper::after { @include globals.position(50%, 0, null, 0); position: absolute; @@ -78,6 +196,17 @@ z-index: 2; } +// Start/End Slots +// ---------------------------------------------------------------- + +::slotted([slot="start"]) { + margin-inline-end: globals.$ion-space-200; +} + +::slotted([slot="end"]) { + margin-inline-start: globals.$ion-space-200; +} + // Input Clear Button // ---------------------------------------------------------------- @@ -202,29 +331,12 @@ --background: #{globals.$ion-primitives-neutral-100}; } -// Input Highlight -// ---------------------------------------------------------------- - -.input-highlight { - @include globals.position(null, null, -1px, 0); - - position: absolute; - - width: 100%; - height: globals.$ion-border-size-050; - - transform: scale(0); - - transition: transform globals.$ion-transition-time-200; - - background: var(--border-color); -} - // Input Focus // ---------------------------------------------------------------- :host(.has-focus) { --border-color: #{globals.$ion-border-focus-default}; + --border-width: #{globals.$ion-border-size-050}; } :host(.has-focus) .input-highlight { diff --git a/core/src/components/input/input.ionic.solid.scss b/core/src/components/input/input.ionic.solid.scss new file mode 100644 index 00000000000..cad50c1fc30 --- /dev/null +++ b/core/src/components/input/input.ionic.solid.scss @@ -0,0 +1,51 @@ +@use "../../themes/ionic/ionic.globals.scss" as globals; +// Input Fill: Solid +// ---------------------------------------------------------------- + +:host(.input-fill-solid) { + --background: #{globals.$ion-bg-input-bold-default}; + --border-color: #{globals.$ion-bg-input-bold-default}; +} + +/** + * If the input has a validity state, the + * border should reflect that as a color. + */ +:host(.input-fill-solid.has-focus.ion-valid) .native-wrapper, +:host(.input-fill-solid.ion-touched.ion-invalid) .native-wrapper { + --border-color: var(--highlight-color); +} + +/** + * Background and border should be + * slightly darker on hover. + */ +@media (any-hover: hover) { + :host(:hover) { + --border-color: #{globals.$ion-border-focus-default}; + } +} + +// Input - Disabled +// ---------------------------------------------------------------- + +:host(.input-fill-solid.input-disabled) { + --background: #{globals.$ion-bg-input-bold-disabled}; + --placeholder-color: #{globals.$ion-text-disabled}; +} + +// Input - Readonly +// ---------------------------------------------------------------- + +:host(.input-fill-solid.input-readonly) { + --background: #{globals.$ion-bg-input-bold-read-only}; + --border-color: #{globals.$ion-bg-input-bold-read-only}; +} + +/** + * Background and border should be + * much darker on focus. + */ +:host(.input-fill-solid.has-focus) { + --border-color: #{globals.$ion-border-focus-default}; +} diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx index 2e8d2b3e9a4..3844556d39e 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -847,7 +847,7 @@ export class Input implements ComponentInterface { const size = this.getSize(); const shape = this.getShape(); const inItem = hostContext('ion-item', this.el); - const shouldRenderHighlight = (theme === 'md' || theme === 'ionic') && fill !== 'outline' && !inItem; + const shouldRenderHighlight = theme === 'md' && fill !== 'outline' && !inItem; const labelPlacement = this.getLabelPlacement(); const hasValue = this.hasValue(); @@ -909,7 +909,7 @@ export class Input implements ComponentInterface { * <label> element, ensuring that clicking the label text * focuses the input. */ - theme === 'ionic' && fill === 'outline' && <div class="input-outline"></div> + theme === 'ionic' && <div class="input-outline"></div> } <slot name="start"></slot> <input diff --git a/core/src/components/input/test/states/index.html b/core/src/components/input/test/states/index.html index 57998885bb5..92bab83332d 100644 --- a/core/src/components/input/test/states/index.html +++ b/core/src/components/input/test/states/index.html @@ -88,6 +88,35 @@ <h2>Outline: Color</h2> readonly="true" ></ion-input> </div> + + <div class="grid-item"> + <h2>Solid</h2> + + <ion-input + fill="solid" + label="Email" + value="hi@ionic.io" + helper-text="Enter an email" + counter="true" + maxlength="20" + readonly="true" + ></ion-input> + </div> + + <div class="grid-item"> + <h2>Solid: Color</h2> + + <ion-input + color="warning" + fill="solid" + label="Email" + value="hi@ionic.io" + helper-text="Enter an email" + counter="true" + maxlength="20" + readonly="true" + ></ion-input> + </div> </div> <h1>Disabled</h1> @@ -209,6 +238,67 @@ <h2>Outline, Color (ionic theme only)</h2> </div> </div> + <div class="grid"> + <div class="grid-item"> + <h2>Solid</h2> + + <ion-input + fill="solid" + label="Email" + value="hi@ionic.io" + helper-text="Enter an email" + counter="true" + maxlength="20" + disabled="true" + ></ion-input> + </div> + + <div class="grid-item"> + <h2>Solid, Valid</h2> + + <ion-input + fill="solid" + label="Email" + value="hi@ionic.io" + helper-text="Enter an email" + counter="true" + maxlength="20" + class="ion-valid has-focus" + disabled="true" + ></ion-input> + </div> + + <div class="grid-item"> + <h2>Solid, Invalid</h2> + + <ion-input + fill="solid" + label="Email" + value="hi@ionic.io" + error-text="Please enter a valid email" + counter="true" + maxlength="20" + class="ion-touched ion-invalid" + disabled="true" + ></ion-input> + </div> + + <div class="grid-item"> + <h2>Solid, Color (ionic theme only)</h2> + + <ion-input + fill="solid" + label="Email" + value="hi@ionic.io" + helper-text="Enter an email" + counter="true" + maxlength="20" + disabled="true" + color="warning" + ></ion-input> + </div> + </div> + <h1>Focused</h1> <div class="grid"> <div class="grid-item"> @@ -222,6 +312,12 @@ <h2>Outline</h2> <ion-input fill="outline" label="Email" value="hi@ionic.io" class="has-focus"></ion-input> </div> + + <div class="grid-item"> + <h2>Solid</h2> + + <ion-input fill="solid" label="Email" value="hi@ionic.io" class="has-focus"></ion-input> + </div> </div> </ion-content> </ion-app> diff --git a/core/src/components/input/test/states/input.e2e.ts b/core/src/components/input/test/states/input.e2e.ts index 307d618f855..0f41e197d7f 100644 --- a/core/src/components/input/test/states/input.e2e.ts +++ b/core/src/components/input/test/states/input.e2e.ts @@ -141,6 +141,62 @@ configs({ modes: ['ionic-md'], directions: ['ltr'] }).forEach(({ title, screensh await expect(container).toHaveScreenshot(screenshot(`input-disabled-outline`)); }); }); + test.describe(title('solid'), () => { + test('should render disabled input correctly', async ({ page }) => { + await page.setContent( + ` + <div class="container"> + <ion-input + fill="solid" + label="Email" + value="hi@ionic.io" + helper-text="Enter an email" + counter="true" + maxlength="20" + disabled="true" + ></ion-input> + + <ion-input + fill="solid" + label="Email" + value="hi@ionic.io" + helper-text="Enter an email" + counter="true" + maxlength="20" + class="ion-valid has-focus" + disabled="true" + ></ion-input> + + <ion-input + fill="solid" + label="Email" + value="hi@ionic.io" + error-text="Please enter a valid email" + counter="true" + maxlength="20" + class="ion-touched ion-invalid" + disabled="true" + ></ion-input> + + <ion-input + fill="solid" + label="Email" + value="hi@ionic.io" + helper-text="Enter an email" + counter="true" + maxlength="20" + disabled="true" + color="warning" + ></ion-input> + </div> + `, + config + ); + + const container = page.locator('.container'); + await expect(container).toHaveScreenshot(screenshot(`input-disabled-solid`)); + }); + }); }); test.describe(title('focused'), () => { @@ -166,6 +222,16 @@ configs({ modes: ['ionic-md'], directions: ['ltr'] }).forEach(({ title, screensh maxlength="20" class="has-focus" ></ion-input> + + <ion-input + fill="solid" + label="Email" + value="hi@ionic.io" + helper-text="Enter an email" + counter="true" + maxlength="20" + class="has-focus" + ></ion-input> </div> `, config @@ -223,6 +289,30 @@ configs({ modes: ['ionic-md'], directions: ['ltr'] }).forEach(({ title, screensh await expect(container).toHaveScreenshot(screenshot(`input-readonly-outline`)); }); }); + + test.describe(title('solid'), () => { + test('should render readonly input correctly', async ({ page }) => { + await page.setContent( + ` + <div class="container"> + <ion-input + fill="solid" + label="Email" + value="hi@ionic.io" + helper-text="Enter an email" + counter="true" + maxlength="20" + readonly="true" + ></ion-input> + </div> + `, + config + ); + + const container = page.locator('.container'); + await expect(container).toHaveScreenshot(screenshot(`input-readonly-solid`)); + }); + }); }); }); }); diff --git a/core/src/components/input/test/states/input.e2e.ts-snapshots/input-disabled-solid-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-disabled-solid-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..11e790559f0 Binary files /dev/null and b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-disabled-solid-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/input/test/states/input.e2e.ts-snapshots/input-disabled-solid-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-disabled-solid-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..ea750df56de Binary files /dev/null and b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-disabled-solid-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/input/test/states/input.e2e.ts-snapshots/input-disabled-solid-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-disabled-solid-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..f0d0d3eb2dc Binary files /dev/null and b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-disabled-solid-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/input/test/states/input.e2e.ts-snapshots/input-focused-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-focused-ionic-md-ltr-light-Mobile-Chrome-linux.png index fdb41d6cddd..f13dc225cd2 100644 Binary files a/core/src/components/input/test/states/input.e2e.ts-snapshots/input-focused-ionic-md-ltr-light-Mobile-Chrome-linux.png and b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-focused-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/input/test/states/input.e2e.ts-snapshots/input-focused-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-focused-ionic-md-ltr-light-Mobile-Firefox-linux.png index 5569bd6f446..9b8e19bfb89 100644 Binary files a/core/src/components/input/test/states/input.e2e.ts-snapshots/input-focused-ionic-md-ltr-light-Mobile-Firefox-linux.png and b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-focused-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/input/test/states/input.e2e.ts-snapshots/input-focused-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-focused-ionic-md-ltr-light-Mobile-Safari-linux.png index a7ba7590843..440b7ebdbe9 100644 Binary files a/core/src/components/input/test/states/input.e2e.ts-snapshots/input-focused-ionic-md-ltr-light-Mobile-Safari-linux.png and b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-focused-ionic-md-ltr-light-Mobile-Safari-linux.png differ diff --git a/core/src/components/input/test/states/input.e2e.ts-snapshots/input-readonly-solid-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-readonly-solid-ionic-md-ltr-light-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..c9b5091cce0 Binary files /dev/null and b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-readonly-solid-ionic-md-ltr-light-Mobile-Chrome-linux.png differ diff --git a/core/src/components/input/test/states/input.e2e.ts-snapshots/input-readonly-solid-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-readonly-solid-ionic-md-ltr-light-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..7b28c8a16cb Binary files /dev/null and b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-readonly-solid-ionic-md-ltr-light-Mobile-Firefox-linux.png differ diff --git a/core/src/components/input/test/states/input.e2e.ts-snapshots/input-readonly-solid-ionic-md-ltr-light-Mobile-Safari-linux.png b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-readonly-solid-ionic-md-ltr-light-Mobile-Safari-linux.png new file mode 100644 index 00000000000..125956c3978 Binary files /dev/null and b/core/src/components/input/test/states/input.e2e.ts-snapshots/input-readonly-solid-ionic-md-ltr-light-Mobile-Safari-linux.png differ