Skip to content

Commit f59fada

Browse files
committed
feat(a11y): add roving tabindex + tabpanels for package manager tabs
1 parent 6361d3c commit f59fada

1 file changed

Lines changed: 61 additions & 2 deletions

File tree

app/pages/[...package].vue

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,37 @@ const createCommand = computed(() => {
387387
const { copied: createCopied, copy: copyCreate } = useClipboard({ copiedDuring: 2000 })
388388
const copyCreateCommand = () => copyCreate(createCommand.value)
389389
390+
const tablistNavigationKeys = new Set(['ArrowRight', 'ArrowLeft', 'Home', 'End'])
391+
392+
function onTabListKeydown(event: KeyboardEvent) {
393+
if (!tablistNavigationKeys.has(event.key)) return
394+
const tablist = event.currentTarget as HTMLElement | null
395+
if (!tablist) return
396+
397+
const tabs = Array.from(tablist.querySelectorAll<HTMLElement>('[role="tab"]'))
398+
const count = Math.min(tabs.length, packageManagers.length)
399+
if (!count) return
400+
401+
event.preventDefault()
402+
403+
let activeIndex = packageManagers.findIndex(pm => pm.id === selectedPM.value)
404+
if (activeIndex < 0) activeIndex = 0
405+
406+
let nextIndex = activeIndex
407+
if (event.key === 'ArrowRight') nextIndex = (activeIndex + 1) % count
408+
if (event.key === 'ArrowLeft') nextIndex = (activeIndex - 1 + count) % count
409+
if (event.key === 'Home') nextIndex = 0
410+
if (event.key === 'End') nextIndex = count - 1
411+
412+
const nextTab = tabs[nextIndex]
413+
const nextId = packageManagers[nextIndex]?.id
414+
if (nextId && nextId !== selectedPM.value) {
415+
selectedPM.value = nextId
416+
}
417+
418+
nextTick(() => nextTab?.focus())
419+
}
420+
390421
// Primary run command parts
391422
const runCommandParts = computed(() => {
392423
if (!executableInfo.value?.hasExecutable) return []
@@ -947,12 +978,18 @@ function handleClick(event: MouseEvent) {
947978
class="flex items-center gap-1 p-0.5 bg-bg-subtle border border-border-subtle rounded-md"
948979
role="tablist"
949980
aria-label="Package manager"
981+
@keydown="onTabListKeydown"
950982
>
951983
<button
952984
v-for="pm in packageManagers"
953985
:key="pm.id"
986+
:id="`pm-tab-${pm.id}`"
954987
role="tab"
955988
:aria-selected="isMounted && selectedPM === pm.id"
989+
:aria-controls="`pm-panel-${pm.id}`"
990+
:tabindex="isMounted && selectedPM === pm.id ? 0 : -1"
991+
:data-pm-id="pm.id"
992+
type="button"
956993
class="px-2 py-1.5 font-mono text-xs rounded transition-colors duration-150 border border-solid focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 inline-flex items-center gap-1.5"
957994
:class="
958995
isMounted && selectedPM === pm.id
@@ -966,7 +1003,15 @@ function handleClick(event: MouseEvent) {
9661003
</button>
9671004
</div>
9681005
</div>
969-
<div class="relative group">
1006+
<div
1007+
v-for="pm in packageManagers"
1008+
:key="pm.id"
1009+
role="tabpanel"
1010+
:id="`pm-panel-${pm.id}`"
1011+
:aria-labelledby="`pm-tab-${pm.id}`"
1012+
v-show="selectedPM === pm.id"
1013+
class="relative group"
1014+
>
9701015
<!-- Terminal-style execute command -->
9711016
<div class="bg-bg-subtle border border-border rounded-lg overflow-hidden">
9721017
<div class="flex gap-1.5 px-3 pt-2 sm:px-4 sm:pt-3">
@@ -1027,12 +1072,18 @@ function handleClick(event: MouseEvent) {
10271072
class="flex items-center gap-1 p-0.5 bg-bg-subtle border border-border-subtle rounded-md overflow-x-auto"
10281073
role="tablist"
10291074
:aria-label="$t('package.get_started.pm_label')"
1075+
@keydown="onTabListKeydown"
10301076
>
10311077
<button
10321078
v-for="pm in packageManagers"
10331079
:key="pm.id"
1080+
:id="`pm-tab-${pm.id}`"
10341081
role="tab"
10351082
:aria-selected="isMounted && selectedPM === pm.id"
1083+
:aria-controls="`pm-panel-${pm.id}`"
1084+
:tabindex="isMounted && selectedPM === pm.id ? 0 : -1"
1085+
:data-pm-id="pm.id"
1086+
type="button"
10361087
class="px-2 py-1.5 font-mono text-xs rounded transition-colors duration-150 border border-solid focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 inline-flex items-center gap-1.5"
10371088
:class="
10381089
isMounted && selectedPM === pm.id
@@ -1046,7 +1097,15 @@ function handleClick(event: MouseEvent) {
10461097
</button>
10471098
</div>
10481099
</div>
1049-
<div class="relative group">
1100+
<div
1101+
v-for="pm in packageManagers"
1102+
:key="pm.id"
1103+
role="tabpanel"
1104+
:id="`pm-panel-${pm.id}`"
1105+
:aria-labelledby="`pm-tab-${pm.id}`"
1106+
v-show="selectedPM === pm.id"
1107+
class="relative group"
1108+
>
10501109
<!-- Terminal-style install command -->
10511110
<div class="bg-bg-subtle border border-border rounded-lg overflow-hidden">
10521111
<div class="flex gap-1.5 px-3 pt-2 sm:px-4 sm:pt-3">

0 commit comments

Comments
 (0)