Skip to content

Commit 7b16397

Browse files
fix(content): allow custom roles and aria attributes to be set on content (#29753)
Issue number: N/A --------- ## What is the current behavior? Setting a custom `role` on the `ion-content` element does not work. ## What is the new behavior? - Inherit attributes for the content element which allows a custom `role` property to be set - Adds e2e tests for content, header, and footer verifying that the proper roles are assigned ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information To test this PR: 1. Switch to the branch and navigate to the `core/` directory 1. Make sure to run `npx playwright install` if it has not been updated recenly 1. Run `npm run test.e2e src/components/content/test/a11y/` 1. Verify that the tests pass 1. Remove my fix in `core/src/components/content/content.tsx` and run the test again 1. Verify that the `should allow for custom role` tests fail
1 parent ab4f279 commit 7b16397

File tree

4 files changed

+151
-8
lines changed

4 files changed

+151
-8
lines changed

core/src/components/content/content.tsx

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ComponentInterface, EventEmitter } from '@stencil/core';
22
import { Build, Component, Element, Event, Host, Listen, Method, Prop, forceUpdate, h, readTask } from '@stencil/core';
3-
import { componentOnReady, hasLazyBuild } from '@utils/helpers';
3+
import { componentOnReady, hasLazyBuild, inheritAriaAttributes } from '@utils/helpers';
4+
import type { Attributes } from '@utils/helpers';
45
import { isPlatform } from '@utils/platform';
56
import { isRTL } from '@utils/rtl';
67
import { createColorClasses, hostContext } from '@utils/theme';
@@ -33,6 +34,7 @@ export class Content implements ComponentInterface {
3334
private backgroundContentEl?: HTMLElement;
3435
private isMainContent = true;
3536
private resizeTimeout: ReturnType<typeof setTimeout> | null = null;
37+
private inheritedAttributes: Attributes = {};
3638

3739
private tabsElement: HTMLElement | null = null;
3840
private tabsLoadCallback?: () => void;
@@ -125,6 +127,10 @@ export class Content implements ComponentInterface {
125127
*/
126128
@Event() ionScrollEnd!: EventEmitter<ScrollBaseDetail>;
127129

130+
componentWillLoad() {
131+
this.inheritedAttributes = inheritAriaAttributes(this.el);
132+
}
133+
128134
connectedCallback() {
129135
this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null;
130136

@@ -432,7 +438,7 @@ export class Content implements ComponentInterface {
432438
}
433439

434440
render() {
435-
const { fixedSlotPlacement, isMainContent, scrollX, scrollY, el } = this;
441+
const { fixedSlotPlacement, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
436442
const rtl = isRTL(el) ? 'rtl' : 'ltr';
437443
const mode = getIonMode(this);
438444
const forceOverscroll = this.shouldForceOverscroll();
@@ -453,6 +459,7 @@ export class Content implements ComponentInterface {
453459
'--offset-top': `${this.cTop}px`,
454460
'--offset-bottom': `${this.cBottom}px`,
455461
}}
462+
{...inheritedAttributes}
456463
>
457464
<div ref={(el) => (this.backgroundContentEl = el)} id="background-content" part="background"></div>
458465

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { expect } from '@playwright/test';
2+
import { configs, test } from '@utils/test/playwright';
3+
4+
/**
5+
* Content does not have mode-specific styling
6+
*/
7+
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
8+
test.describe(title('content: a11y'), () => {
9+
test('should have the main role', async ({ page }) => {
10+
await page.setContent(
11+
`
12+
<ion-content></ion-content>
13+
`,
14+
config
15+
);
16+
const content = page.locator('ion-content');
17+
18+
await expect(content).toHaveAttribute('role', 'main');
19+
});
20+
21+
test('should have no role in popover', async ({ page }) => {
22+
await page.setContent(
23+
`
24+
<ion-popover>
25+
<ion-content></ion-content>
26+
</ion-popover>
27+
`,
28+
config
29+
);
30+
31+
const content = page.locator('ion-content');
32+
33+
/**
34+
* Playwright can't do .not.toHaveAttribute() because a value is expected,
35+
* and toHaveAttribute can't accept a value of type null.
36+
*/
37+
const role = await content.getAttribute('role');
38+
expect(role).toBeNull();
39+
});
40+
41+
test('should allow for custom role', async ({ page }) => {
42+
await page.setContent(
43+
`
44+
<ion-content role="complementary"></ion-content>
45+
`,
46+
config
47+
);
48+
const content = page.locator('ion-content');
49+
50+
await expect(content).toHaveAttribute('role', 'complementary');
51+
});
52+
53+
test('should allow for custom role in popover', async ({ page }) => {
54+
await page.setContent(
55+
`
56+
<ion-popover>
57+
<ion-content role="complementary"></ion-content>
58+
</ion-popover>
59+
`,
60+
config
61+
);
62+
const content = page.locator('ion-content');
63+
64+
await expect(content).toHaveAttribute('role', 'complementary');
65+
});
66+
});
67+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { expect } from '@playwright/test';
2+
import { configs, test } from '@utils/test/playwright';
3+
4+
/**
5+
* Footer does not have mode-specific styling
6+
*/
7+
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
8+
test.describe(title('footer: a11y'), () => {
9+
test('should have the contentinfo role', async ({ page }) => {
10+
await page.setContent(
11+
`
12+
<ion-footer></ion-footer>
13+
`,
14+
config
15+
);
16+
const footer = page.locator('ion-footer');
17+
18+
await expect(footer).toHaveAttribute('role', 'contentinfo');
19+
});
20+
21+
test('should allow for custom role', async ({ page }) => {
22+
await page.setContent(
23+
`
24+
<ion-footer role="complementary"></ion-footer>
25+
`,
26+
config
27+
);
28+
const footer = page.locator('ion-footer');
29+
30+
await expect(footer).toHaveAttribute('role', 'complementary');
31+
});
32+
});
33+
});

core/src/components/header/test/a11y/header.e2e.ts

+42-6
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,56 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
1515
expect(results.violations).toEqual([]);
1616
});
1717

18+
test('should have the banner role', async ({ page }) => {
19+
await page.setContent(
20+
`
21+
<ion-header></ion-header>
22+
`,
23+
config
24+
);
25+
const header = page.locator('ion-header');
26+
27+
await expect(header).toHaveAttribute('role', 'banner');
28+
});
29+
30+
test('should have no role in menu', async ({ page }) => {
31+
await page.setContent(
32+
`
33+
<ion-menu>
34+
<ion-header></ion-header>
35+
</ion-menu>
36+
`,
37+
config
38+
);
39+
const header = page.locator('ion-header');
40+
41+
await expect(header).toHaveAttribute('role', 'none');
42+
});
43+
1844
test('should allow for custom role', async ({ page }) => {
19-
/**
20-
* Note: This example should not be used in production.
21-
* This only serves to check that `role` can be customized.
22-
*/
2345
await page.setContent(
2446
`
25-
<ion-header role="heading"></ion-header>
47+
<ion-header role="complementary"></ion-header>
48+
`,
49+
config
50+
);
51+
const header = page.locator('ion-header');
52+
53+
await expect(header).toHaveAttribute('role', 'complementary');
54+
});
55+
56+
test('should allow for custom role in menu', async ({ page }) => {
57+
await page.setContent(
58+
`
59+
<ion-menu>
60+
<ion-header role="complementary"></ion-header>
61+
</ion-menu>
2662
`,
2763
config
2864
);
2965
const header = page.locator('ion-header');
3066

31-
await expect(header).toHaveAttribute('role', 'heading');
67+
await expect(header).toHaveAttribute('role', 'complementary');
3268
});
3369
});
3470
});

0 commit comments

Comments
 (0)