Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
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
35 changes: 28 additions & 7 deletions BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ This is a comprehensive list of the breaking changes introduced in the major ver
- [Card](#version-9x-card)
- [Chip](#version-9x-chip)
- [Grid](#version-9x-grid)
- [Radio Group](#version-9x-radio-group)
- [Textarea](#version-9x-textarea)

<h2 id="version-9x-components">Components</h2>

Expand All @@ -38,15 +40,10 @@ This is a comprehensive list of the breaking changes introduced in the major ver

- The properties `pull` and `push` have been deprecated and no longer work. A similar look can be achieved with the newly added property `order`.

<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).<br/>
If you were targeting the internals of `ion-radio-group` in your CSS, you will need to target the `supporting-text`, `helper-text` or `error-text` [Shadow Parts](https://ionicframework.com/docs/theming/css-shadow-parts) instead, or use the provided CSS Variables.<br/>
Additionally, the `radio-group-wrapper` div element has been removed, causing slotted elements to be direct children of the `ion-radio-group`.

<h5>Example 1: Swap two columns</h5>

**Version up to 8.x**

```html
<ion-grid>
<ion-row>
Expand All @@ -57,6 +54,7 @@ Additionally, the `radio-group-wrapper` div element has been removed, causing sl
</ion-grid>
```
**Version 9.x+**

```html
<ion-grid>
<ion-row>
Expand All @@ -68,9 +66,11 @@ Additionally, the `radio-group-wrapper` div element has been removed, causing sl
```

<h5>Example 2: Reorder columns with specific sizes</h5>

To reorder two columns where column 1 has `size="9" push="3"` and column 2 has `size="3" pull="9"`:

**Version up to 8.x**

```html
<ion-grid>
<ion-row>
Expand All @@ -79,7 +79,9 @@ To reorder two columns where column 1 has `size="9" push="3"` and column 2 has `
</ion-row>
</ion-grid>
```

**Version 9.x+**

```html
<ion-grid>
<ion-row>
Expand All @@ -88,7 +90,9 @@ To reorder two columns where column 1 has `size="9" push="3"` and column 2 has `
</ion-row>
</ion-grid>
```

<h5>Example 3: Push</h5>

```html
<ion-grid>
<ion-row>
Expand All @@ -102,6 +106,7 @@ To reorder two columns where column 1 has `size="9" push="3"` and column 2 has `
</ion-grid>
```
**Version 9.x+**

```html
<ion-grid>
<ion-row>
Expand All @@ -116,6 +121,7 @@ To reorder two columns where column 1 has `size="9" push="3"` and column 2 has `
```

<h5>Example 4: Push and Pull</h5>

```html
<ion-grid>
<ion-row>
Expand All @@ -128,6 +134,7 @@ To reorder two columns where column 1 has `size="9" push="3"` and column 2 has `
</ion-row>
</ion-grid>
```

**Version 9.x+**
```html
<ion-grid>
Expand All @@ -140,4 +147,18 @@ To reorder two columns where column 1 has `size="9" push="3"` and column 2 has `
</ion-col>
</ion-row>
</ion-grid>
```
```

<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).

If you were targeting the internals of `ion-radio-group` in your CSS, you will need to target the `supporting-text`, `helper-text` or `error-text` [Shadow Parts](https://ionicframework.com/docs/theming/css-shadow-parts) instead, or use the provided CSS Variables.

Additionally, the `radio-group-wrapper` div element has been removed, causing slotted elements to be direct children of the `ion-radio-group`.

<h4 id="version-9x-textarea">Textarea</h4>

Converted `ion-textarea` 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-textarea` in your CSS, you will need to target the `container`, `label`, `native`, `supporting-text`, `helper-text`, `error-text`, `counter`, or `bottom` [Shadow Parts](https://ionicframework.com/docs/theming/css-shadow-parts) instead, or use the provided CSS Variables.
12 changes: 10 additions & 2 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2426,7 +2426,7 @@ ion-text,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "second
ion-text,prop,mode,"ios" | "md",undefined,false,false
ion-text,prop,theme,"ios" | "md" | "ionic",undefined,false,false

ion-textarea,scoped
ion-textarea,shadow
ion-textarea,prop,autoGrow,boolean,false,false,true
ion-textarea,prop,autocapitalize,string,'none',false,false
ion-textarea,prop,autofocus,boolean,false,false,false
Expand All @@ -2450,7 +2450,7 @@ ion-textarea,prop,mode,"ios" | "md",undefined,false,false
ion-textarea,prop,name,string,this.inputId,false,false
ion-textarea,prop,placeholder,string | undefined,undefined,false,false
ion-textarea,prop,readonly,boolean,false,false,false
ion-textarea,prop,required,boolean,false,false,false
ion-textarea,prop,required,boolean,false,false,true
ion-textarea,prop,rows,number | undefined,undefined,false,false
ion-textarea,prop,shape,"rectangular" | "round" | "soft" | undefined,undefined,false,false
ion-textarea,prop,size,"large" | "medium" | "small" | undefined,'medium',false,false
Expand Down Expand Up @@ -2518,6 +2518,14 @@ ion-textarea,css-prop,--placeholder-font-weight,md
ion-textarea,css-prop,--placeholder-opacity,ionic
ion-textarea,css-prop,--placeholder-opacity,ios
ion-textarea,css-prop,--placeholder-opacity,md
ion-textarea,part,bottom
ion-textarea,part,container
ion-textarea,part,counter
ion-textarea,part,error-text
ion-textarea,part,helper-text
ion-textarea,part,label
ion-textarea,part,native
ion-textarea,part,supporting-text

ion-thumbnail,shadow
ion-thumbnail,prop,mode,"ios" | "md",undefined,false,false
Expand Down
46 changes: 46 additions & 0 deletions core/src/components/popover/test/basic/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@
>
Popover With Textarea
</button>
<button
id="popover-with-input"
class="expand"
onclick="presentPopover({ component: 'input-page', event: event, htmlAttributes: { 'data-testid': 'popover-with-input'} })"
>
Popover With Input
</button>
<button
id="popover-with-buttons"
class="expand"
onclick="presentPopover({ component: 'buttons-page', event: event, htmlAttributes: { 'data-testid': 'popover-with-buttons'} })"
>
Popover With Buttons
</button>
</ion-content>

<style>
Expand Down Expand Up @@ -225,6 +239,38 @@ <h1>Translucent Popover</h1>
}

customElements.define('textarea-page', TextAreaPage);

class InputPage extends HTMLElement {
constructor() {
super();
}

connectedCallback() {
this.innerHTML = `
<ion-content>
<ion-input aria-label="input" value="the cursor in this <ion-input> must be able to be moved with the arrow keys and home and end keys"></ion-input>
<input value="the cursor in this <input> must be able to be moved with the arrow keys and home and end keys"></input>
</ion-content>
`;
}
}
customElements.define('input-page', InputPage);

class ButtonsPage extends HTMLElement {
constructor() {
super();
}

connectedCallback() {
this.innerHTML = `
<ion-content>
<ion-button>Button 1</ion-button>
<ion-button>Button 2</ion-button>
</ion-content>
`;
}
}
customElements.define('buttons-page', ButtonsPage);
</script>
</ion-app>
</body>
Expand Down
88 changes: 73 additions & 15 deletions core/src/components/popover/test/basic/popover.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
test('should not override keyboard interactions for textarea elements', async ({ page, browserName }) => {
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
const popover = page.locator('ion-popover');
const innerNativeTextarea = page.locator('ion-textarea textarea').nth(0);
const innerNativeTextarea = page.locator('ion-textarea').locator('textarea').nth(0);
const vanillaTextarea = page.locator('ion-textarea + textarea');

await popoverFixture.open('#popover-with-textarea');
Expand All @@ -220,44 +220,102 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
*/
await expect(popover).toBeFocused();

// Tab should focus the native textarea inside ion-textarea
await page.keyboard.press(tabKey);

// for Firefox, ion-textarea is focused first
// need to tab again to get to native input
if (browserName === 'firefox') {
await page.keyboard.press(tabKey);
}

await expect(innerNativeTextarea).toBeFocused();

// Arrow keys should work on the ion-textarea
await page.keyboard.press('ArrowDown');

await expect(innerNativeTextarea).toBeFocused();

await page.keyboard.press('ArrowUp');

await expect(innerNativeTextarea).toBeFocused();

// Tab again should focus the vanilla textarea
await page.keyboard.press(tabKey);
// Checking within HTML textarea

await expect(vanillaTextarea).toBeFocused();

// Arrow keys should work on the vanilla textarea
await page.keyboard.press('ArrowDown');

await expect(vanillaTextarea).toBeFocused();

await page.keyboard.press('ArrowUp');

await expect(vanillaTextarea).toBeFocused();

await page.keyboard.press('Home');
await expect(vanillaTextarea).toBeFocused();

await page.keyboard.press('End');
await expect(vanillaTextarea).toBeFocused();
});

test('should not override keyboard interactions for input elements', async ({ page, browserName }) => {
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
const popover = page.locator('ion-popover');
const innerNativeInput = page.locator('ion-input input').nth(0);
const vanillaInput = page.locator('ion-input + input');

await popoverFixture.open('#popover-with-input');

/**
* Focusing happens async inside of popover so we need
* to wait for the requestAnimationFrame to fire.
*/
await expect(popover).toBeFocused();

// Tab should focus the native input inside ion-input
await page.keyboard.press(tabKey);

await expect(innerNativeInput).toBeFocused();

// Arrow keys should work on the ion-input
await page.keyboard.press('ArrowDown');
await expect(innerNativeInput).toBeFocused();

await page.keyboard.press('ArrowUp');
await expect(innerNativeInput).toBeFocused();

// Tab again should focus the vanilla input
await page.keyboard.press(tabKey);
await expect(vanillaInput).toBeFocused();

// Arrow keys should work on the vanilla input
await page.keyboard.press('ArrowDown');
await expect(vanillaInput).toBeFocused();

await page.keyboard.press('ArrowUp');
await expect(vanillaInput).toBeFocused();

await page.keyboard.press('Home');
await expect(vanillaInput).toBeFocused();

await page.keyboard.press('End');
await expect(vanillaInput).toBeFocused();
});

await expect(vanillaTextarea).toBeFocused();
test('should move focus between buttons', async ({ page, browserName }) => {
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
const buttons = page.locator('ion-popover button');

await popoverFixture.open('#popover-with-buttons');

await page.keyboard.press(tabKey);
await expect(buttons.nth(0)).toBeFocused();

await page.keyboard.press(tabKey);
await expect(buttons.nth(1)).toBeFocused();

await page.keyboard.press(tabKey);
await expect(buttons.nth(0)).toBeFocused();

await page.keyboard.press(`Shift+${tabKey}`);
await expect(buttons.nth(1)).toBeFocused();

await page.keyboard.press(`Shift+${tabKey}`);
await expect(buttons.nth(0)).toBeFocused();

await page.keyboard.press(`Shift+${tabKey}`);
await expect(buttons.nth(1)).toBeFocused();
});
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,12 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
await page.setContent(
`
<style>
ion-textarea.custom-textarea.md .textarea-bottom .helper-text {
ion-textarea::part(helper-text) {
font-size: 20px;
color: green;
}
</style>
<ion-textarea class="custom-textarea" label="Label" helper-text="Helper text"></ion-textarea>
<ion-textarea label="Label" helper-text="Helper text"></ion-textarea>
`,
config
);
Expand All @@ -174,12 +174,12 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
await page.setContent(
`
<style>
ion-textarea.custom-textarea.md .textarea-bottom .error-text {
ion-textarea::part(error-text) {
font-size: 20px;
color: purple;
}
</style>
<ion-textarea class="ion-invalid ion-touched custom-textarea" label="Label" error-text="Error text"></ion-textarea>
<ion-textarea class="ion-invalid ion-touched" label="Label" error-text="Error text"></ion-textarea>
`,
config
);
Expand All @@ -193,11 +193,11 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
await page.setContent(
`
<style>
ion-textarea.custom-textarea {
ion-textarea {
--highlight-color-invalid: purple;
}
</style>
<ion-textarea class="ion-invalid ion-touched custom-textarea" label="Label" error-text="Error text"></ion-textarea>
<ion-textarea class="ion-invalid ion-touched" label="Label" error-text="Error text"></ion-textarea>
`,
config
);
Expand Down
Loading
Loading