fix(segment, segment-button): use correct tablist and tab roles for screen readers (#23145)

* fix(segment, segment-button): change aria attributes for segment and segment-button

* add axe test

* Add tests, screen reader doc

* add updated screen reader

* fix(segment-button): move aria tags to host

* verify nvda and talkback behavior

* fix(segment-button): remove outline on focus

* Update core/src/components/segment/test/basic/index.html

Co-authored-by: Liam DeBeasi <liamdebeasi@icloud.com>
This commit is contained in:
William Martin
2021-04-12 09:20:59 -04:00
committed by GitHub
parent 669d24c551
commit 91ac340ae7
8 changed files with 129 additions and 2 deletions

40
core/package-lock.json generated
View File

@ -14,6 +14,7 @@
"tslib": "^2.1.0" "tslib": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {
"@axe-core/puppeteer": "^4.1.1",
"@jest/core": "^26.6.3", "@jest/core": "^26.6.3",
"@rollup/plugin-node-resolve": "^8.4.0", "@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3", "@rollup/plugin-virtual": "^2.0.3",
@ -43,6 +44,21 @@
"typescript": "^4.0.5" "typescript": "^4.0.5"
} }
}, },
"node_modules/@axe-core/puppeteer": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@axe-core/puppeteer/-/puppeteer-4.1.1.tgz",
"integrity": "sha512-Ao9N7HL//s26hdasx3Ba18tlJgxpoO+1SmIN6eSx5vC50dqYhiRU0xp6wBKWqzo10u1jpzl/s4RFsOAuolFMBA==",
"dev": true,
"dependencies": {
"axe-core": "^4.1.1"
},
"engines": {
"node": ">=6.4.0"
},
"peerDependencies": {
"puppeteer": ">=1.10.0 < 6"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.10.1", "version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
@ -2029,6 +2045,15 @@
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==", "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==",
"dev": true "dev": true
}, },
"node_modules/axe-core": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.4.tgz",
"integrity": "sha512-Pdgfv6iP0gNx9ejRGa3zE7Xgkj/iclXqLfe7BnatdZz0QnLZ3jrRHUVH8wNSdN68w05Sk3ShGTb3ydktMTooig==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/babel-plugin-istanbul": { "node_modules/babel-plugin-istanbul": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz",
@ -13802,6 +13827,15 @@
} }
}, },
"dependencies": { "dependencies": {
"@axe-core/puppeteer": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@axe-core/puppeteer/-/puppeteer-4.1.1.tgz",
"integrity": "sha512-Ao9N7HL//s26hdasx3Ba18tlJgxpoO+1SmIN6eSx5vC50dqYhiRU0xp6wBKWqzo10u1jpzl/s4RFsOAuolFMBA==",
"dev": true,
"requires": {
"axe-core": "^4.1.1"
}
},
"@babel/code-frame": { "@babel/code-frame": {
"version": "7.10.1", "version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
@ -15481,6 +15515,12 @@
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==", "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==",
"dev": true "dev": true
}, },
"axe-core": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.4.tgz",
"integrity": "sha512-Pdgfv6iP0gNx9ejRGa3zE7Xgkj/iclXqLfe7BnatdZz0QnLZ3jrRHUVH8wNSdN68w05Sk3ShGTb3ydktMTooig==",
"dev": true
},
"babel-plugin-istanbul": { "babel-plugin-istanbul": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz",

View File

@ -36,6 +36,7 @@
"tslib": "^2.1.0" "tslib": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {
"@axe-core/puppeteer": "^4.1.1",
"@jest/core": "^26.6.3", "@jest/core": "^26.6.3",
"@rollup/plugin-node-resolve": "^8.4.0", "@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3", "@rollup/plugin-virtual": "^2.0.3",

View File

@ -166,6 +166,10 @@
} }
} }
:host(:focus) {
outline: none;
}
// Segment Button: Hover // Segment Button: Hover
// -------------------------------------------------- // --------------------------------------------------

View File

@ -86,13 +86,28 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
} }
} }
private get tabIndex() {
if (this.disabled) { return -1; }
const hasTabIndex = this.el.hasAttribute('tabindex');
if (hasTabIndex) {
return this.el.getAttribute('tabindex');
}
return 0;
}
render() { render() {
const { checked, type, disabled, hasIcon, hasLabel, layout, segmentEl } = this; const { checked, type, disabled, hasIcon, hasLabel, layout, segmentEl, tabIndex } = this;
const mode = getIonMode(this); const mode = getIonMode(this);
const hasSegmentColor = () => segmentEl !== null && segmentEl.color !== undefined; const hasSegmentColor = () => segmentEl !== null && segmentEl.color !== undefined;
return ( return (
<Host <Host
role="tab"
aria-selected={checked ? 'true' : 'false'}
aria-disabled={disabled ? 'true' : null} aria-disabled={disabled ? 'true' : null}
tabIndex={tabIndex}
class={{ class={{
[mode]: true, [mode]: true,
'in-toolbar': hostContext('ion-toolbar', this.el), 'in-toolbar': hostContext('ion-toolbar', this.el),
@ -113,7 +128,7 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
> >
<button <button
type={type} type={type}
aria-pressed={checked ? 'true' : 'false'} tabIndex={-1}
class="button-native" class="button-native"
part="native" part="native"
disabled={disabled} disabled={disabled}

View File

@ -425,6 +425,7 @@ export class Segment implements ComponentInterface {
const mode = getIonMode(this); const mode = getIonMode(this);
return ( return (
<Host <Host
role="tablist"
onClick={this.onClick} onClick={this.onClick}
class={createColorClasses(this.color, { class={createColorClasses(this.color, {
[mode]: true, [mode]: true,

View File

@ -0,0 +1,11 @@
import { newE2EPage } from '@stencil/core/testing';
import { AxePuppeteer } from '@axe-core/puppeteer';
test('segment: axe', async () => {
const page = await newE2EPage({
url: '/src/components/segment/test/a11y?ionic:_testing=true'
});
const results = await new AxePuppeteer(page).analyze();
expect(results.violations.length).toEqual(0);
});

View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Segment - a11y</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link href="../../../../../css/core.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<main>
<h1>Segment</h1>
<ion-segment aria-label="Tab Options" color="dark" value="reading-list">
<ion-segment-button value="bookmarks">
<ion-label>Bookmarks</ion-label>
</ion-segment-button>
<ion-segment-button value="reading-list">
<ion-label>Reading List</ion-label>
</ion-segment-button>
<ion-segment-button value="shared-links">
<ion-label>Shared Links</ion-label>
</ion-segment-button>
</ion-segment>
</main>
</body>
</html>

View File

@ -0,0 +1,24 @@
"native" refers to this sample: https://w3c.github.io/aria-practices/examples/tabs/tabs-2/tabs.html
### Tabbing to Segment Button
| | native | Ionic |
| ------------------------ | ------------------------------------------------ | ------------------------------------------------ |
| VoiceOver macOS - Chrome | BOOKMARKS, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, tab, 1 of 3, Tab Options, tab group |
| VoiceOver macOS - Safari | BOOKMARKS, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, tab, 1 of 3, Tab Options, tab group |
| VoiceOver iOS | Bookmarks, tab | Bookmarks, tab |
| Android TalkBack | Bookmarks, tab | Bookmarks, tab |
| Windows NVDA | Tab Options, tab control, BOOKMARKS, tab, 1 of 3 | Tab Options, tab control, BOOKMARKS, tab, 1 of 3 |
### Selecting Segment Button
| | native | Ionic |
| ------------------------ | -------------------------------------------------------- | ------------------------ |
| VoiceOver macOS - Chrome | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group |
| VoiceOver macOS - Safari | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group | BOOKMARKS, selected, tab, 1 of 3, Tab Options, tab group |
| VoiceOver iOS | selected, Bookmarks, tab | selected, Bookmarks, tab |
| Android TalkBack | selected | selected |
| Windows NVDA | BOOKMARKS, tab, 1 of 3, selected | BOOKMARKS, tab, 1 of 3, selected |
Note: The `aria-label` for tablist is typically only read on the first interaction.