Skip to content

Commit

Permalink
fix(content): allow custom roles and aria attributes to be set on con…
Browse files Browse the repository at this point in the history
…tent (#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
  • Loading branch information
brandyscarney authored Aug 7, 2024
1 parent ab4f279 commit 7b16397
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 8 deletions.
11 changes: 9 additions & 2 deletions core/src/components/content/content.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand All @@ -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>

Expand Down
67 changes: 67 additions & 0 deletions core/src/components/content/test/a11y/content.e2e.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
33 changes: 33 additions & 0 deletions core/src/components/footer/test/a11y/footer.e2e.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
48 changes: 42 additions & 6 deletions core/src/components/header/test/a11y/header.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,56 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
expect(results.violations).toEqual([]);
});

test('should have the banner role', async ({ page }) => {
await page.setContent(
`
<ion-header></ion-header>
`,
config
);
const header = page.locator('ion-header');

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 }) => {
/**
* Note: This example should not be used in production.
* This only serves to check that `role` can be customized.
*/
await page.setContent(
`
<ion-header role="heading"></ion-header>
<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', 'heading');
await expect(header).toHaveAttribute('role', 'complementary');
});
});
});

0 comments on commit 7b16397

Please sign in to comment.