Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(content): allow custom roles and aria attributes to be set on content #29753

Merged
merged 6 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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'), () => {
brandyscarney marked this conversation as resolved.
Show resolved Hide resolved
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'), () => {
brandyscarney marked this conversation as resolved.
Show resolved Hide resolved
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
brandyscarney marked this conversation as resolved.
Show resolved Hide resolved
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');
});
});
});
Loading