diff --git a/core/src/components/content/content.tsx b/core/src/components/content/content.tsx index 9c9e1341140..74e44c63597 100644 --- a/core/src/components/content/content.tsx +++ b/core/src/components/content/content.tsx @@ -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 | 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; + 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} >
(this.backgroundContentEl = el)} id="background-content" part="background">
diff --git a/core/src/components/content/test/a11y/content.e2e.ts b/core/src/components/content/test/a11y/content.e2e.ts new file mode 100644 index 00000000000..9a5e796d5fd --- /dev/null +++ b/core/src/components/content/test/a11y/content.e2e.ts @@ -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( + ` + + `, + 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( + ` + + + + `, + 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( + ` + + `, + 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( + ` + + + + `, + config + ); + const content = page.locator('ion-content'); + + await expect(content).toHaveAttribute('role', 'complementary'); + }); + }); +}); diff --git a/core/src/components/footer/test/a11y/footer.e2e.ts b/core/src/components/footer/test/a11y/footer.e2e.ts new file mode 100644 index 00000000000..f1e4b8155ce --- /dev/null +++ b/core/src/components/footer/test/a11y/footer.e2e.ts @@ -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( + ` + + `, + config + ); + const footer = page.locator('ion-footer'); + + await expect(footer).toHaveAttribute('role', 'contentinfo'); + }); + + test('should allow for custom role', async ({ page }) => { + await page.setContent( + ` + + `, + config + ); + const footer = page.locator('ion-footer'); + + await expect(footer).toHaveAttribute('role', 'complementary'); + }); + }); +}); diff --git a/core/src/components/header/test/a11y/header.e2e.ts b/core/src/components/header/test/a11y/header.e2e.ts index 51f35b7d8e8..dc46b7c6364 100644 --- a/core/src/components/header/test/a11y/header.e2e.ts +++ b/core/src/components/header/test/a11y/header.e2e.ts @@ -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( + ` + + `, + 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( + ` + + + + `, + 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( ` - + + `, + 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( + ` + + + `, config ); const header = page.locator('ion-header'); - await expect(header).toHaveAttribute('role', 'heading'); + await expect(header).toHaveAttribute('role', 'complementary'); }); }); });