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
This commit is contained in:
Brandy Carney
2024-08-07 10:57:29 -04:00
committed by GitHub
parent ab4f2791c1
commit 7b16397714
4 changed files with 152 additions and 9 deletions

View File

@ -1,6 +1,7 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Build, Component, Element, Event, Host, Listen, Method, Prop, forceUpdate, h, readTask } from '@stencil/core';
import { componentOnReady, hasLazyBuild } from '@utils/helpers';
import { componentOnReady, hasLazyBuild, inheritAriaAttributes } from '@utils/helpers';
import type { Attributes } from '@utils/helpers';
import { isPlatform } from '@utils/platform';
import { isRTL } from '@utils/rtl';
import { createColorClasses, hostContext } from '@utils/theme';
@ -33,6 +34,7 @@ export class Content implements ComponentInterface {
private backgroundContentEl?: HTMLElement;
private isMainContent = true;
private resizeTimeout: ReturnType<typeof setTimeout> | null = null;
private inheritedAttributes: Attributes = {};
private tabsElement: HTMLElement | null = null;
private tabsLoadCallback?: () => void;
@ -125,6 +127,10 @@ export class Content implements ComponentInterface {
*/
@Event() ionScrollEnd!: EventEmitter<ScrollBaseDetail>;
componentWillLoad() {
this.inheritedAttributes = inheritAriaAttributes(this.el);
}
connectedCallback() {
this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null;
@ -432,7 +438,7 @@ export class Content implements ComponentInterface {
}
render() {
const { fixedSlotPlacement, isMainContent, scrollX, scrollY, el } = this;
const { fixedSlotPlacement, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
const rtl = isRTL(el) ? 'rtl' : 'ltr';
const mode = getIonMode(this);
const forceOverscroll = this.shouldForceOverscroll();
@ -453,6 +459,7 @@ export class Content implements ComponentInterface {
'--offset-top': `${this.cTop}px`,
'--offset-bottom': `${this.cBottom}px`,
}}
{...inheritedAttributes}
>
<div ref={(el) => (this.backgroundContentEl = el)} id="background-content" part="background"></div>

View File

@ -0,0 +1,67 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
/**
* Content does not have mode-specific styling
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('content: a11y'), () => {
test('should have the main role', async ({ page }) => {
await page.setContent(
`
<ion-content></ion-content>
`,
config
);
const content = page.locator('ion-content');
await expect(content).toHaveAttribute('role', 'main');
});
test('should have no role in popover', async ({ page }) => {
await page.setContent(
`
<ion-popover>
<ion-content></ion-content>
</ion-popover>
`,
config
);
const content = page.locator('ion-content');
/**
* Playwright can't do .not.toHaveAttribute() because a value is expected,
* and toHaveAttribute can't accept a value of type null.
*/
const role = await content.getAttribute('role');
expect(role).toBeNull();
});
test('should allow for custom role', async ({ page }) => {
await page.setContent(
`
<ion-content role="complementary"></ion-content>
`,
config
);
const content = page.locator('ion-content');
await expect(content).toHaveAttribute('role', 'complementary');
});
test('should allow for custom role in popover', async ({ page }) => {
await page.setContent(
`
<ion-popover>
<ion-content role="complementary"></ion-content>
</ion-popover>
`,
config
);
const content = page.locator('ion-content');
await expect(content).toHaveAttribute('role', 'complementary');
});
});
});

View File

@ -0,0 +1,33 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
/**
* Footer does not have mode-specific styling
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('footer: a11y'), () => {
test('should have the contentinfo role', async ({ page }) => {
await page.setContent(
`
<ion-footer></ion-footer>
`,
config
);
const footer = page.locator('ion-footer');
await expect(footer).toHaveAttribute('role', 'contentinfo');
});
test('should allow for custom role', async ({ page }) => {
await page.setContent(
`
<ion-footer role="complementary"></ion-footer>
`,
config
);
const footer = page.locator('ion-footer');
await expect(footer).toHaveAttribute('role', 'complementary');
});
});
});

View File

@ -15,20 +15,56 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
expect(results.violations).toEqual([]);
});
test('should allow for custom role', async ({ page }) => {
/**
* Note: This example should not be used in production.
* This only serves to check that `role` can be customized.
*/
test('should have the banner role', async ({ page }) => {
await page.setContent(
`
<ion-header role="heading"></ion-header>
<ion-header></ion-header>
`,
config
);
const header = page.locator('ion-header');
await expect(header).toHaveAttribute('role', 'heading');
await expect(header).toHaveAttribute('role', 'banner');
});
test('should have no role in menu', async ({ page }) => {
await page.setContent(
`
<ion-menu>
<ion-header></ion-header>
</ion-menu>
`,
config
);
const header = page.locator('ion-header');
await expect(header).toHaveAttribute('role', 'none');
});
test('should allow for custom role', async ({ page }) => {
await page.setContent(
`
<ion-header role="complementary"></ion-header>
`,
config
);
const header = page.locator('ion-header');
await expect(header).toHaveAttribute('role', 'complementary');
});
test('should allow for custom role in menu', async ({ page }) => {
await page.setContent(
`
<ion-menu>
<ion-header role="complementary"></ion-header>
</ion-menu>
`,
config
);
const header = page.locator('ion-header');
await expect(header).toHaveAttribute('role', 'complementary');
});
});
});