feat(input-otp): add styling for the ionic theme (#30465)

Adds styles for the ionic theme, splits the scss files into common and native, and adds the theme to the e2e tests.

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
This commit is contained in:
Brandy Smith
2025-06-12 17:07:55 -04:00
committed by GitHub
parent ecfffe9486
commit 911684f83a
120 changed files with 332 additions and 131 deletions

View File

@@ -1047,50 +1047,76 @@ ion-input-otp,event,ionChange,InputOtpChangeEventDetail,true
ion-input-otp,event,ionComplete,InputOtpCompleteEventDetail,true
ion-input-otp,event,ionFocus,FocusEvent,true
ion-input-otp,event,ionInput,InputOtpInputEventDetail,true
ion-input-otp,css-prop,--background,ionic
ion-input-otp,css-prop,--background,ios
ion-input-otp,css-prop,--background,md
ion-input-otp,css-prop,--border-color,ionic
ion-input-otp,css-prop,--border-color,ios
ion-input-otp,css-prop,--border-color,md
ion-input-otp,css-prop,--border-radius,ionic
ion-input-otp,css-prop,--border-radius,ios
ion-input-otp,css-prop,--border-radius,md
ion-input-otp,css-prop,--border-style,ionic
ion-input-otp,css-prop,--border-style,ios
ion-input-otp,css-prop,--border-style,md
ion-input-otp,css-prop,--border-width,ionic
ion-input-otp,css-prop,--border-width,ios
ion-input-otp,css-prop,--border-width,md
ion-input-otp,css-prop,--color,ionic
ion-input-otp,css-prop,--color,ios
ion-input-otp,css-prop,--color,md
ion-input-otp,css-prop,--height,ionic
ion-input-otp,css-prop,--height,ios
ion-input-otp,css-prop,--height,md
ion-input-otp,css-prop,--highlight-color-focused,ionic
ion-input-otp,css-prop,--highlight-color-focused,ios
ion-input-otp,css-prop,--highlight-color-focused,md
ion-input-otp,css-prop,--highlight-color-invalid,ionic
ion-input-otp,css-prop,--highlight-color-invalid,ios
ion-input-otp,css-prop,--highlight-color-invalid,md
ion-input-otp,css-prop,--highlight-color-valid,ionic
ion-input-otp,css-prop,--highlight-color-valid,ios
ion-input-otp,css-prop,--highlight-color-valid,md
ion-input-otp,css-prop,--margin-bottom,ionic
ion-input-otp,css-prop,--margin-bottom,ios
ion-input-otp,css-prop,--margin-bottom,md
ion-input-otp,css-prop,--margin-end,ionic
ion-input-otp,css-prop,--margin-end,ios
ion-input-otp,css-prop,--margin-end,md
ion-input-otp,css-prop,--margin-start,ionic
ion-input-otp,css-prop,--margin-start,ios
ion-input-otp,css-prop,--margin-start,md
ion-input-otp,css-prop,--margin-top,ionic
ion-input-otp,css-prop,--margin-top,ios
ion-input-otp,css-prop,--margin-top,md
ion-input-otp,css-prop,--min-width,ionic
ion-input-otp,css-prop,--min-width,ios
ion-input-otp,css-prop,--min-width,md
ion-input-otp,css-prop,--padding-bottom,ionic
ion-input-otp,css-prop,--padding-bottom,ios
ion-input-otp,css-prop,--padding-bottom,md
ion-input-otp,css-prop,--padding-end,ionic
ion-input-otp,css-prop,--padding-end,ios
ion-input-otp,css-prop,--padding-end,md
ion-input-otp,css-prop,--padding-start,ionic
ion-input-otp,css-prop,--padding-start,ios
ion-input-otp,css-prop,--padding-start,md
ion-input-otp,css-prop,--padding-top,ionic
ion-input-otp,css-prop,--padding-top,ios
ion-input-otp,css-prop,--padding-top,md
ion-input-otp,css-prop,--separator-border-radius,ionic
ion-input-otp,css-prop,--separator-border-radius,ios
ion-input-otp,css-prop,--separator-border-radius,md
ion-input-otp,css-prop,--separator-color,ionic
ion-input-otp,css-prop,--separator-color,ios
ion-input-otp,css-prop,--separator-color,md
ion-input-otp,css-prop,--separator-height,ionic
ion-input-otp,css-prop,--separator-height,ios
ion-input-otp,css-prop,--separator-height,md
ion-input-otp,css-prop,--separator-width,ionic
ion-input-otp,css-prop,--separator-width,ios
ion-input-otp,css-prop,--separator-width,md
ion-input-otp,css-prop,--width,ionic
ion-input-otp,css-prop,--width,ios
ion-input-otp,css-prop,--width,md

View File

@@ -1,6 +1,6 @@
@import "../../themes/native/native.globals";
@import "../../themes/mixins";
// Input OTP
// Input OTP Common
// --------------------------------------------------
:host {
@@ -10,6 +10,7 @@
* @prop --border-radius: Border radius of the input boxes
*
* @prop --border-width: Border width of the input boxes
* @prop --border-style: Border style of the input boxes
* @prop --border-color: Border color of the input boxes
*
* @prop --color: Text color of the input
@@ -41,19 +42,7 @@
--margin-end: 0;
--margin-bottom: 0;
--margin-start: 0;
--padding-top: 16px;
--padding-end: 0;
--padding-bottom: 16px;
--padding-start: 0;
--color: initial;
--min-width: 40px;
--separator-width: 8px;
--separator-height: var(--separator-width);
--separator-border-radius: 999px;
--separator-color: #{$background-color-step-150};
--highlight-color-focused: #{ion-color(primary, base)};
--highlight-color-valid: #{ion-color(success, base)};
--highlight-color-invalid: #{ion-color(danger, base)};
/**
* This is a private API that is used to switch
@@ -65,8 +54,6 @@
display: block;
position: relative;
font-size: dynamic-font(14px);
}
.input-otp-group {
@@ -101,7 +88,7 @@
height: var(--height);
border-width: var(--border-width);
border-style: solid;
border-style: var(--border-style);
border-color: var(--border-color);
background: var(--background);
@@ -121,12 +108,6 @@
// ----------------------------------------------------------------
.input-otp-description {
color: $text-color-step-300;
font-size: dynamic-font(12px);
line-height: dynamic-font(20px);
text-align: center;
}
@@ -148,48 +129,6 @@
background: var(--separator-color);
}
// Sizes
// --------------------------------------------------
:host(.input-otp-size-small) {
--width: 40px;
--height: 40px;
}
:host(.input-otp-size-small) .input-otp-group {
gap: 8px;
}
:host(.input-otp-size-medium) {
--width: 48px;
--height: 48px;
}
:host(.input-otp-size-large) {
--width: 56px;
--height: 56px;
}
:host(.input-otp-size-medium) .input-otp-group,
:host(.input-otp-size-large) .input-otp-group {
gap: 12px;
}
// Shapes
// --------------------------------------------------
:host(.input-otp-shape-round) {
--border-radius: 16px;
}
:host(.input-otp-shape-soft) {
--border-radius: 8px;
}
:host(.input-otp-shape-rectangular) {
--border-radius: 0;
}
// Fills
// --------------------------------------------------
@@ -197,23 +136,9 @@
--background: none;
}
:host(.input-otp-fill-solid) {
--border-color: #{$background-color-step-50};
--background: #{$background-color-step-50};
}
// States
// --------------------------------------------------
:host(.input-otp-disabled) {
--color: #{$text-color-step-650};
}
:host(.input-otp-fill-outline.input-otp-disabled) {
--background: #{$background-color-step-50};
--border-color: #{$background-color-step-100};
}
:host(.input-otp-disabled),
:host(.input-otp-disabled) .native-input:disabled {
cursor: not-allowed;
@@ -225,16 +150,6 @@
outline: none;
}
:host(.input-otp-fill-outline.input-otp-readonly) {
--background: #{$background-color-step-50};
}
:host(.input-otp-fill-solid.input-otp-disabled),
:host(.input-otp-fill-solid.input-otp-readonly) {
--border-color: #{$background-color-step-100};
--background: #{$background-color-step-100};
}
// Input Highlight
// ----------------------------------------------------------------
@@ -270,38 +185,3 @@
:host(.ion-touched.ion-invalid) {
--border-color: var(--highlight-color);
}
// Colors
// ----------------------------------------------------------------
:host(.ion-color) {
--highlight-color-focused: #{current-color(base)};
}
// Outline border should match the current color
// and the solid border should match when focused
:host(.input-otp-fill-outline.ion-color) .native-input,
:host(.input-otp-fill-solid.ion-color) .native-input:focus {
border-color: current-color(base, 0.6);
}
// Invalid
:host(.input-otp-fill-outline.ion-color.ion-invalid) .native-input,
:host(.input-otp-fill-solid.ion-color.ion-invalid) .native-input,
:host(.input-otp-fill-outline.ion-color.has-focus.ion-invalid) .native-input,
:host(.input-otp-fill-solid.ion-color.has-focus.ion-invalid) .native-input {
border-color: ion-color(danger, base);
}
// Valid
:host(.input-otp-fill-outline.ion-color.ion-valid) .native-input,
:host(.input-otp-fill-solid.ion-color.ion-valid) .native-input,
:host(.input-otp-fill-outline.ion-color.has-focus.ion-valid) .native-input,
:host(.input-otp-fill-solid.ion-color.has-focus.ion-valid) .native-input {
border-color: ion-color(success, base);
}
// Outline & Disabled
:host(.input-otp-fill-outline.input-otp-disabled.ion-color) .native-input {
border-color: current-color(base, 0.3);
}

View File

@@ -0,0 +1,149 @@
@use "../../themes/ionic/ionic.globals.scss" as globals;
@use "./input-otp.common";
// Ionic Input OTP
// --------------------------------------------------
:host {
--padding-top: #{globals.$ion-space-400};
--padding-end: #{globals.$ion-space-0};
--padding-bottom: #{globals.$ion-space-400};
--padding-start: #{globals.$ion-space-0};
--color: #{globals.$ion-text-default};
--min-width: #{globals.$ion-scale-1000};
--border-width: #{globals.$ion-border-size-025};
--border-style: #{globals.$ion-border-style-solid};
--border-color: #{globals.$ion-border-input-default};
--separator-width: #{globals.$ion-scale-200};
--separator-border-radius: #{globals.$ion-border-radius-full};
--separator-color: #{globals.$ion-bg-neutral-subtle-press};
--highlight-color-focused: #{globals.$ion-border-focus-default};
--highlight-color-valid: #{globals.$ion-border-success};
--highlight-color-invalid: #{globals.$ion-border-danger-default};
@include globals.typography(globals.$ion-body-md-regular);
}
:host(.has-focus) .native-input:focus {
--border-width: #{globals.$ion-border-size-050};
}
// Input Description
// ----------------------------------------------------------------
.input-otp-description {
@include globals.typography(globals.$ion-body-sm-medium);
color: globals.$ion-text-subtlest;
}
// Sizes
// --------------------------------------------------
:host(.input-otp-size-small) {
--width: #{globals.$ion-scale-1000};
--height: #{globals.$ion-scale-1000};
}
:host(.input-otp-size-small) .input-otp-group {
gap: globals.$ion-space-200;
}
:host(.input-otp-size-medium) {
--width: #{globals.$ion-scale-1200};
--height: #{globals.$ion-scale-1200};
}
:host(.input-otp-size-large) {
--width: #{globals.$ion-scale-1400};
--height: #{globals.$ion-scale-1400};
}
:host(.input-otp-size-medium) .input-otp-group,
:host(.input-otp-size-large) .input-otp-group {
gap: globals.$ion-space-300;
}
// Shapes
// --------------------------------------------------
:host(.input-otp-shape-round) {
--border-radius: #{globals.$ion-border-radius-400};
}
:host(.input-otp-shape-soft) {
--border-radius: #{globals.$ion-border-radius-200};
}
:host(.input-otp-shape-rectangular) {
--border-radius: #{globals.$ion-border-radius-0};
}
// Fills
// --------------------------------------------------
:host(.input-otp-fill-solid) {
--border-color: #{globals.$ion-bg-input-bold-default};
--background: #{globals.$ion-bg-input-bold-default};
}
// States
// --------------------------------------------------
:host(.input-otp-disabled) {
--color: #{globals.$ion-text-disabled};
}
:host(.input-otp-fill-outline.input-otp-disabled) {
--background: #{globals.$ion-bg-input-disabled};
--border-color: #{globals.$ion-border-disabled};
}
:host(.input-otp-fill-outline.input-otp-readonly) {
--background: #{globals.$ion-bg-input-read-only};
}
:host(.input-otp-fill-solid.input-otp-disabled) {
--border-color: #{globals.$ion-bg-input-bold-disabled};
--background: #{globals.$ion-bg-input-bold-disabled};
}
:host(.input-otp-fill-solid.input-otp-readonly) {
--border-color: #{globals.$ion-bg-input-bold-read-only};
--background: #{globals.$ion-bg-input-bold-read-only};
}
// Colors
// ----------------------------------------------------------------
:host(.ion-color) {
--highlight-color-focused: #{globals.current-color(base)};
}
// Outline border should match the current color
// and the solid border should match when focused
:host(.input-otp-fill-outline.ion-color) .native-input,
:host(.input-otp-fill-solid.ion-color) .native-input:focus {
border-color: globals.current-color(base, 0.6);
}
// Invalid
:host(.input-otp-fill-outline.ion-color.ion-invalid) .native-input,
:host(.input-otp-fill-solid.ion-color.ion-invalid) .native-input,
:host(.input-otp-fill-outline.ion-color.has-focus.ion-invalid) .native-input,
:host(.input-otp-fill-solid.ion-color.has-focus.ion-invalid) .native-input {
border-color: globals.ion-color(danger, base);
}
// Valid
:host(.input-otp-fill-outline.ion-color.ion-valid) .native-input,
:host(.input-otp-fill-solid.ion-color.ion-valid) .native-input,
:host(.input-otp-fill-outline.ion-color.has-focus.ion-valid) .native-input,
:host(.input-otp-fill-solid.ion-color.has-focus.ion-valid) .native-input {
border-color: globals.ion-color(success, base);
}
// Outline & Disabled
:host(.input-otp-fill-outline.input-otp-disabled.ion-color) .native-input {
border-color: globals.current-color(base, 0.3);
}

View File

@@ -1,4 +1,4 @@
@import "./input-otp";
@import "./input-otp.native";
@import "../../themes/native/native.globals.ios";
// iOS Input OTP

View File

@@ -1,4 +1,4 @@
@import "./input-otp";
@import "./input-otp.native";
@import "../../themes/native/native.globals.md";
// Material Design Input OTP

View File

@@ -0,0 +1,145 @@
@import "../../themes/native/native.globals";
@import "./input-otp.common";
// Input OTP Native
// --------------------------------------------------
:host {
--padding-top: 16px;
--padding-end: 0;
--padding-bottom: 16px;
--padding-start: 0;
--color: initial;
--min-width: 40px;
--border-style: solid;
--separator-width: 8px;
--separator-border-radius: 999px;
--separator-color: #{$background-color-step-150};
--highlight-color-focused: #{ion-color(primary, base)};
--highlight-color-valid: #{ion-color(success, base)};
--highlight-color-invalid: #{ion-color(danger, base)};
font-size: dynamic-font(14px);
}
// Input Description
// ----------------------------------------------------------------
.input-otp-description {
color: $text-color-step-300;
font-size: dynamic-font(12px);
line-height: dynamic-font(20px);
}
// Sizes
// --------------------------------------------------
:host(.input-otp-size-small) {
--width: 40px;
--height: 40px;
}
:host(.input-otp-size-small) .input-otp-group {
gap: 8px;
}
:host(.input-otp-size-medium) {
--width: 48px;
--height: 48px;
}
:host(.input-otp-size-large) {
--width: 56px;
--height: 56px;
}
:host(.input-otp-size-medium) .input-otp-group,
:host(.input-otp-size-large) .input-otp-group {
gap: 12px;
}
// Shapes
// --------------------------------------------------
:host(.input-otp-shape-round) {
--border-radius: 16px;
}
:host(.input-otp-shape-soft) {
--border-radius: 8px;
}
:host(.input-otp-shape-rectangular) {
--border-radius: 0;
}
// Fills
// --------------------------------------------------
:host(.input-otp-fill-outline) {
--background: none;
}
:host(.input-otp-fill-solid) {
--border-color: #{$background-color-step-50};
--background: #{$background-color-step-50};
}
// States
// --------------------------------------------------
:host(.input-otp-disabled) {
--color: #{$text-color-step-650};
}
:host(.input-otp-fill-outline.input-otp-disabled) {
--background: #{$background-color-step-50};
--border-color: #{$background-color-step-100};
}
:host(.input-otp-fill-outline.input-otp-readonly) {
--background: #{$background-color-step-50};
}
:host(.input-otp-fill-solid.input-otp-disabled),
:host(.input-otp-fill-solid.input-otp-readonly) {
--border-color: #{$background-color-step-100};
--background: #{$background-color-step-100};
}
// Colors
// ----------------------------------------------------------------
:host(.ion-color) {
--highlight-color-focused: #{current-color(base)};
}
// Outline border should match the current color
// and the solid border should match when focused
:host(.input-otp-fill-outline.ion-color) .native-input,
:host(.input-otp-fill-solid.ion-color) .native-input:focus {
border-color: current-color(base, 0.6);
}
// Invalid
:host(.input-otp-fill-outline.ion-color.ion-invalid) .native-input,
:host(.input-otp-fill-solid.ion-color.ion-invalid) .native-input,
:host(.input-otp-fill-outline.ion-color.has-focus.ion-invalid) .native-input,
:host(.input-otp-fill-solid.ion-color.has-focus.ion-invalid) .native-input {
border-color: ion-color(danger, base);
}
// Valid
:host(.input-otp-fill-outline.ion-color.ion-valid) .native-input,
:host(.input-otp-fill-solid.ion-color.ion-valid) .native-input,
:host(.input-otp-fill-outline.ion-color.has-focus.ion-valid) .native-input,
:host(.input-otp-fill-solid.ion-color.has-focus.ion-valid) .native-input {
border-color: ion-color(success, base);
}
// Outline & Disabled
:host(.input-otp-fill-outline.input-otp-disabled.ion-color) .native-input {
border-color: current-color(base, 0.3);
}

View File

@@ -21,6 +21,7 @@ import type {
styleUrls: {
ios: 'input-otp.ios.scss',
md: 'input-otp.md.scss',
ionic: 'input-otp.ionic.scss',
},
scoped: true,
})

View File

@@ -3,7 +3,7 @@ import { configs, test } from '@utils/test/playwright';
const VALID_FILLS = ['outline', 'solid'];
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('input-otp: color'), () => {
// Test all colors with all fills
VALID_FILLS.forEach((fill) => {

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -3,7 +3,7 @@ import { configs, test } from '@utils/test/playwright';
const VALID_FILLS = ['outline', 'solid'];
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('input-otp: fill'), () => {
VALID_FILLS.forEach((fill) => {
test(`${fill} fill should not have visual regressions`, async ({ page }) => {

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -30,7 +30,7 @@ const collectWarnings = async (page: E2EPage): Promise<string[]> => {
return warnings;
};
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('input-otp: separators'), () => {
// Test separators with all sizes
VALID_SIZES.forEach((size) => {

View File

@@ -4,7 +4,7 @@ import { configs, test } from '@utils/test/playwright';
const VALID_SHAPES = ['rectangular', 'round', 'soft'];
const VALID_SIZES = ['small', 'medium', 'large'];
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('input-otp: shape'), () => {
// Test all shapes with all sizes
VALID_SHAPES.forEach((shape) => {

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Some files were not shown because too many files have changed in this diff Show More