Skip to content

Commit 2e51ef3

Browse files
authored
Merge pull request #742 from craftcms/a11y/tab-aria
Updates tabs/tabpanel to match markup/ARIA for tabs with automated activation
2 parents cb56325 + 4bbe590 commit 2e51ef3

File tree

3 files changed

+96
-35
lines changed

3 files changed

+96
-35
lines changed
Lines changed: 94 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
<template>
22
<div class="code-toggle">
3-
<ul class="code-language-switcher theme-default-content-override" v-if="!usePageToggle">
4-
<li v-for="(language, index) in languages" :key="index">
5-
<button
6-
:class="{ active: isSelectedLanguage(language) }"
7-
:aria-selected="isSelectedLanguage(language)"
8-
role="tab"
9-
:aria-controls="'#' + getLanguageTabId(language)"
10-
@click="setLanguage(language)"
11-
>{{ getLanguageLabel(language) }}</button>
12-
</li>
13-
</ul>
3+
<div class="code-lang-switcher theme-default-content-override" role="tablist" v-if="!usePageToggle">
4+
<button
5+
v-for="(language, index) in languages"
6+
:key="language"
7+
:class="{ active: isSelectedTab(language) }"
8+
:aria-selected="isSelectedTab(language)"
9+
:id="getTabId(language)"
10+
:data-language="language"
11+
role="tab"
12+
:aria-controls="getTabPanelId(language)"
13+
@click="setLanguage(language)"
14+
:tabindex="isSelectedTab(language) ? null : '-1'"
15+
v-on:keyup="handleKeyup"
16+
>{{ getLanguageLabel(language) }}</button>
17+
</div>
1418
<div
1519
v-for="(language, index) in languages"
20+
tabindex="0"
1621
:key="index"
17-
:id="getLanguageTabId(language)"
18-
:hidden="!isSelectedLanguage(language)"
22+
:id="getTabPanelId(language)"
23+
:hidden="!isSelectedTab(language)"
24+
:aria-labelledby="getTabId(language)"
1925
role="tabpanel">
2026
<slot :name="language" />
2127
</div>
@@ -42,48 +48,63 @@
4248
}
4349
}
4450
45-
ul.code-language-switcher {
51+
.code-lang-switcher {
4652
@apply flex flex-row rounded-t box-border m-0 p-2;
4753
background: var(--border-color);
4854
z-index: 2;
55+
gap: .3rem;
4956
50-
li {
51-
@apply p-0 mr-1 list-none;
52-
53-
button {
54-
@apply block py-3 px-4 font-medium text-xs tracking-wider uppercase leading-none cursor-pointer rounded;
57+
button {
58+
@apply block py-3 px-4 font-medium text-xs tracking-wider uppercase leading-none cursor-pointer rounded;
59+
border: 1px solid transparent;
60+
&:hover {
61+
background-color: var(--sidebar-bg-color);
62+
}
5563
56-
&:hover {
57-
background-color: var(--sidebar-bg-color);
58-
}
64+
&:focus-visible {
65+
outline: var(--custom-focus-outline);
66+
outline-offset: 2px;
67+
}
5968
60-
&.active {
61-
color: var(--text-color);
62-
background-color: var(--bg-color);
63-
}
69+
&.active {
70+
border-color: var(--active-tab-border-color);
71+
color: var(--text-color);
72+
background-color: var(--bg-color);
6473
}
6574
}
6675
}
6776
6877
.theme-default-content {
69-
ul.code-language-switcher {
78+
.code-lang-switcher {
7079
@apply mb-0;
7180
}
7281
}
7382
</style>
7483

7584
<script>
76-
import { isStarted } from 'nprogress';
85+
import { v4 as uuidv4 } from 'uuid';
7786
7887
export default {
7988
props: ["languages", "labels"],
8089
8190
data() {
8291
return {
8392
selectedLanguage: this.languages[0],
93+
uniqueId: null,
94+
focusedTabIndex: null,
8495
};
8596
},
8697
98+
mounted() {
99+
this.uniqueId = uuidv4();
100+
},
101+
102+
watch: {
103+
selectedLanguage(newLanguage) {
104+
this.$el.querySelector(`button[data-language="${newLanguage}"]`).focus();
105+
},
106+
},
107+
87108
computed: {
88109
usePageToggle() {
89110
if (this.$page === undefined) {
@@ -98,6 +119,12 @@ export default {
98119
setLanguage(language) {
99120
this.selectedLanguage = language;
100121
},
122+
getLanguageFromIndex(index) {
123+
return this.languages[index];
124+
},
125+
getIndexFromLanguage(language) {
126+
return this.languages.indexOf(language);
127+
},
101128
getLanguageLabel(language) {
102129
if (this.labels && this.labels[language]) {
103130
return this.labels[language];
@@ -111,20 +138,53 @@ export default {
111138
(themeLanguages && themeLanguages[language]) ||
112139
language
113140
);
114-
115-
return language;
116141
},
117-
isSelectedLanguage(language) {
142+
isSelectedTab(language) {
118143
return (
119144
language ==
120145
(this.usePageToggle
121146
? this.$store.state.codeLanguage
122147
: this.selectedLanguage)
123148
);
124149
},
125-
getLanguageTabId(language) {
126-
return `tab-${this._uid}-${language}`;
127-
}
150+
getTabId(language) {
151+
return `tab-${this.uniqueId}-${language}`;
152+
},
153+
getTabPanelId(language) {
154+
return `tabpanel-${this.uniqueId}-${language}`;
155+
},
156+
getNextIndex(index) {
157+
return index + 1 <= this.languages.length - 1 ? index + 1 : 0;
158+
},
159+
getPrevIndex(index) {
160+
return index - 1 >= 0 ? index - 1 : this.languages.length - 1;
161+
},
162+
handleKeyup(event) {
163+
const {keyCode, target} = event;
164+
let indexToFocus;
165+
const currentIndex = this.getIndexFromLanguage(target.getAttribute('data-language'));
166+
167+
switch (keyCode) {
168+
case 37:
169+
indexToFocus = this.getPrevIndex(currentIndex);
170+
break;
171+
case 39:
172+
indexToFocus = this.getNextIndex(currentIndex);
173+
break;
174+
case 36:
175+
indexToFocus = 0;
176+
break;
177+
case 35:
178+
indexToFocus = this.languages.length - 1;
179+
break;
180+
default:
181+
return;
182+
}
183+
184+
if (indexToFocus !== undefined) {
185+
this.setLanguage(this.getLanguageFromIndex(indexToFocus));
186+
}
187+
},
128188
}
129189
};
130190
</script>

docs/.vuepress/theme/styles/base.pcss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
--white: theme("colors.white");
2828
--black: theme("colors.black");
2929
--custom-focus-outline: 2px solid var(--link-color-default);
30+
--active-tab-border-color: (theme("colors.slate"));
3031

3132
/* Custom button */
3233
--button-background-color: theme("colors.blue.default");

docs/.vuepress/theme/styles/color-mode.pcss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@
219219
border-bottom: 1px solid var(--border-color);
220220
}
221221

222-
ul.code-language-switcher {
222+
div.code-language-switcher {
223223
background: var(--border-color);
224224

225225
li a.active {

0 commit comments

Comments
 (0)