Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
54a5e4e
refactor(textarea): convert to a form associated shadow component
brandyscarney Nov 14, 2025
8417741
style: lint
brandyscarney Nov 17, 2025
59b2423
chore: build
brandyscarney Nov 17, 2025
08a90a2
test(textarea): query the shadowRoot in tests
brandyscarney Nov 17, 2025
f7e8146
test(angular): add textarea to lazy forms test
brandyscarney Nov 17, 2025
6c7d3fb
test(vue): query textarea by shadow and add validation tests
brandyscarney Nov 17, 2025
1fd0f26
test(react): query textarea by shadow and add validation tests
brandyscarney Nov 17, 2025
3bf9289
test(frameworks): input-otp doesn't support required
brandyscarney Nov 17, 2025
27c18c2
test(react): blur focused element instead of targeting textarea
brandyscarney Nov 17, 2025
cd58d35
feat(textarea): expose shadow parts
brandyscarney Nov 18, 2025
391d2f6
test(textarea): add tests for form validation and customizing with parts
brandyscarney Nov 18, 2025
a401ba2
test(textarea): fix customizing bottom content css
brandyscarney Nov 18, 2025
cab5efa
fix(textarea): update styles due to shadow encapsulation
brandyscarney Nov 18, 2025
4ec7a7f
fix(textarea): update styles due to shadow encapsulation
brandyscarney Nov 18, 2025
0e4dee8
fix(textarea): update element internals on load, value change, disabl…
brandyscarney Nov 19, 2025
3c5a152
fix(textarea): add a check for ElementInternals setFormValue for tests
brandyscarney Nov 19, 2025
0db5da6
Merge branch 'next' into FW-6912-textarea
brandyscarney Nov 21, 2025
7f25e7b
test(popover): add popover examples with inputs and buttons
brandyscarney Dec 3, 2025
5fe0c00
chore(): add updated snapshots
brandyscarney Dec 3, 2025
f39a1e3
test(popover): update to grab the textarea in shadow root
brandyscarney Dec 4, 2025
ec80920
test(popover): add tests for keyboard interactions on inputs and focu…
brandyscarney Dec 5, 2025
a2a584f
fix(utils): fix focus behavior for textareas and buttons in popovers
brandyscarney Dec 5, 2025
903b799
fix(overlays): override tab navigation to skip over host elements
brandyscarney Dec 5, 2025
91a2f74
docs(breaking): add textarea conversion
brandyscarney Dec 5, 2025
4480c7a
Merge branch 'next' into FW-6912-textarea
brandyscarney Dec 5, 2025
efef5a1
docs(breaking): move radio group to the right place
brandyscarney Dec 5, 2025
823943d
test(textarea): use shadow part for styling native werapper
brandyscarney Dec 5, 2025
3e724c5
fix(textarea): add box-sizing to fix label border shift
brandyscarney Dec 8, 2025
34e104c
Merge branch 'next' into FW-6912-textarea
brandyscarney Dec 11, 2025
bb45463
chore(): add updated snapshots
brandyscarney Dec 11, 2025
ab96071
Merge branch 'next' into FW-6912-textarea
brandyscarney Dec 16, 2025
9957376
feat(input-otp): convert to shadow
brandyscarney Dec 19, 2025
d316fc3
test(react, vue): fix input reference
brandyscarney Dec 19, 2025
9d14d67
test(textarea): add on to form test for submit and reset
brandyscarney Dec 22, 2025
1d8e95d
fix(textarea): implement formResetCallback to reset forms
brandyscarney Dec 22, 2025
b6db94c
style: lint
brandyscarney Dec 23, 2025
9e1fb27
feat(textarea): add wrapper part
brandyscarney Dec 23, 2025
502dfe5
Merge branch 'FW-6912-textarea' into FW-6912-input-otp
brandyscarney Dec 23, 2025
d73311a
test(input-otp): add test for customizing shadow parts
brandyscarney Jan 12, 2026
a67b85a
test(input-otp): add forms test
brandyscarney Jan 12, 2026
8123b37
feat(input-otp): implement updateElementInternals for forms
brandyscarney Jan 12, 2026
87d636f
test(react, vue): fix typing in input otp tests
brandyscarney Jan 12, 2026
7cd1c5b
docs(breaking): add wrapper part for textarea
brandyscarney Jan 13, 2026
ddf9706
Merge branch 'FW-6912-textarea' into FW-6912-input-otp
brandyscarney Jan 13, 2026
dbd307e
test(input-otp): add e2e test for form
brandyscarney Jan 13, 2026
957de28
Merge branch 'next' into FW-6912-textarea
brandyscarney Jan 13, 2026
f8e9c7a
Merge branch 'FW-6912-textarea' into FW-6912-input-otp
brandyscarney Jan 13, 2026
eb323d7
test(input-otp): remove duplicate checks
brandyscarney Jan 16, 2026
7a2a990
test(popover): replace page.keyboard.press with pageUtils.pressKeys
brandyscarney Jan 16, 2026
d1d2635
test(textarea): remove duplicate checks
brandyscarney Jan 16, 2026
0aac0dc
fix(textarea): add delegatesFocus to resolve Firefox focus errors
brandyscarney Jan 16, 2026
8ea32da
docs(input-otp): remove required mention
brandyscarney Feb 2, 2026
4e11f32
fix(textarea): restore value when the form state restores
brandyscarney Feb 2, 2026
1fc7cb0
Merge branch 'FW-6912-textarea' into FW-6912-input-otp
brandyscarney Feb 2, 2026
4ddebce
fix(input-otp): restore value when the form state restores
brandyscarney Feb 2, 2026
ed1de2d
Merge branch 'next' into FW-6912-input-otp
brandyscarney Feb 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver
- [Chip](#version-9x-chip)
- [Datetime](#version-9x-datetime)
- [Grid](#version-9x-grid)
- [Input Otp](#version-9x-input-otp)
- [Radio Group](#version-9x-radio-group)
- [Textarea](#version-9x-textarea)

Expand Down Expand Up @@ -185,6 +186,12 @@ To reorder two columns where column 1 has `size="9" push="3"` and column 2 has `
</ion-grid>
```

<h4 id="version-9x-input-otp">Input Otp</h4>

Converted `ion-input-otp` to use [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM).

If you were targeting the internals of `ion-input-otp` in your CSS, you will need to target the `group`, `container`, `native`, `separator` or `description` [Shadow Parts](https://ionicframework.com/docs/theming/css-shadow-parts) instead, or use the provided CSS Variables.

<h4 id="version-9x-radio-group">Radio Group</h4>

Converted `ion-radio-group` to use [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM).
Expand Down
9 changes: 8 additions & 1 deletion core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1035,18 +1035,20 @@ ion-input,css-prop,--placeholder-opacity,ionic
ion-input,css-prop,--placeholder-opacity,ios
ion-input,css-prop,--placeholder-opacity,md

ion-input-otp,scoped
ion-input-otp,shadow
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<never, never> | undefined,undefined,false,true
ion-input-otp,prop,disabled,boolean,false,false,true
ion-input-otp,prop,fill,"outline" | "solid" | undefined,'outline',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,mode,"ios" | "md",undefined,false,false
ion-input-otp,prop,pattern,string | undefined,undefined,false,false
ion-input-otp,prop,readonly,boolean,false,false,true
ion-input-otp,prop,separators,number[] | string | undefined,undefined,false,false
ion-input-otp,prop,shape,"rectangular" | "round" | "soft",'round',false,false
ion-input-otp,prop,size,"large" | "medium" | "small",'medium',false,false
ion-input-otp,prop,theme,"ios" | "md" | "ionic",undefined,false,false
ion-input-otp,prop,type,"number" | "text",'number',false,false
ion-input-otp,prop,value,null | number | string | undefined,'',false,false
ion-input-otp,method,setFocus,setFocus(index?: number) => Promise<void>
Expand Down Expand Up @@ -1127,6 +1129,11 @@ 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
ion-input-otp,part,container
ion-input-otp,part,description
ion-input-otp,part,group
ion-input-otp,part,native
ion-input-otp,part,separator

ion-input-password-toggle,shadow
ion-input-password-toggle,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
Expand Down
16 changes: 16 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1779,6 +1779,10 @@ export namespace Components {
* @default 4
*/
"length": number;
/**
* The mode determines the platform behaviors of the component.
*/
"mode"?: "ios" | "md";
/**
* A regex pattern string for allowed characters. Defaults based on type. For numbers (`type="number"`): `"[\p{N}]"` For text (`type="text"`): `"[\p{L}\p{N}]"`
*/
Expand Down Expand Up @@ -1807,6 +1811,10 @@ export namespace Components {
* @default 'medium'
*/
"size": 'small' | 'medium' | 'large';
/**
* The theme determines the visual appearance of the component.
*/
"theme"?: "ios" | "md" | "ionic";
/**
* The type of input allowed in the input boxes.
* @default 'number'
Expand Down Expand Up @@ -7762,6 +7770,10 @@ declare namespace LocalJSX {
* @default 4
*/
"length"?: number;
/**
* The mode determines the platform behaviors of the component.
*/
"mode"?: "ios" | "md";
/**
* Emitted when the input group loses focus.
*/
Expand Down Expand Up @@ -7805,6 +7817,10 @@ declare namespace LocalJSX {
* @default 'medium'
*/
"size"?: 'small' | 'medium' | 'large';
/**
* The theme determines the visual appearance of the component.
*/
"theme"?: "ios" | "md" | "ionic";
/**
* The type of input allowed in the input boxes.
* @default 'number'
Expand Down
3 changes: 3 additions & 0 deletions core/src/components/input-otp/input-otp.common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,13 @@
background: var(--background);
color: var(--color);

font-family: inherit;
font-size: inherit;

text-align: center;
appearance: none;

box-sizing: border-box;
}

:host(.has-focus) .native-input {
Expand Down
2 changes: 2 additions & 0 deletions core/src/components/input-otp/input-otp.native.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
--highlight-color-valid: #{ion-color(success, base)};
--highlight-color-invalid: #{ion-color(danger, base)};

font-family: $font-family-base;

font-size: dynamic-font(14px);
}

Expand Down
86 changes: 81 additions & 5 deletions core/src/components/input-otp/input-otp.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Fragment, Host, Prop, State, h, Watch } from '@stencil/core';
import { AttachInternals, Component, Element, Event, Fragment, Host, Prop, State, h, Watch } from '@stencil/core';
import { reportValidityToElementInternals } from '@utils/forms';
import type { Attributes } from '@utils/helpers';
import { inheritAriaAttributes } from '@utils/helpers';
import { printIonWarning } from '@utils/logging';
Expand All @@ -16,14 +17,25 @@ import type {
InputOtpInputEventDetail,
} from './input-otp-interface';

/**
* @virtualProp {"ios" | "md"} mode - The mode determines the platform behaviors of the component.
* @virtualProp {"ios" | "md" | "ionic"} theme - The theme determines the visual appearance of the component.
*
* @part group - The container element that wraps all input boxes.
* @part container - The wrapper element for each individual input box.
* @part native - The native input element.
* @part separator - The separator element displayed between input boxes.
* @part description - The container element for the description text.
*/
@Component({
tag: 'ion-input-otp',
styleUrls: {
ios: 'input-otp.ios.scss',
md: 'input-otp.md.scss',
ionic: 'input-otp.ionic.scss',
},
scoped: true,
shadow: true,
formAssociated: true,
})
export class InputOTP implements ComponentInterface {
private inheritedAttributes: Attributes = {};
Expand All @@ -47,6 +59,8 @@ export class InputOTP implements ComponentInterface {

@Element() el!: HTMLIonInputOtpElement;

@AttachInternals() internals!: ElementInternals;

@State() private inputValues: string[] = [];
@State() hasFocus = false;
@State() private previousInputValues: string[] = [];
Expand All @@ -69,6 +83,14 @@ export class InputOTP implements ComponentInterface {
*/
@Prop({ reflect: true }) disabled = false;

/**
* Update element internals when disabled prop changes
*/
@Watch('disabled')
protected disabledChanged() {
this.updateElementInternals();
}

/**
* 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.
Expand Down Expand Up @@ -197,6 +219,7 @@ export class InputOTP implements ComponentInterface {
valueChanged() {
this.initializeValues();
this.updateTabIndexes();
this.updateElementInternals();
}

/**
Expand Down Expand Up @@ -272,6 +295,7 @@ export class InputOTP implements ComponentInterface {

componentDidLoad() {
this.updateTabIndexes();
this.updateElementInternals();
}

/**
Expand Down Expand Up @@ -356,6 +380,50 @@ export class InputOTP implements ComponentInterface {
}
}

/**
* Gets the value of the input group as a string for form submission.
* Returns an empty string if the value is null or undefined.
*/
private getValue(): string {
return this.value != null ? this.value.toString() : '';
}

/**
* Called when the form state is restored.
* Restores the component's value.
*/
formStateRestoreCallback(value: string) {
this.value = value;
}

/**
* Called when the form is reset.
* Resets the component's value.
*/
formResetCallback() {
this.value = '';
}

/**
* Updates the form value and reports validity state to the browser via
* ElementInternals. This should be called when the component loads, when
* the disabled prop changes, and when the value changes to ensure the form
* value stays in sync and validation state is updated.
*/
private updateElementInternals() {
// Disabled form controls should not be included in form data
// Pass null to setFormValue when disabled to exclude it from form submission
const value = this.disabled ? null : this.getValue();
// ElementInternals may not be fully available in test environments
// so we need to check if the method exists before calling it
if (typeof this.internals.setFormValue === 'function') {
this.internals.setFormValue(value);
}
// Use the first input element for validity reporting since all inputs
// share the same validation state
reportValidityToElementInternals(this.inputRefs[0] ?? null, this.internals);
}

/**
* Emits an `ionChange` event.
* This API should be called for user committed changes.
Expand Down Expand Up @@ -817,12 +885,19 @@ export class InputOTP implements ComponentInterface {
'input-otp-readonly': readonly,
})}
>
<div role="group" aria-label="One-time password input" class="input-otp-group" {...inheritedAttributes}>
<div
role="group"
aria-label="One-time password input"
class="input-otp-group"
part="group"
{...inheritedAttributes}
>
{Array.from({ length }).map((_, index) => (
<>
<div class="native-wrapper">
<div class="native-wrapper" part="container">
<input
class="native-input"
part="native"
id={`${inputId}-${index}`}
aria-label={`Input ${index + 1} of ${length}`}
type="text"
Expand All @@ -842,7 +917,7 @@ export class InputOTP implements ComponentInterface {
onPaste={this.onPaste}
/>
</div>
{this.showSeparator(index) && <div class="input-otp-separator" />}
{this.showSeparator(index) && <div class="input-otp-separator" part="separator" />}
</>
))}
</div>
Expand All @@ -851,6 +926,7 @@ export class InputOTP implements ComponentInterface {
'input-otp-description': true,
'input-otp-description-hidden': !hasDescription,
}}
part="description"
>
<slot></slot>
</div>
Expand Down
Loading
Loading