Skip to content

Commit 2b76463

Browse files
feat(theme-default): improve a11y of CodeGroup (#163)
Co-authored-by: meteorlxy <[email protected]>
1 parent 6a96af0 commit 2b76463

File tree

3 files changed

+68
-13
lines changed

3 files changed

+68
-13
lines changed

packages/@vuepress/theme-default/src/client/components/global/CodeGroup.ts

+59-12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,43 @@ export default defineComponent({
88
// index of current active item
99
const activeIndex = ref(-1)
1010

11+
// refs of the tab buttons
12+
const tabRefs = ref<HTMLButtonElement[]>([])
13+
14+
// activate next tab
15+
const activateNext = (i = activeIndex.value): void => {
16+
if (i < tabRefs.value.length - 1) {
17+
activeIndex.value = i + 1
18+
} else {
19+
activeIndex.value = 0
20+
}
21+
tabRefs.value[activeIndex.value].focus()
22+
}
23+
24+
// activate previous tab
25+
const activatePrev = (i = activeIndex.value): void => {
26+
if (i > 0) {
27+
activeIndex.value = i - 1
28+
} else {
29+
activeIndex.value = tabRefs.value.length - 1
30+
}
31+
tabRefs.value[activeIndex.value].focus()
32+
}
33+
34+
// handle keyboard event
35+
const keyboardHandler = (event: KeyboardEvent, i: number): void => {
36+
if (event.key === ' ' || event.key === 'Enter') {
37+
event.preventDefault()
38+
activeIndex.value = i
39+
} else if (event.key === 'ArrowRight') {
40+
event.preventDefault()
41+
activateNext(i)
42+
} else if (event.key === 'ArrowLeft') {
43+
event.preventDefault()
44+
activatePrev(i)
45+
}
46+
}
47+
1148
return () => {
1249
// NOTICE: here we put the `slots.default()` inside the render function to make
1350
// the slots reactive, otherwise the slot content won't be changed once the
@@ -23,13 +60,16 @@ export default defineComponent({
2360
return vnode as VNode & { props: Exclude<VNode['props'], null> }
2461
})
2562

63+
// clear tabRefs for HMR
64+
tabRefs.value = []
65+
2666
// do not render anything if there is no code-group-item
2767
if (items.length === 0) {
2868
return null
2969
}
3070

31-
if (activeIndex.value === -1) {
32-
// initial state
71+
if (activeIndex.value < 0 || activeIndex.value > items.length - 1) {
72+
// if `activeIndex` is invalid
3373

3474
// find the index of the code-group-item with `active` props
3575
activeIndex.value = items.findIndex(
@@ -41,8 +81,6 @@ export default defineComponent({
4181
activeIndex.value = 0
4282
}
4383
} else {
44-
// re-render triggered by modifying `activeIndex` ref
45-
4684
// set the active item
4785
items.forEach((vnode, i) => {
4886
vnode.props.active = i === activeIndex.value
@@ -56,24 +94,33 @@ export default defineComponent({
5694
h(
5795
'ul',
5896
{ class: 'code-group__ul' },
59-
items.map((vnode, i) =>
60-
h(
97+
items.map((vnode, i) => {
98+
const isActive = i === activeIndex.value
99+
100+
return h(
61101
'li',
62102
{ class: 'code-group__li' },
63103
h(
64104
'button',
65105
{
66-
class: `code-group__nav-tab${
67-
i === activeIndex.value
68-
? ' code-group__nav-tab-active'
69-
: ''
70-
}`,
106+
ref: (element) => {
107+
if (element) {
108+
tabRefs.value[i] = element as HTMLButtonElement
109+
}
110+
},
111+
class: {
112+
'code-group__nav-tab': true,
113+
'code-group__nav-tab-active': isActive,
114+
},
115+
ariaPressed: isActive,
116+
ariaExpanded: isActive,
71117
onClick: () => (activeIndex.value = i),
118+
onKeydown: (e) => keyboardHandler(e, i),
72119
},
73120
vnode.props.title
74121
)
75122
)
76-
)
123+
})
77124
)
78125
),
79126
items,

packages/@vuepress/theme-default/src/client/components/global/CodeGroupItem.vue

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<template>
2-
<div class="code-group-item" :class="{ 'code-group-item__active': active }">
2+
<div
3+
class="code-group-item"
4+
:class="{ 'code-group-item__active': active }"
5+
:aria-selected="active"
6+
>
37
<slot />
48
</div>
59
</template>

packages/@vuepress/theme-default/src/client/styles/code-group.scss

+4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
outline: none;
3838
}
3939

40+
.code-group__nav-tab:focus-visible {
41+
outline: 1px solid rgba(255, 255, 255, 0.9);
42+
}
43+
4044
.code-group__nav-tab-active {
4145
border-bottom: var(--c-brand) 1px solid;
4246
}

0 commit comments

Comments
 (0)