mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32b679dbe3 | ||
|
|
dcea071daa | ||
|
|
9cab6052fb | ||
|
|
379e804fbf | ||
|
|
07f4121658 | ||
|
|
bf3426dad7 |
@@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
147
core/src/components/title/test/title.spec.ts
Normal file
147
core/src/components/title/test/title.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user