From 1c6621e7d2f01777d8d136c15d4ef3e2ecfc0dfd Mon Sep 17 00:00:00 2001 From: ShaneK Date: Tue, 27 Oct 2026 13:12:25 -0700 Subject: [PATCH 1/2] feat(input-otp): add helper and error text and input label formatter --- core/api.txt | 3 + core/src/components.d.ts | 30 ++- .../input-otp/input-otp-interface.ts | 2 + .../components/input-otp/input-otp.ios.scss | 2 +- .../components/input-otp/input-otp.md.scss | 2 +- core/src/components/input-otp/input-otp.scss | 44 +++- core/src/components/input-otp/input-otp.tsx | 219 +++++++++++++++++- .../components/input-otp/test/a11y/index.html | 64 +++++ .../input-otp/test/a11y/input-otp.e2e.ts | 113 +++++++++ .../input-otp/test/basic/index.html | 20 +- .../test/bottom-content/input-otp.e2e.ts | 104 +++++++++ .../input-otp/test/fill/input-otp.e2e.ts | 55 +++++ .../input-otp/test/input-otp.spec.ts | 215 +++++++++++++++++ .../angular/src/lazy/directives/proxies.ts | 4 +- .../src/standalone/directives/input-otp.ts | 3 + packages/vue/src/proxies.ts | 3 + 16 files changed, 860 insertions(+), 23 deletions(-) create mode 100644 core/src/components/input-otp/test/a11y/index.html create mode 100644 core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts create mode 100644 core/src/components/input-otp/test/input-otp.spec.ts diff --git a/core/api.txt b/core/api.txt index 5a79c3903aa..074a30b2035 100644 --- a/core/api.txt +++ b/core/api.txt @@ -794,7 +794,10 @@ ion-input-otp,scoped ion-input-otp,prop,autocapitalize,string,'off',false,false ion-input-otp,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record | undefined,undefined,false,true ion-input-otp,prop,disabled,boolean,false,false,true +ion-input-otp,prop,errorText,string | undefined,undefined,false,false ion-input-otp,prop,fill,"outline" | "solid" | undefined,'outline',false,false +ion-input-otp,prop,helperText,string | undefined,undefined,false,false +ion-input-otp,prop,inputLabelFormatter,((index: number, length: number) => string) | undefined,undefined,false,false ion-input-otp,prop,inputmode,"decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined,undefined,false,false ion-input-otp,prop,length,number,4,false,false ion-input-otp,prop,pattern,string | undefined,undefined,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 22e751460f0..2816528dc72 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -18,7 +18,7 @@ import { ScrollBaseDetail, ScrollDetail } from "./components/content/content-int import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimeParts, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; import { SpinnerTypes } from "./components/spinner/spinner-configs"; import { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface"; -import { InputOtpChangeEventDetail, InputOtpCompleteEventDetail, InputOtpInputEventDetail } from "./components/input-otp/input-otp-interface"; +import { InputOtpChangeEventDetail, InputOtpCompleteEventDetail, InputOtpInputEventDetail, InputOtpLabelFormatter } from "./components/input-otp/input-otp-interface"; import { MenuChangeEventDetail, MenuCloseEventDetail, MenuType, Side } from "./components/menu/menu-interface"; import { ModalBreakpointChangeEventDetail, ModalDragEventDetail, ModalHandleBehavior } from "./components/modal/modal-interface"; import { NavComponent, NavComponentWithProps, NavOptions, RouterOutletOptions, SwipeGestureHandler, TransitionDoneFn, TransitionInstruction } from "./components/nav/nav-interface"; @@ -55,7 +55,7 @@ export { ScrollBaseDetail, ScrollDetail } from "./components/content/content-int export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimeParts, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; export { SpinnerTypes } from "./components/spinner/spinner-configs"; export { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface"; -export { InputOtpChangeEventDetail, InputOtpCompleteEventDetail, InputOtpInputEventDetail } from "./components/input-otp/input-otp-interface"; +export { InputOtpChangeEventDetail, InputOtpCompleteEventDetail, InputOtpInputEventDetail, InputOtpLabelFormatter } from "./components/input-otp/input-otp-interface"; export { MenuChangeEventDetail, MenuCloseEventDetail, MenuType, Side } from "./components/menu/menu-interface"; export { ModalBreakpointChangeEventDetail, ModalDragEventDetail, ModalHandleBehavior } from "./components/modal/modal-interface"; export { NavComponent, NavComponentWithProps, NavOptions, RouterOutletOptions, SwipeGestureHandler, TransitionDoneFn, TransitionInstruction } from "./components/nav/nav-interface"; @@ -1468,11 +1468,23 @@ export namespace Components { * @default false */ "disabled": boolean; + /** + * Text that is placed under the input boxes and displayed when an error is detected. + */ + "errorText"?: string; /** * The fill for the input boxes. If `"solid"` the input boxes will have a background. If `"outline"` the input boxes will be transparent with a border. * @default 'outline' */ "fill"?: 'outline' | 'solid'; + /** + * Text that is placed under the input boxes and displayed when no error is detected. + */ + "helperText"?: string; + /** + * A callback used to format the accessible name of each input box. It receives the 0-based `index` of the box and the total `length`. By default the name is "Input 1 of 4", or "PIN, 1 of 4" with an `aria-label` of "PIN". See https://ionicframework.com/docs/troubleshooting/runtime#accessing-this if you need to access `this` from within the callback. + */ + "inputLabelFormatter"?: InputOtpLabelFormatter; /** * A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`. For numbers (type="number"): "numeric" For text (type="text"): "text" */ @@ -6628,11 +6640,23 @@ declare namespace LocalJSX { * @default false */ "disabled"?: boolean; + /** + * Text that is placed under the input boxes and displayed when an error is detected. + */ + "errorText"?: string; /** * The fill for the input boxes. If `"solid"` the input boxes will have a background. If `"outline"` the input boxes will be transparent with a border. * @default 'outline' */ "fill"?: 'outline' | 'solid'; + /** + * Text that is placed under the input boxes and displayed when no error is detected. + */ + "helperText"?: string; + /** + * A callback used to format the accessible name of each input box. It receives the 0-based `index` of the box and the total `length`. By default the name is "Input 1 of 4", or "PIN, 1 of 4" with an `aria-label` of "PIN". See https://ionicframework.com/docs/troubleshooting/runtime#accessing-this if you need to access `this` from within the callback. + */ + "inputLabelFormatter"?: InputOtpLabelFormatter; /** * A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`. For numbers (type="number"): "numeric" For text (type="text"): "text" */ @@ -9236,7 +9260,9 @@ declare namespace LocalJSX { "autocapitalize": string; "color": Color; "disabled": boolean; + "errorText": string; "fill": 'outline' | 'solid'; + "helperText": string; "inputmode": 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'; "length": number; "pattern": string; diff --git a/core/src/components/input-otp/input-otp-interface.ts b/core/src/components/input-otp/input-otp-interface.ts index 8fb289cfec4..8d7daf48011 100644 --- a/core/src/components/input-otp/input-otp-interface.ts +++ b/core/src/components/input-otp/input-otp-interface.ts @@ -17,6 +17,8 @@ export interface InputOtpCompleteEventDetail { event?: Event; } +export type InputOtpLabelFormatter = (index: number, length: number) => string; + export interface InputOtpCustomEvent extends CustomEvent { detail: T; target: HTMLIonInputOtpElement; diff --git a/core/src/components/input-otp/input-otp.ios.scss b/core/src/components/input-otp/input-otp.ios.scss index 8f0e8176784..fd4eaa6b96c 100644 --- a/core/src/components/input-otp/input-otp.ios.scss +++ b/core/src/components/input-otp/input-otp.ios.scss @@ -16,5 +16,5 @@ // -------------------------------------------------- :host(.input-otp-fill-outline) { - --border-color: #{$item-ios-border-color}; + --internal-border-color: #{$item-ios-border-color}; } diff --git a/core/src/components/input-otp/input-otp.md.scss b/core/src/components/input-otp/input-otp.md.scss index f4e2b16e97e..60563673fa8 100644 --- a/core/src/components/input-otp/input-otp.md.scss +++ b/core/src/components/input-otp/input-otp.md.scss @@ -16,5 +16,5 @@ // -------------------------------------------------- :host(.input-otp-fill-outline) { - --border-color: #{$background-color-step-300}; + --internal-border-color: #{$background-color-step-300}; } diff --git a/core/src/components/input-otp/input-otp.scss b/core/src/components/input-otp/input-otp.scss index 2b8b12ee585..a47bd460652 100644 --- a/core/src/components/input-otp/input-otp.scss +++ b/core/src/components/input-otp/input-otp.scss @@ -63,6 +63,9 @@ */ --highlight-color: var(--highlight-color-focused); + // Don't inherit `--border-color` from ancestors like `ion-modal` + --border-color: initial; + display: block; position: relative; @@ -102,7 +105,9 @@ border-width: var(--border-width); border-style: solid; - border-color: var(--border-color); + // The defaults use `--internal-border-color` since a host + // selector can't beat `:host(.input-otp-fill-*)` + border-color: var(--border-color, var(--internal-border-color)); background: var(--background); color: var(--color); @@ -134,6 +139,37 @@ display: none; } +// Input Hint Text +// ---------------------------------------------------------------- + +.input-otp-bottom { + font-size: dynamic-font(12px); + + line-height: dynamic-font(20px); + + text-align: center; +} + +.input-otp-bottom .error-text { + display: none; + + color: var(--highlight-color-invalid); +} + +.input-otp-bottom .helper-text { + display: block; + + color: $text-color-step-300; +} + +:host(.ion-touched.ion-invalid) .input-otp-bottom .error-text { + display: block; +} + +:host(.ion-touched.ion-invalid) .input-otp-bottom .helper-text { + display: none; +} + // Input Separator // ---------------------------------------------------------------- @@ -198,7 +234,7 @@ } :host(.input-otp-fill-solid) { - --border-color: #{$background-color-step-50}; + --internal-border-color: #{$background-color-step-50}; --background: #{$background-color-step-50}; } @@ -211,7 +247,7 @@ :host(.input-otp-fill-outline.input-otp-disabled) { --background: #{$background-color-step-50}; - --border-color: #{$background-color-step-100}; + --internal-border-color: #{$background-color-step-100}; } :host(.input-otp-disabled), @@ -231,7 +267,7 @@ :host(.input-otp-fill-solid.input-otp-disabled), :host(.input-otp-fill-solid.input-otp-readonly) { - --border-color: #{$background-color-step-100}; + --internal-border-color: #{$background-color-step-100}; --background: #{$background-color-step-100}; } diff --git a/core/src/components/input-otp/input-otp.tsx b/core/src/components/input-otp/input-otp.tsx index 1c84b3c9a42..59955488d45 100644 --- a/core/src/components/input-otp/input-otp.tsx +++ b/core/src/components/input-otp/input-otp.tsx @@ -1,8 +1,24 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Component, Element, Event, Fragment, Host, Method, Prop, State, h, Watch } from '@stencil/core'; +import { + Build, + Component, + Element, + Event, + Fragment, + Host, + Method, + Prop, + State, + forceUpdate, + h, + Watch, +} from '@stencil/core'; +import type { AttributeController } from '@utils/attribute-controller'; +import { createAttributeController } from '@utils/attribute-controller'; +import { checkInvalidState } from '@utils/forms'; import type { Attributes } from '@utils/helpers'; -import { inheritAriaAttributes } from '@utils/helpers'; -import { printIonWarning } from '@utils/logging'; +import { ariaAttributes } from '@utils/helpers'; +import { printIonError, printIonWarning } from '@utils/logging'; import { isRTL } from '@utils/rtl'; import { createColorClasses } from '@utils/theme'; @@ -13,6 +29,7 @@ import type { InputOtpChangeEventDetail, InputOtpCompleteEventDetail, InputOtpInputEventDetail, + InputOtpLabelFormatter, } from './input-otp-interface'; @Component({ @@ -24,9 +41,13 @@ import type { scoped: true, }) export class InputOTP implements ComponentInterface { - private inheritedAttributes: Attributes = {}; + private ariaController?: AttributeController; private inputRefs: HTMLInputElement[] = []; private inputId = `ion-input-otp-${inputIds++}`; + private helperTextId = `${this.inputId}-helper-text`; + private errorTextId = `${this.inputId}-error-text`; + private descriptionId = `${this.inputId}-description`; + private validationObserver?: MutationObserver; private parsedSeparators: number[] = []; /** @@ -55,6 +76,8 @@ export class InputOTP implements ComponentInterface { @State() hasFocus = false; @State() private previousInputValues: string[] = []; + @State() isInvalid = false; + /** * Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user. * Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`. @@ -73,12 +96,32 @@ export class InputOTP implements ComponentInterface { */ @Prop({ reflect: true }) disabled = false; + /** + * Text that is placed under the input boxes and displayed when an error is detected. + */ + @Prop() errorText?: string; + /** * The fill for the input boxes. If `"solid"` the input boxes will have a background. If * `"outline"` the input boxes will be transparent with a border. */ @Prop() fill?: 'outline' | 'solid' = 'outline'; + /** + * Text that is placed under the input boxes and displayed when no error is detected. + */ + @Prop() helperText?: string; + + /** + * A callback used to format the accessible name of each input box. It + * receives the 0-based `index` of the box and the total `length`. By default + * the name is "Input 1 of 4", or "PIN, 1 of 4" with an `aria-label` of "PIN". + * + * See https://ionicframework.com/docs/troubleshooting/runtime#accessing-this + * if you need to access `this` from within the callback. + */ + @Prop() inputLabelFormatter?: InputOtpLabelFormatter; + /** * A hint to the browser for which keyboard to display. * Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, @@ -197,6 +240,13 @@ export class InputOTP implements ComponentInterface { } } + // The host keeps these (see `HOST_ARIA_ATTRIBUTES`), so re-render when they change + @Watch('aria-describedby') + @Watch('aria-invalid') + onHostAriaChange() { + forceUpdate(this); + } + @Watch('value') valueChanged() { this.initializeValues(); @@ -274,14 +324,59 @@ export class InputOTP implements ComponentInterface { this.parsedSeparators = separatorValues.filter((pos) => pos <= length); } + connectedCallback() { + const { el } = this; + + if (Build.isBrowser && typeof MutationObserver !== 'undefined') { + this.validationObserver = new MutationObserver(() => { + const newIsInvalid = checkInvalidState(el); + if (this.isInvalid !== newIsInvalid) { + this.isInvalid = newIsInvalid; + // Force a re-render to update aria-describedby immediately + forceUpdate(this); + } + }); + + this.validationObserver.observe(el, { + attributes: true, + attributeFilter: ['class'], + }); + } + + this.isInvalid = checkInvalidState(el); + + // Resumes watching after a move, since `componentWillLoad` only runs once + this.ariaController?.init(); + } + + disconnectedCallback() { + this.ariaController?.destroy(); + + if (this.validationObserver) { + this.validationObserver.disconnect(); + this.validationObserver = undefined; + } + } + componentWillLoad() { - this.inheritedAttributes = inheritAriaAttributes(this.el); + // Only read `role` at load so a later host write isn't copied to the group + this.ariaController = createAttributeController( + this.el, + ariaAttributes.filter((attr) => !HOST_ARIA_ATTRIBUTES.includes(attr)), + () => forceUpdate(this), + ['role'] + ); this.processSeparators(); this.initializeValues(); } componentDidLoad() { this.updateTabIndexes(); + + // The description only exists after the first render, so render again to reference it + if (this.getDescriptionText()) { + forceUpdate(this); + } } componentDidRender() { @@ -818,18 +913,90 @@ export class InputOTP implements ComponentInterface { return this.parsedSeparators.includes(index + 1) && index < length - 1; } + /** + * Returns `undefined` until the first render creates the description container. + */ + private getDescriptionText() { + return this.el.querySelector('.input-otp-description')?.textContent?.trim(); + } + + private getHintTextId(): string | undefined { + const { isInvalid, helperText, errorText, helperTextId, errorTextId } = this; + + if (isInvalid && errorText) { + return errorTextId; + } + + if (helperText) { + return helperTextId; + } + + return undefined; + } + + private renderHintText() { + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; + + return [ +
+ {!isInvalid ? helperText : null} +
, + , + ]; + } + + private renderBottomContent() { + const { helperText, errorText } = this; + + if (!helperText && !errorText) { + return; + } + + return
{this.renderHintText()}
; + } + + /** + * A custom formatter is used as is so translators control the whole name. + * With a host `aria-labelledby`, each box also references itself so its + * position is still read. + */ + private getInputLabelAttributes(index: number, hostAttributes: Attributes) { + const { inputId, inputLabelFormatter, length } = this; + const hostLabel = hostAttributes['aria-label']; + const hostLabelledBy = hostAttributes['aria-labelledby']; + const position = `${index + 1} of ${length}`; + + if (inputLabelFormatter !== undefined) { + try { + return { 'aria-label': inputLabelFormatter(index, length) }; + } catch (e) { + printIonError('[ion-input-otp] - Exception in provided `inputLabelFormatter`:', e); + } + } + + if (hostLabelledBy) { + return { 'aria-label': position, 'aria-labelledby': `${hostLabelledBy} ${inputId}-${index}` }; + } + + return { 'aria-label': hostLabel ? `${hostLabel}, ${position}` : `Input ${position}` }; + } + render() { const { + ariaController, autocapitalize, color, + descriptionId, disabled, el, fill, hasFocus, - inheritedAttributes, inputId, inputRefs, inputValues, + isInvalid, length, readonly, shape, @@ -839,7 +1006,22 @@ export class InputOTP implements ComponentInterface { const inputmode = this.getInputmode(); const tabbableIndex = this.getTabbableIndex(); const pattern = this.getPattern(); - const hasDescription = el.querySelector('.input-otp-description')?.textContent?.trim() !== ''; + const descriptionText = this.getDescriptionText(); + // Keep the description visible until the first render can check it + const hasDescription = descriptionText !== ''; + + /** + * Screen readers only announce these from the focused input. The + * description stays on the group so it isn't repeated for every input. + */ + const { + 'aria-errormessage': ariaErrorMessage, + 'aria-required': ariaRequired, + ...groupAttributes + } = ariaController?.attributes ?? {}; + const hostDescribedBy = el.getAttribute('aria-describedby'); + const hostInvalid = el.getAttribute('aria-invalid') ?? undefined; + const describedBy = [hostDescribedBy, this.getHintTextId()].filter(Boolean).join(' '); return ( -
+
{Array.from({ length }).map((_, index) => ( <>
+ {this.renderBottomContent()} ); } } let inputIds = 0; + +/** + * The attribute controller removes these from the host, so an app could never + * clear them. They stay on the host instead, which only works for global + * attributes since the host has no role. + */ +const HOST_ARIA_ATTRIBUTES = ['aria-describedby', 'aria-invalid']; diff --git a/core/src/components/input-otp/test/a11y/index.html b/core/src/components/input-otp/test/a11y/index.html new file mode 100644 index 00000000000..5b3cdc71c47 --- /dev/null +++ b/core/src/components/input-otp/test/a11y/index.html @@ -0,0 +1,64 @@ + + + + + Input OTP - a11y + + + + + + + + + +
+

Input OTP

+ +

Host aria-label, description and hint text

+ + Didn't get a code? + + + +

Host aria-labelledby

+

Verification code

+ + +

Label formatter

+ + +

Host aria-describedby and aria-invalid

+ +

The server rejected this code

+ +
+ + + + diff --git a/core/src/components/input-otp/test/a11y/input-otp.e2e.ts b/core/src/components/input-otp/test/a11y/input-otp.e2e.ts index 7cc5374d8ef..63f2085e66c 100644 --- a/core/src/components/input-otp/test/a11y/input-otp.e2e.ts +++ b/core/src/components/input-otp/test/a11y/input-otp.e2e.ts @@ -128,3 +128,116 @@ configs().forEach(({ title, config }) => { }); }); }); + +/** + * This behavior does not vary across modes/directions + */ +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('input-otp: host aria attributes'), () => { + test('should forward aria-invalid and aria-describedby set on the host after load', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31619', + }); + + await page.setContent(``, config); + + const inputOtp = page.locator('ion-input-otp'); + const inputBoxes = page.locator('ion-input-otp input'); + + await inputOtp.evaluate((el) => { + el.setAttribute('aria-invalid', 'true'); + el.setAttribute('aria-describedby', 'server-error'); + }); + + for (const input of await inputBoxes.all()) { + await expect(input).toHaveAttribute('aria-invalid', 'true'); + await expect(input).toHaveAttribute('aria-describedby', 'server-error'); + } + }); + + test('should clear aria-invalid and aria-describedby from the inputs when removed from the host', async ({ + page, + }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31619', + }); + + await page.setContent( + ``, + config + ); + + const inputOtp = page.locator('ion-input-otp'); + const inputBoxes = page.locator('ion-input-otp input'); + + await inputOtp.evaluate((el) => { + el.removeAttribute('aria-invalid'); + el.removeAttribute('aria-describedby'); + }); + + for (const input of await inputBoxes.all()) { + await expect(input).not.toHaveAttribute('aria-invalid'); + await expect(input).not.toHaveAttribute('aria-describedby'); + } + }); + + test('should rename the inputs when the host aria-label changes', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31619', + }); + + await page.setContent(``, config); + + await page.locator('ion-input-otp').evaluate((el) => el.setAttribute('aria-label', 'Code')); + + const inputBoxes = page.locator('ion-input-otp input'); + await expect(inputBoxes.nth(0)).toHaveAttribute('aria-label', 'Code, 1 of 2'); + await expect(inputBoxes.nth(1)).toHaveAttribute('aria-label', 'Code, 2 of 2'); + await expect(page.locator('ion-input-otp .input-otp-group')).toHaveAttribute('aria-label', 'Code'); + }); + + test('should describe the group, not each input, with the slotted description', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31619', + }); + + await page.setContent(`Didn't get a code?`, config); + + const description = page.locator('ion-input-otp .input-otp-description'); + await expect(description).toBeVisible(); + const descriptionId = await description.getAttribute('id'); + + await expect(page.locator('ion-input-otp .input-otp-group')).toHaveAttribute('aria-describedby', descriptionId!); + for (const input of await page.locator('ion-input-otp input').all()) { + await expect(input).not.toHaveAttribute('aria-describedby'); + } + }); + + test('should restore the default labels when a host aria-label set after load is removed', async ({ + page, + }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31619', + }); + + await page.setContent(``, config); + + const inputOtp = page.locator('ion-input-otp'); + await inputOtp.evaluate((el) => el.setAttribute('aria-label', 'PIN')); + await expect(page.locator('ion-input-otp input').nth(0)).toHaveAttribute('aria-label', 'PIN, 1 of 2'); + + await inputOtp.evaluate((el) => el.removeAttribute('aria-label')); + + await expect(page.locator('ion-input-otp .input-otp-group')).toHaveAttribute( + 'aria-label', + 'One-time password input' + ); + await expect(page.locator('ion-input-otp input').nth(0)).toHaveAttribute('aria-label', 'Input 1 of 2'); + }); + }); +}); diff --git a/core/src/components/input-otp/test/basic/index.html b/core/src/components/input-otp/test/basic/index.html index 71e31de7aff..c43f0c21c17 100644 --- a/core/src/components/input-otp/test/basic/index.html +++ b/core/src/components/input-otp/test/basic/index.html @@ -77,11 +77,23 @@

Readonly

Invalid / Touched

- Description - Description + Description + Description

Valid / Focused

- Description - Description + Description + Description
diff --git a/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts b/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts new file mode 100644 index 00000000000..70dd0d90ea5 --- /dev/null +++ b/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts @@ -0,0 +1,104 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +/** + * The hint text styles are the same across modes and directions + */ +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('input-otp: hint text rendering'), () => { + test('should not have visual regressions when rendering helper text with a description', async ({ page }) => { + await page.setContent(`Description`, config); + + const inputOtp = page.locator('ion-input-otp'); + await expect(inputOtp).toHaveScreenshot(screenshot(`input-otp-helper-text-description`)); + }); + + test('should not have visual regressions when rendering helper text with wrapping text', async ({ page }) => { + await page.setContent( + ``, + config + ); + + const inputOtp = page.locator('ion-input-otp'); + await expect(inputOtp).toHaveScreenshot(screenshot(`input-otp-helper-text-wrapping`)); + }); + + test('should not have visual regressions when rendering error text', async ({ page }) => { + await page.setContent( + ``, + config + ); + + const inputOtp = page.locator('ion-input-otp'); + await expect(inputOtp).toHaveScreenshot(screenshot(`input-otp-error-text`)); + }); + }); +}); + +/** + * Customizing supporting text is the same across modes and directions + */ +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('input-otp: hint text customization'), () => { + test('should not have visual regressions when rendering error text with a custom css variable', async ({ + page, + }) => { + await page.setContent( + ` + + + `, + config + ); + + const inputOtp = page.locator('ion-input-otp'); + await expect(inputOtp).toHaveScreenshot(screenshot(`input-otp-error-text-custom-css-var`)); + }); + }); +}); + +/** + * This behavior does not vary across modes/directions + */ +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('input-otp: hint text functionality'), () => { + test('should switch every input between helper and error text as the validation classes change', async ({ + page, + }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31619', + }); + + await page.setContent( + ``, + config + ); + + const inputOtp = page.locator('ion-input-otp'); + const inputBoxes = page.locator('ion-input-otp input'); + const helperTextId = await page.locator('ion-input-otp .helper-text').getAttribute('id'); + const errorTextId = await page.locator('ion-input-otp .error-text').getAttribute('id'); + + await inputOtp.evaluate((el) => el.classList.add('ion-touched', 'ion-invalid')); + + for (const input of await inputBoxes.all()) { + await expect(input).toHaveAttribute('aria-describedby', errorTextId!); + await expect(input).toHaveAttribute('aria-invalid', 'true'); + } + await expect(page.locator('ion-input-otp .error-text')).toHaveText('Wrong code'); + + await inputOtp.evaluate((el) => el.classList.remove('ion-invalid')); + + for (const input of await inputBoxes.all()) { + await expect(input).toHaveAttribute('aria-describedby', helperTextId!); + await expect(input).not.toHaveAttribute('aria-invalid'); + } + await expect(page.locator('ion-input-otp .helper-text')).toHaveText('Check your email'); + }); + }); +}); diff --git a/core/src/components/input-otp/test/fill/input-otp.e2e.ts b/core/src/components/input-otp/test/fill/input-otp.e2e.ts index b20e93a4591..dbf49f91557 100644 --- a/core/src/components/input-otp/test/fill/input-otp.e2e.ts +++ b/core/src/components/input-otp/test/fill/input-otp.e2e.ts @@ -42,3 +42,58 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { }); }); }); + +/** + * The fill defaults differ between modes but not directions. + * These check computed colors since the value that wins is what's under test. + */ +configs({ directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('input-otp: fill border color'), () => { + VALID_FILLS.forEach((fill) => { + test(`${fill} fill should use a --border-color set on the host`, async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31619', + }); + + await page.setContent( + ` + + + `, + config + ); + + const input = page.locator('ion-input-otp input').first(); + await expect(input).toHaveCSS('border-top-color', 'rgb(255, 0, 0)'); + }); + + test(`${fill} fill should not inherit --border-color from an ancestor`, async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31619', + }); + + await page.setContent( + ` + +
+ +
+ `, + config + ); + + const referenceColor = await page + .locator('#reference input') + .first() + .evaluate((el) => getComputedStyle(el).borderTopColor); + await expect(page.locator('#nested input').first()).toHaveCSS('border-top-color', referenceColor); + }); + }); + }); +}); diff --git a/core/src/components/input-otp/test/input-otp.spec.ts b/core/src/components/input-otp/test/input-otp.spec.ts new file mode 100644 index 00000000000..39abe131198 --- /dev/null +++ b/core/src/components/input-otp/test/input-otp.spec.ts @@ -0,0 +1,215 @@ +import { newSpecPage } from '@stencil/core/testing'; + +import { InputOTP } from '../input-otp'; + +const getInputLabels = (page: { body: HTMLElement }) => + Array.from(page.body.querySelectorAll('ion-input-otp input')).map((input) => input.getAttribute('aria-label')); + +describe('input-otp: input labels', () => { + it('should label each input with its position by default', async () => { + const page = await newSpecPage({ + components: [InputOTP], + html: ``, + }); + + expect(getInputLabels(page)).toEqual(['Input 1 of 4', 'Input 2 of 4', 'Input 3 of 4', 'Input 4 of 4']); + }); + + it('should prefix each input label with the host aria-label', async () => { + const page = await newSpecPage({ + components: [InputOTP], + html: ``, + }); + + expect(getInputLabels(page)).toEqual(['PIN, 1 of 4', 'PIN, 2 of 4', 'PIN, 3 of 4', 'PIN, 4 of 4']); + }); + + it('should label each input with the output of inputLabelFormatter', async () => { + const page = await newSpecPage({ + components: [InputOTP], + html: ``, + }); + + const inputOtp = page.body.querySelector('ion-input-otp')!; + inputOtp.inputLabelFormatter = (index: number, length: number) => `Ziffer ${index + 1} von ${length}`; + await page.waitForChanges(); + + expect(getInputLabels(page)).toEqual(['Ziffer 1 von 4', 'Ziffer 2 von 4', 'Ziffer 3 von 4', 'Ziffer 4 von 4']); + }); + + it('should fall back to the default label when inputLabelFormatter throws', async () => { + const page = await newSpecPage({ + components: [InputOTP], + html: ``, + }); + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); + + const inputOtp = page.body.querySelector('ion-input-otp')!; + inputOtp.inputLabelFormatter = () => { + throw new Error('formatter error'); + }; + await page.waitForChanges(); + + expect(getInputLabels(page)).toEqual(['PIN, 1 of 2', 'PIN, 2 of 2']); + expect(consoleError).toHaveBeenCalled(); + consoleError.mockRestore(); + }); +}); + +describe('input-otp: hint text', () => { + it('should describe every input with the helper text', async () => { + const page = await newSpecPage({ + components: [InputOTP], + html: ``, + }); + + const helperText = page.body.querySelector('ion-input-otp .helper-text')!; + expect(helperText.textContent).toBe('Check your email'); + + const inputs = Array.from(page.body.querySelectorAll('ion-input-otp input')); + inputs.forEach((input) => { + expect(input.getAttribute('aria-describedby')).toBe(helperText.id); + }); + }); +}); + +describe('input-otp: error text', () => { + it('should describe every input with the error text and mark it invalid when touched and invalid', async () => { + const page = await newSpecPage({ + components: [InputOTP], + html: ``, + }); + + const errorText = page.body.querySelector('ion-input-otp .error-text')!; + expect(errorText.textContent).toBe('Wrong code'); + expect(page.body.querySelector('ion-input-otp .helper-text')!.textContent).toBe(''); + + const inputs = Array.from(page.body.querySelectorAll('ion-input-otp input')); + inputs.forEach((input) => { + expect(input.getAttribute('aria-describedby')).toBe(errorText.id); + expect(input.getAttribute('aria-invalid')).toBe('true'); + }); + }); + + it('should not mark the inputs invalid when not touched', async () => { + const page = await newSpecPage({ + components: [InputOTP], + html: ``, + }); + + const helperText = page.body.querySelector('ion-input-otp .helper-text')!; + const inputs = Array.from(page.body.querySelectorAll('ion-input-otp input')); + inputs.forEach((input) => { + expect(input.getAttribute('aria-describedby')).toBe(helperText.id); + expect(input.hasAttribute('aria-invalid')).toBe(false); + }); + }); +}); + +describe('input-otp: description', () => { + it('should describe the group, not each input, with the slotted description', async () => { + const page = await newSpecPage({ + components: [InputOTP], + html: `Didn't get a code?`, + }); + await page.waitForChanges(); + + const description = page.body.querySelector('ion-input-otp .input-otp-description')!; + const helperText = page.body.querySelector('ion-input-otp .helper-text')!; + expect(description.id).not.toBe(''); + + const group = page.body.querySelector('ion-input-otp .input-otp-group')!; + expect(group.getAttribute('aria-describedby')).toBe(description.id); + + const inputs = Array.from(page.body.querySelectorAll('ion-input-otp input')); + inputs.forEach((input) => { + expect(input.getAttribute('aria-describedby')).toBe(helperText.id); + }); + }); + + it('should not reference the description when it is empty', async () => { + const page = await newSpecPage({ + components: [InputOTP], + html: ``, + }); + + const group = page.body.querySelector('ion-input-otp .input-otp-group')!; + expect(group.hasAttribute('aria-describedby')).toBe(false); + + const inputs = Array.from(page.body.querySelectorAll('ion-input-otp input')); + inputs.forEach((input) => { + expect(input.hasAttribute('aria-describedby')).toBe(false); + }); + }); +}); + +describe('input-otp: host aria attributes', () => { + it('should merge the host aria-describedby with the hint text on every input', async () => { + const page = await newSpecPage({ + components: [InputOTP], + html: ``, + }); + + const helperText = page.body.querySelector('ion-input-otp .helper-text')!; + const inputs = Array.from(page.body.querySelectorAll('ion-input-otp input')); + inputs.forEach((input) => { + expect(input.getAttribute('aria-describedby')).toBe(`server-error ${helperText.id}`); + }); + }); + + it('should forward the host aria-invalid to every input', async () => { + const page = await newSpecPage({ + components: [InputOTP], + html: ``, + }); + + const inputs = Array.from(page.body.querySelectorAll('ion-input-otp input')); + inputs.forEach((input) => { + expect(input.getAttribute('aria-invalid')).toBe('true'); + }); + }); + + it('should forward aria-errormessage and aria-required to every input instead of the group', async () => { + const page = await newSpecPage({ + components: [InputOTP], + html: ``, + }); + + const group = page.body.querySelector('ion-input-otp .input-otp-group')!; + expect(group.hasAttribute('aria-errormessage')).toBe(false); + expect(group.hasAttribute('aria-required')).toBe(false); + + const inputs = Array.from(page.body.querySelectorAll('ion-input-otp input')); + inputs.forEach((input) => { + expect(input.getAttribute('aria-errormessage')).toBe('server-error'); + expect(input.getAttribute('aria-required')).toBe('true'); + }); + }); + + it('should name every input from the host aria-labelledby followed by its position', async () => { + const page = await newSpecPage({ + components: [InputOTP], + html: ``, + }); + + const group = page.body.querySelector('ion-input-otp .input-otp-group')!; + expect(group.getAttribute('aria-labelledby')).toBe('pin-label'); + + const inputs = Array.from(page.body.querySelectorAll('ion-input-otp input')); + expect(inputs.map((input) => input.getAttribute('aria-label'))).toEqual(['1 of 2', '2 of 2']); + inputs.forEach((input) => { + expect(input.getAttribute('aria-labelledby')).toBe(`pin-label ${input.id}`); + }); + }); + + it('should keep the group label when the host has an aria-label', async () => { + const page = await newSpecPage({ + components: [InputOTP], + html: ``, + }); + + const group = page.body.querySelector('ion-input-otp .input-otp-group')!; + expect(group.getAttribute('role')).toBe('group'); + expect(group.getAttribute('aria-label')).toBe('PIN'); + }); +}); diff --git a/packages/angular/src/lazy/directives/proxies.ts b/packages/angular/src/lazy/directives/proxies.ts index b6be5b42f03..ba57340314c 100644 --- a/packages/angular/src/lazy/directives/proxies.ts +++ b/packages/angular/src/lazy/directives/proxies.ts @@ -1115,7 +1115,7 @@ This event will not emit when programmatically setting the `value` property. @ProxyCmp({ - inputs: ['autocapitalize', 'color', 'disabled', 'fill', 'inputmode', 'length', 'pattern', 'readonly', 'separators', 'shape', 'size', 'type', 'value'], + inputs: ['autocapitalize', 'color', 'disabled', 'errorText', 'fill', 'helperText', 'inputLabelFormatter', 'inputmode', 'length', 'pattern', 'readonly', 'separators', 'shape', 'size', 'type', 'value'], methods: ['setFocus'] }) @Component({ @@ -1123,7 +1123,7 @@ This event will not emit when programmatically setting the `value` property. changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['autocapitalize', 'color', 'disabled', 'fill', 'inputmode', 'length', 'pattern', 'readonly', 'separators', 'shape', 'size', 'type', 'value'], + inputs: ['autocapitalize', 'color', 'disabled', 'errorText', 'fill', 'helperText', 'inputLabelFormatter', 'inputmode', 'length', 'pattern', 'readonly', 'separators', 'shape', 'size', 'type', 'value'], outputs: ['ionInput', 'ionChange', 'ionComplete', 'ionBlur', 'ionFocus'], standalone: false }) diff --git a/packages/angular/src/standalone/directives/input-otp.ts b/packages/angular/src/standalone/directives/input-otp.ts index fe6b983174f..9c1508950ff 100644 --- a/packages/angular/src/standalone/directives/input-otp.ts +++ b/packages/angular/src/standalone/directives/input-otp.ts @@ -25,7 +25,10 @@ const INPUT_OTP_INPUTS = [ 'autocapitalize', 'color', 'disabled', + 'errorText', 'fill', + 'helperText', + 'inputLabelFormatter', 'inputmode', 'length', 'pattern', diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index f3a1d3bede3..0831646d978 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -498,7 +498,10 @@ export const IonInputOtp: StencilVueComponent Date: Wed, 28 Oct 2026 14:11:23 +0000 Subject: [PATCH 2/2] chore(): add updated snapshots --- ...custom-css-var-md-ltr-Mobile-Chrome-linux.png | Bin 0 -> 2189 bytes ...ustom-css-var-md-ltr-Mobile-Firefox-linux.png | Bin 0 -> 3124 bytes ...custom-css-var-md-ltr-Mobile-Safari-linux.png | Bin 0 -> 2815 bytes ...otp-error-text-md-ltr-Mobile-Chrome-linux.png | Bin 0 -> 2247 bytes ...tp-error-text-md-ltr-Mobile-Firefox-linux.png | Bin 0 -> 3162 bytes ...otp-error-text-md-ltr-Mobile-Safari-linux.png | Bin 0 -> 2793 bytes ...xt-description-md-ltr-Mobile-Chrome-linux.png | Bin 0 -> 2988 bytes ...t-description-md-ltr-Mobile-Firefox-linux.png | Bin 0 -> 3989 bytes ...xt-description-md-ltr-Mobile-Safari-linux.png | Bin 0 -> 3866 bytes ...-text-wrapping-md-ltr-Mobile-Chrome-linux.png | Bin 0 -> 3426 bytes ...text-wrapping-md-ltr-Mobile-Firefox-linux.png | Bin 0 -> 5047 bytes ...-text-wrapping-md-ltr-Mobile-Safari-linux.png | Bin 0 -> 5700 bytes 12 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-error-text-custom-css-var-md-ltr-Mobile-Chrome-linux.png create mode 100644 core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-error-text-custom-css-var-md-ltr-Mobile-Firefox-linux.png create mode 100644 core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-error-text-custom-css-var-md-ltr-Mobile-Safari-linux.png create mode 100644 core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-error-text-md-ltr-Mobile-Chrome-linux.png create mode 100644 core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-error-text-md-ltr-Mobile-Firefox-linux.png create mode 100644 core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-error-text-md-ltr-Mobile-Safari-linux.png create mode 100644 core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-helper-text-description-md-ltr-Mobile-Chrome-linux.png create mode 100644 core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-helper-text-description-md-ltr-Mobile-Firefox-linux.png create mode 100644 core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-helper-text-description-md-ltr-Mobile-Safari-linux.png create mode 100644 core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-helper-text-wrapping-md-ltr-Mobile-Chrome-linux.png create mode 100644 core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-helper-text-wrapping-md-ltr-Mobile-Firefox-linux.png create mode 100644 core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-helper-text-wrapping-md-ltr-Mobile-Safari-linux.png diff --git a/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-error-text-custom-css-var-md-ltr-Mobile-Chrome-linux.png b/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-error-text-custom-css-var-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..db20b7650d784fde418a4fde14b801c4439580e7 GIT binary patch literal 2189 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 M%^3uI<>Yto54J>X#Q*>R literal 0 HcmV?d00001 diff --git a/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-error-text-custom-css-var-md-ltr-Mobile-Firefox-linux.png b/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-error-text-custom-css-var-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..be492fcd56c5fd8994234681fe0ea22259b1ac04 GIT binary patch literal 3124 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 literal 0 HcmV?d00001 diff --git a/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-error-text-md-ltr-Mobile-Chrome-linux.png b/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-error-text-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..7b5f852fc3f9f4182d0be333bfd93d457b9e280e GIT binary patch literal 2247 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 fEiz- literal 0 HcmV?d00001 diff --git a/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-error-text-md-ltr-Mobile-Firefox-linux.png b/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-error-text-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..6eb5bd9992f90b4ef1cc041a2035d78b161ecbd0 GIT binary patch literal 3162 zcmeHKXH-+!8pV+kKsw4$RR~2|5b+6;7$LzBf&(H|>Afd_7=!>CdY1@Mo>BxEsvt<{ z3Bn*qKp~2NfF_6(r3FF}5qNiI&090`{=Kzky@$1af@BH4}1UptTGdP@maHn{(cDj)4bk_r7OO$83%Jtj^#dm?kord_+`CY?u2C znOjUu+6^r#mVNv;=>9jrhFi0(XC243PpUV@-W-=`dE%ha5UUlwAzT{5*(9PYi#7+# zEG)Rs>Bz{hY#*MaQa1+u1ScVbcJbAtoQ*`~DdWc1x{{AHFKo}^rzUD4#9lb4;}J4~ zo3BghQQ9xefhA^o&>Y51<7x4%D=+=SBoX8A4iw|7scA77hr-?7YpHp*yGD>dKCwZd zx}%#-JHPu_)_cQcpmaPa&9l|%FNvX&RBYWrWop_r)AwNUZ(gMob?vPx z-U5gLQQ56nZ}8ZmR{I~$h=>3zoAAUPb?A{M!qu_L=0Auw+rppSCS%uo)y4X>b(I9O z7Zp#{i~a_4pY|szyD8>E%>Oj`8?-cN3$zzkvD)sE{Qsi==|P;ud8qM{AH=s67Setu z{`|#XuD#+6uZ>jGpTB$hJaTD2Wixa6VQodxrl)phmO|Q|YDch*Z?az9w5U~^%z-)q&AM7|Qw~u>eo7ns%i{zM zFJD@p^k09!RR>Th9Yl4ltwXOhFKpUApNV<<4A9UMKtqmpN$W+X;Cxu>=B@no@VwKK zM~*Aal5IPDg8s*DQUJ-jr98zwZh_6nNOo*toa4q6OPT2xht{p%@BM^ zepVKO(d@DfxRfT~QgzHPKEmxnw%imJ+q=VyMUK|;nc%ApoqBzt?-TiV?XI0dh%O$vuAmFvVJ<$3NC6piD zZDn=})kTZeT{{3J#;s?ykRE@6u6Yj_l~BZ~_&{UB*w`{C>{l1(v2z;iCgJjkIVNfj zZ3j9f6p-_a9nbp3mq~Lq{RRoer5cx3KO}+jH-79Yxc8YfHxqM%_G-u$2%vPlxc0Cm zLD*+b^ZfSDcosZRhj>&*kQq;7gM{Plnm|c+^hSrD*#;*(9!RZ(h!lNa3Mz*c#IaUA zMC-ny-#EuM+X87eDpv0_II2NL%0B6kEe{hXIen1Uv$gK>etaI|L`C>bMz>QG z*&u;3(Uu{?T>LCP#3X!=fP3gzb%vh`+z@DT3RROr(gu9%k9o;fE4zN z_QuSCPE(8Ayb%vODG^*y@GUYRr-qq)Qw%Zn{*;-ElGOnWF(=qQHC*%l+2x^NaguB`9T_DR=}|WMISrXEH9Q+DT+#WC=*dL@YtGnCVy> z{W2}j;RTHoGe)mw&&$Q%8xCSGF-5#&nZ`zR_XfSw4;LCxqGVm!(pp+3=`Ey$IRzP- z9J-nAjGm|Y^S>G>$ofV!Zs2g@ycp1H`RBHGx{+%h=-XB?HDKXh5u^2%-dRL6h_F#3>^6qSdjamrf5@SNTZEic;D@gI8C34=D7#-@8KUv7fK6>Q`K{;{ zRFx2f@q^7bZP!&pcCb1uLQP}LjK{((o?@6t+krC_N-Fq2A7_l8*}-ltBQ~V`=38b8 znbC{;jt=5C-uU?8dBiYMzDJunoZU2bo&FwwA&Td~C*yq%8hN-P)kf*Py8?E{V^MGS)&Z9G ziaz!s5yaS}=WdvG)wjT-p{hTj!lGgB=OU$O{Lz%~KS=&rS(Po8oBSeNW9ai}iDf%M zh|Y}UOL6cU4-mhg{18@#_W=`j4G7cjmJ}>JR0Ud@n`XBKT3UL1E;{nK>n9z#3t7gXyZ7twQL- zX0YY3Y{8B_{^ZCYt@aW{%BXs}S(i3;+&m{}G9CD%o9^j6c&XJ*DV1D3?3^}_+W{e| zt-PhR)LW_YZK8#Zs5Xp24o<`PPT50FH@_OQ;c!_QSc$MV4a(oO+g%Xv%TzW(A`|V! gkx0+v|Ac#dcG5>8wQe?ZfI9;h+Tjj=27Uyh!^BLDyZ literal 0 HcmV?d00001 diff --git a/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-error-text-md-ltr-Mobile-Safari-linux.png b/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-error-text-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..cb0ce1d9f0dcaddc12fe11643b23255181e882f9 GIT binary patch literal 2793 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 i`#sDpjN<2mwJPlx$Ey2t^kmN*ARF;zkgH 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 e%?|T8ht=C$Nc{(IwY8A| literal 0 HcmV?d00001 diff --git a/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-helper-text-description-md-ltr-Mobile-Firefox-linux.png b/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-helper-text-description-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..227ee9e66136d324381c58d806cea1da1a5cbe92 GIT binary patch literal 3989 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 QaNP#!=@@F0;E#g;4nv~KJpcdz literal 0 HcmV?d00001 diff --git a/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-helper-text-description-md-ltr-Mobile-Safari-linux.png b/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-helper-text-description-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..7df4224d4edc2c3eaa0dc92278b70e0c04948c7d GIT binary patch literal 3866 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 literal 0 HcmV?d00001 diff --git a/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-helper-text-wrapping-md-ltr-Mobile-Chrome-linux.png b/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-helper-text-wrapping-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..0e8607d0831c8e9b7d4178f03c03161ec347ae30 GIT binary patch literal 3426 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 literal 0 HcmV?d00001 diff --git a/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-helper-text-wrapping-md-ltr-Mobile-Firefox-linux.png b/core/src/components/input-otp/test/bottom-content/input-otp.e2e.ts-snapshots/input-otp-helper-text-wrapping-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..ee1b625dad57ebd4a22cfb0922afc90a478e20c7 GIT binary patch literal 5047 zcmeHLXHXPvmqjIKiGl>l!iYr4nITFLL^4CpC?Gk%ELxy2shMbc` zhYSPE5Wjirt9omzc7N>F*8bZceY>ik+fVg9_w+fpJN}i98W}MoF%Aw6nT9%0AA7#U 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 literal 0 HcmV?d00001