Skip to content

Commit 1d36021

Browse files
feat(textarea): convert to a form associated shadow component (#30785)
Issue number: internal --------- ## What is the current behavior? Textarea uses `scoped` encapsulation. This causes issues with CSP compatibility and is inconsistent with our goal of having all components use Shadow DOM. ## What is the new behavior? - Converts `ion-textarea` to `shadow` with `formAssociated: true` - Adds shadow parts for inner elements - Adds and updates existing e2e tests in core for textarea - Updated Angular test app to target textarea shadowRoot and updated lazy forms test to include textarea (standalone already has these) - Updated React & Vue test apps to target textarea shadowRoot and added validation tests - Improves focus behavior inside of a popover so that it is no longer required to tab twice to get to the textarea in any browser ## Does this introduce a breaking change? - [x] Yes - [ ] No BREAKING CHANGE: Textarea has been converted 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. --------- Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
1 parent 89f3b1f commit 1d36021

61 files changed

Lines changed: 1377 additions & 257 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

BREAKING.md

Lines changed: 16 additions & 6 deletions

core/api.txt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2426,7 +2426,7 @@ ion-text,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "second
24262426
ion-text,prop,mode,"ios" | "md",undefined,false,false
24272427
ion-text,prop,theme,"ios" | "md" | "ionic",undefined,false,false
24282428

2429-
ion-textarea,scoped
2429+
ion-textarea,shadow
24302430
ion-textarea,prop,autoGrow,boolean,false,false,true
24312431
ion-textarea,prop,autocapitalize,string,'none',false,false
24322432
ion-textarea,prop,autofocus,boolean,false,false,false
@@ -2450,7 +2450,7 @@ ion-textarea,prop,mode,"ios" | "md",undefined,false,false
24502450
ion-textarea,prop,name,string,this.inputId,false,false
24512451
ion-textarea,prop,placeholder,string | undefined,undefined,false,false
24522452
ion-textarea,prop,readonly,boolean,false,false,false
2453-
ion-textarea,prop,required,boolean,false,false,false
2453+
ion-textarea,prop,required,boolean,false,false,true
24542454
ion-textarea,prop,rows,number | undefined,undefined,false,false
24552455
ion-textarea,prop,shape,"rectangular" | "round" | "soft" | undefined,undefined,false,false
24562456
ion-textarea,prop,size,"large" | "medium" | "small" | undefined,'medium',false,false
@@ -2518,6 +2518,15 @@ ion-textarea,css-prop,--placeholder-font-weight,md
25182518
ion-textarea,css-prop,--placeholder-opacity,ionic
25192519
ion-textarea,css-prop,--placeholder-opacity,ios
25202520
ion-textarea,css-prop,--placeholder-opacity,md
2521+
ion-textarea,part,bottom
2522+
ion-textarea,part,container
2523+
ion-textarea,part,counter
2524+
ion-textarea,part,error-text
2525+
ion-textarea,part,helper-text
2526+
ion-textarea,part,label
2527+
ion-textarea,part,native
2528+
ion-textarea,part,supporting-text
2529+
ion-textarea,part,wrapper
25212530

25222531
ion-thumbnail,shadow
25232532
ion-thumbnail,prop,mode,"ios" | "md",undefined,false,false

core/src/components/popover/test/basic/index.html

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,20 @@
7676
>
7777
Popover With Textarea
7878
</button>
79+
<button
80+
id="popover-with-input"
81+
class="expand"
82+
onclick="presentPopover({ component: 'input-page', event: event, htmlAttributes: { 'data-testid': 'popover-with-input'} })"
83+
>
84+
Popover With Input
85+
</button>
86+
<button
87+
id="popover-with-buttons"
88+
class="expand"
89+
onclick="presentPopover({ component: 'buttons-page', event: event, htmlAttributes: { 'data-testid': 'popover-with-buttons'} })"
90+
>
91+
Popover With Buttons
92+
</button>
7993
</ion-content>
8094

8195
<style>
@@ -225,6 +239,38 @@ <h1>Translucent Popover</h1>
225239
}
226240

227241
customElements.define('textarea-page', TextAreaPage);
242+
243+
class InputPage extends HTMLElement {
244+
constructor() {
245+
super();
246+
}
247+
248+
connectedCallback() {
249+
this.innerHTML = `
250+
<ion-content>
251+
<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>
252+
<input value="the cursor in this <input> must be able to be moved with the arrow keys and home and end keys"></input>
253+
</ion-content>
254+
`;
255+
}
256+
}
257+
customElements.define('input-page', InputPage);
258+
259+
class ButtonsPage extends HTMLElement {
260+
constructor() {
261+
super();
262+
}
263+
264+
connectedCallback() {
265+
this.innerHTML = `
266+
<ion-content>
267+
<ion-button>Button 1</ion-button>
268+
<ion-button>Button 2</ion-button>
269+
</ion-content>
270+
`;
271+
}
272+
}
273+
customElements.define('buttons-page', ButtonsPage);
228274
</script>
229275
</ion-app>
230276
</body>

core/src/components/popover/test/basic/popover.e2e.ts

Lines changed: 89 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -169,47 +169,45 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
169169
await popoverFixture.goto('/src/components/popover/test/basic', config);
170170
});
171171

172-
test('should focus the first ion-item on ArrowDown', async ({ page }) => {
172+
test('should focus the first ion-item on ArrowDown', async ({ page, pageUtils }) => {
173173
const item0 = page.locator('ion-popover ion-item:nth-of-type(1)');
174174

175175
await popoverFixture.open('#basic-popover');
176176

177-
await page.keyboard.press('ArrowDown');
177+
await pageUtils.pressKeys('ArrowDown');
178178
await expect(item0).toBeFocused();
179179
});
180180

181-
test('should trap focus', async ({ page, browserName }) => {
182-
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
181+
test('should trap focus', async ({ page, pageUtils }) => {
183182
const items = page.locator('ion-popover ion-item');
184183

185184
await popoverFixture.open('#basic-popover');
186185

187-
await page.keyboard.press(tabKey);
186+
await pageUtils.pressKeys('Tab');
188187
await expect(items.nth(0)).toBeFocused();
189188

190-
await page.keyboard.press(`Shift+${tabKey}`);
189+
await pageUtils.pressKeys('Shift+Tab');
191190
await expect(items.nth(3)).toBeFocused();
192191

193-
await page.keyboard.press(tabKey);
192+
await pageUtils.pressKeys('Tab');
194193
await expect(items.nth(0)).toBeFocused();
195194

196-
await page.keyboard.press('ArrowDown');
195+
await pageUtils.pressKeys('ArrowDown');
197196
await expect(items.nth(1)).toBeFocused();
198197

199-
await page.keyboard.press('ArrowDown');
198+
await pageUtils.pressKeys('ArrowDown');
200199
await expect(items.nth(2)).toBeFocused();
201200

202-
await page.keyboard.press('Home');
201+
await pageUtils.pressKeys('Home');
203202
await expect(items.nth(0)).toBeFocused();
204203

205-
await page.keyboard.press('End');
204+
await pageUtils.pressKeys('End');
206205
await expect(items.nth(3)).toBeFocused();
207206
});
208207

209-
test('should not override keyboard interactions for textarea elements', async ({ page, browserName }) => {
210-
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
208+
test('should not override keyboard interactions for textarea elements', async ({ page, pageUtils }) => {
211209
const popover = page.locator('ion-popover');
212-
const innerNativeTextarea = page.locator('ion-textarea textarea').nth(0);
210+
const innerNativeTextarea = page.locator('ion-textarea').locator('textarea').nth(0);
213211
const vanillaTextarea = page.locator('ion-textarea + textarea');
214212

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

223-
await page.keyboard.press(tabKey);
224-
225-
// for Firefox, ion-textarea is focused first
226-
// need to tab again to get to native input
227-
if (browserName === 'firefox') {
228-
await page.keyboard.press(tabKey);
229-
}
230-
221+
// Tab should focus the native textarea inside ion-textarea
222+
await pageUtils.pressKeys('Tab');
231223
await expect(innerNativeTextarea).toBeFocused();
232224

233-
await page.keyboard.press('ArrowDown');
234-
225+
// Arrow keys should work on the ion-textarea
226+
await pageUtils.pressKeys('ArrowDown');
235227
await expect(innerNativeTextarea).toBeFocused();
236228

237-
await page.keyboard.press('ArrowUp');
238-
229+
await pageUtils.pressKeys('ArrowUp');
239230
await expect(innerNativeTextarea).toBeFocused();
240231

241-
await page.keyboard.press(tabKey);
242-
// Checking within HTML textarea
243-
232+
// Tab again should focus the vanilla textarea
233+
await pageUtils.pressKeys('Tab');
244234
await expect(vanillaTextarea).toBeFocused();
245235

246-
await page.keyboard.press('ArrowDown');
236+
// Arrow keys should work on the vanilla textarea
237+
await pageUtils.pressKeys('ArrowDown');
238+
await expect(vanillaTextarea).toBeFocused();
247239

240+
await pageUtils.pressKeys('ArrowUp');
248241
await expect(vanillaTextarea).toBeFocused();
249242

250-
await page.keyboard.press('ArrowUp');
243+
await pageUtils.pressKeys('Home');
244+
await expect(vanillaTextarea).toBeFocused();
251245

246+
await pageUtils.pressKeys('End');
252247
await expect(vanillaTextarea).toBeFocused();
248+
});
253249

254-
await page.keyboard.press('Home');
250+
test('should not override keyboard interactions for input elements', async ({ page, pageUtils }) => {
251+
const popover = page.locator('ion-popover');
252+
const innerNativeInput = page.locator('ion-input input').nth(0);
253+
const vanillaInput = page.locator('ion-input + input');
255254

256-
await expect(vanillaTextarea).toBeFocused();
255+
await popoverFixture.open('#popover-with-input');
256+
257+
/**
258+
* Focusing happens async inside of popover so we need
259+
* to wait for the requestAnimationFrame to fire.
260+
*/
261+
await expect(popover).toBeFocused();
257262

258-
await page.keyboard.press('End');
263+
// Tab should focus the native input inside ion-input
264+
await pageUtils.pressKeys('Tab');
259265

260-
await expect(vanillaTextarea).toBeFocused();
266+
await expect(innerNativeInput).toBeFocused();
267+
268+
// Arrow keys should work on the ion-input
269+
await pageUtils.pressKeys('ArrowDown');
270+
await expect(innerNativeInput).toBeFocused();
271+
272+
await pageUtils.pressKeys('ArrowUp');
273+
await expect(innerNativeInput).toBeFocused();
274+
275+
// Tab again should focus the vanilla input
276+
await pageUtils.pressKeys('Tab');
277+
await expect(vanillaInput).toBeFocused();
278+
279+
// Arrow keys should work on the vanilla input
280+
await pageUtils.pressKeys('ArrowDown');
281+
await expect(vanillaInput).toBeFocused();
282+
283+
await pageUtils.pressKeys('ArrowUp');
284+
await expect(vanillaInput).toBeFocused();
285+
286+
await pageUtils.pressKeys('Home');
287+
await expect(vanillaInput).toBeFocused();
288+
289+
await pageUtils.pressKeys('End');
290+
await expect(vanillaInput).toBeFocused();
291+
});
292+
293+
test('should move focus between buttons', async ({ page, pageUtils }) => {
294+
const buttons = page.locator('ion-popover button');
295+
296+
await popoverFixture.open('#popover-with-buttons');
297+
298+
await pageUtils.pressKeys('Tab');
299+
await expect(buttons.nth(0)).toBeFocused();
300+
301+
await pageUtils.pressKeys('Tab');
302+
await expect(buttons.nth(1)).toBeFocused();
303+
304+
await pageUtils.pressKeys('Tab');
305+
await expect(buttons.nth(0)).toBeFocused();
306+
307+
await pageUtils.pressKeys('Shift+Tab');
308+
await expect(buttons.nth(1)).toBeFocused();
309+
310+
await pageUtils.pressKeys('Shift+Tab');
311+
await expect(buttons.nth(0)).toBeFocused();
312+
313+
await pageUtils.pressKeys('Shift+Tab');
314+
await expect(buttons.nth(1)).toBeFocused();
261315
});
262316
});
263317
});
4.38 KB
5.56 KB
6.62 KB
4.38 KB
5.64 KB
6.62 KB

0 commit comments

Comments
 (0)