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"
},
"devDependencies": {
"@axe-core/puppeteer": "^4.1.1",
"@jest/core": "^26.6.3",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
@ -43,6 +44,21 @@
"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": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
@ -2029,6 +2045,15 @@
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==",
"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": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz",
@ -13802,6 +13827,15 @@
}
},
"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": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
@ -15481,6 +15515,12 @@
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==",
"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": {
"version": "6.0.0",
"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"
},
"devDependencies": {
"@axe-core/puppeteer": "^4.1.1",
"@jest/core": "^26.6.3",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",

View File

@ -166,6 +166,10 @@
}
}
:host(:focus) {
outline: none;
}
// 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() {
const { checked, type, disabled, hasIcon, hasLabel, layout, segmentEl } = this;
const { checked, type, disabled, hasIcon, hasLabel, layout, segmentEl, tabIndex } = this;
const mode = getIonMode(this);
const hasSegmentColor = () => segmentEl !== null && segmentEl.color !== undefined;
return (
<Host
role="tab"
aria-selected={checked ? 'true' : 'false'}
aria-disabled={disabled ? 'true' : null}
tabIndex={tabIndex}
class={{
[mode]: true,
'in-toolbar': hostContext('ion-toolbar', this.el),
@ -113,7 +128,7 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
>
<button
type={type}
aria-pressed={checked ? 'true' : 'false'}
tabIndex={-1}
class="button-native"
part="native"
disabled={disabled}

View File

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