feat(input): add styles for ionic theme for fill=solid (#30417)

Issue number: Internal

---------

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->
- add fill solid to ionic theme input;
- move input.outline.scss to input.ionic.scss;
- add fill solid to states page;
- remove highlight styles for ionic theme;
- add tests for ionic solid input;

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
- fill:
https://ionic-framework-git-rou-11889-ionic1.vercel.app/src/components/input/test/fill?ionic:theme=ionic
- states:
https://ionic-framework-git-rou-11889-ionic1.vercel.app/src/components/input/test/states?ionic:theme=ionic

---------

Co-authored-by: ionitron <hi@ionicframework.com>
This commit is contained in:
João Ferreira
2025-05-23 19:10:21 +01:00
committed by GitHub
parent fc4e5ae287
commit 8b5215ff03
15 changed files with 377 additions and 164 deletions

View File

@@ -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};
}

View File

@@ -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 {

View File

@@ -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};
}

View File

@@ -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

View File

@@ -88,6 +88,35 @@
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 @@
</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 @@
<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>

View File

@@ -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`));
});
});
});
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB