Compare commits

...

6 Commits

Author SHA1 Message Date
Liam DeBeasi
32b679dbe3 Merge branch 'feature-8.0' into FW-119 2024-04-08 15:47:44 -04:00
Liam DeBeasi
dcea071daa lint 2023-12-15 13:30:04 -05:00
Liam DeBeasi
9cab6052fb remove .only and lint 2023-12-15 13:25:34 -05:00
Liam DeBeasi
379e804fbf lint 2023-12-15 13:14:39 -05:00
Liam DeBeasi
07f4121658 ensure title is in a level 1 heading, add more tests 2023-12-15 13:14:19 -05:00
Liam DeBeasi
bf3426dad7 fix(title): title is level one heading if one is not present 2023-12-15 12:35:36 -05:00
3 changed files with 224 additions and 0 deletions

View File

@@ -1,3 +1,4 @@
import AxeBuilder from '@axe-core/playwright';
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
@@ -74,3 +75,52 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, screenshot, c
});
});
});
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('title: level 1 heading'), () => {
test('should not have accessibility violations', async ({ page }) => {
/**
* Level 1 headings must be inside of a landmark (ion-header)
*/
await page.setContent(
`
<ion-header>
<ion-toolbar>
<ion-title>My Title</ion-title>
</ion-toolbar>
</ion-header>
`,
config
);
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
test('should not have accessibility violations with multiple h1 elements on a hidden page', async ({ page }) => {
await page.setContent(
`
<div class="ion-page ion-page-hidden">
<ion-header>
<ion-toolbar>
<ion-title>My Title</ion-title>
</ion-toolbar>
</ion-header>
</div>
<div class="ion-page">
<ion-header>
<ion-toolbar>
<ion-title>My Title</ion-title>
</ion-toolbar>
</ion-header>
</div>
`,
config
);
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
});
});

View File

@@ -0,0 +1,147 @@
import { newSpecPage } from '@stencil/core/testing';
import { Header } from '../../header/header';
import { Toolbar } from '../../toolbar/toolbar';
import { ToolbarTitle } from '../title';
describe('title: a11y', () => {
it('should add heading level 1 attributes when inside of a landmark', async () => {
const page = await newSpecPage({
components: [Header, Toolbar, ToolbarTitle],
html: `
<ion-header>
<ion-toolbar>
<ion-title>Title</ion-title>
</ion-toolbar>
</ion-header>
`,
});
const title = page.body.querySelector('ion-title')!;
expect(title.getAttribute('role')).toBe('heading');
expect(title.getAttribute('aria-level')).toBe('1');
});
it('should not override a custom role', async () => {
const page = await newSpecPage({
components: [Header, Toolbar, ToolbarTitle],
html: `
<ion-header>
<ion-toolbar>
<ion-title role="article">Title 1</ion-title>
</ion-toolbar>
</ion-header>
`,
});
const title = page.body.querySelector('ion-title')!;
expect(title.getAttribute('role')).toBe('article');
expect(title.hasAttribute('aria-level')).toBe(false);
});
it('should not add heading level 1 attributes when outside of a landmark', async () => {
const page = await newSpecPage({
components: [Header, Toolbar, ToolbarTitle],
html: `
<ion-title>Title</ion-title>
`,
});
const title = page.body.querySelector('ion-title')!;
expect(title.hasAttribute('role')).toBe(false);
expect(title.hasAttribute('aria-level')).toBe(false);
});
it('at most one ion-title should have level 1 attributes', async () => {
const page = await newSpecPage({
components: [Header, Toolbar, ToolbarTitle],
html: `
<ion-header>
<ion-toolbar>
<ion-title>Title 1</ion-title>
</ion-toolbar>
</ion-header>
<ion-header>
<ion-toolbar>
<ion-title>Title 2</ion-title>
</ion-toolbar>
</ion-header>
`,
});
const titles = page.body.querySelectorAll('ion-title');
expect(titles[0].getAttribute('role')).toBe('heading');
expect(titles[0].getAttribute('aria-level')).toBe('1');
expect(titles[1].hasAttribute('role')).toBe(false);
expect(titles[1].hasAttribute('aria-level')).toBe(false);
});
it('should not add level 1 attributes if other level 1 headings exist', async () => {
const page = await newSpecPage({
components: [Header, Toolbar, ToolbarTitle],
html: `
<h1>Title</h1>
<ion-header>
<ion-toolbar>
<ion-title>Title 1</ion-title>
</ion-toolbar>
</ion-header>
`,
});
const title = page.body.querySelector('ion-title')!;
expect(title.hasAttribute('role')).toBe(false);
expect(title.hasAttribute('aria-level')).toBe(false);
});
it('should not add level 1 attributes if other level 1 attributes exist', async () => {
const page = await newSpecPage({
components: [Header, Toolbar, ToolbarTitle],
html: `
<div role="heading" aria-level="1">Title</div>
<ion-header>
<ion-toolbar>
<ion-title>Title 1</ion-title>
</ion-toolbar>
</ion-header>
`,
});
const title = page.body.querySelector('ion-title')!;
expect(title.hasAttribute('role')).toBe(false);
expect(title.hasAttribute('aria-level')).toBe(false);
});
it('should have level 1 attributes even if there is a level 1 heading on another page', async () => {
const page = await newSpecPage({
components: [Header, Toolbar, ToolbarTitle],
html: `
<div class="ion-page">
<ion-header>
<ion-toolbar>
<ion-title>Title 1</ion-title>
</ion-toolbar>
</ion-header>
</div>
<div class="ion-page">
<ion-header>
<ion-toolbar>
<ion-title>Title 2</ion-title>
</ion-toolbar>
</ion-header>
</div>
`,
});
const pages = page.body.querySelectorAll('.ion-page');
pages.forEach((page) => {
const title = page.querySelector('ion-title')!;
expect(title.getAttribute('role')).toBe('heading');
expect(title.getAttribute('aria-level')).toBe('1');
});
});
});

View File

@@ -1,5 +1,6 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Prop, Watch, h } from '@stencil/core';
import { doc } from '@utils/browser';
import { createColorClasses } from '@utils/theme';
import { getIonMode } from '../../global/ionic-global';
@@ -56,11 +57,37 @@ export class ToolbarTitle implements ComponentInterface {
}
render() {
const { el } = this;
const mode = getIonMode(this);
const size = this.getSize();
/**
* If there is already a level one heading
* within the context of the page then
* do not add another one.
*/
const root = el.closest('.ion-page') ?? doc?.body;
const hasHeading = root?.querySelector('h1, [role="heading"][aria-level="1"]');
const hasRole = el.hasAttribute('role');
/**
* The first `ion-title` on the page is considered
* the heading. This can be customized by setting
* role="heading" aria-level="1" on another element
* or by using h1.
*
* Level 1 headings must be contained inside of a landmark,
* so we check for ion-header which is typically the landmark.
*/
const isHeading =
hasRole === false &&
hasHeading === null &&
root?.querySelector('ion-title') === el &&
el?.closest('ion-header[role]') !== null;
return (
<Host
role={isHeading ? 'heading' : null}
aria-level={isHeading ? '1' : null}
class={createColorClasses(this.color, {
[mode]: true,
[`title-${size}`]: true,