mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-03 19:43:27 +08:00
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:
@ -1,6 +1,7 @@
|
|||||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||||
import { Build, Component, Element, Event, Host, Listen, Method, Prop, forceUpdate, h, readTask } 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 { isPlatform } from '@utils/platform';
|
||||||
import { isRTL } from '@utils/rtl';
|
import { isRTL } from '@utils/rtl';
|
||||||
import { createColorClasses, hostContext } from '@utils/theme';
|
import { createColorClasses, hostContext } from '@utils/theme';
|
||||||
@ -33,6 +34,7 @@ export class Content implements ComponentInterface {
|
|||||||
private backgroundContentEl?: HTMLElement;
|
private backgroundContentEl?: HTMLElement;
|
||||||
private isMainContent = true;
|
private isMainContent = true;
|
||||||
private resizeTimeout: ReturnType<typeof setTimeout> | null = null;
|
private resizeTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private inheritedAttributes: Attributes = {};
|
||||||
|
|
||||||
private tabsElement: HTMLElement | null = null;
|
private tabsElement: HTMLElement | null = null;
|
||||||
private tabsLoadCallback?: () => void;
|
private tabsLoadCallback?: () => void;
|
||||||
@ -125,6 +127,10 @@ export class Content implements ComponentInterface {
|
|||||||
*/
|
*/
|
||||||
@Event() ionScrollEnd!: EventEmitter<ScrollBaseDetail>;
|
@Event() ionScrollEnd!: EventEmitter<ScrollBaseDetail>;
|
||||||
|
|
||||||
|
componentWillLoad() {
|
||||||
|
this.inheritedAttributes = inheritAriaAttributes(this.el);
|
||||||
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null;
|
this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null;
|
||||||
|
|
||||||
@ -432,7 +438,7 @@ export class Content implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { fixedSlotPlacement, isMainContent, scrollX, scrollY, el } = this;
|
const { fixedSlotPlacement, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
|
||||||
const rtl = isRTL(el) ? 'rtl' : 'ltr';
|
const rtl = isRTL(el) ? 'rtl' : 'ltr';
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
const forceOverscroll = this.shouldForceOverscroll();
|
const forceOverscroll = this.shouldForceOverscroll();
|
||||||
@ -453,6 +459,7 @@ export class Content implements ComponentInterface {
|
|||||||
'--offset-top': `${this.cTop}px`,
|
'--offset-top': `${this.cTop}px`,
|
||||||
'--offset-bottom': `${this.cBottom}px`,
|
'--offset-bottom': `${this.cBottom}px`,
|
||||||
}}
|
}}
|
||||||
|
{...inheritedAttributes}
|
||||||
>
|
>
|
||||||
<div ref={(el) => (this.backgroundContentEl = el)} id="background-content" part="background"></div>
|
<div ref={(el) => (this.backgroundContentEl = el)} id="background-content" part="background"></div>
|
||||||
|
|
||||||
|
|||||||
67
core/src/components/content/test/a11y/content.e2e.ts
Normal file
67
core/src/components/content/test/a11y/content.e2e.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
33
core/src/components/footer/test/a11y/footer.e2e.ts
Normal file
33
core/src/components/footer/test/a11y/footer.e2e.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -15,20 +15,56 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
|||||||
expect(results.violations).toEqual([]);
|
expect(results.violations).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should allow for custom role', async ({ page }) => {
|
test('should have the banner 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(
|
await page.setContent(
|
||||||
`
|
`
|
||||||
<ion-header role="heading"></ion-header>
|
<ion-header></ion-header>
|
||||||
`,
|
`,
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
const header = page.locator('ion-header');
|
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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user