Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions app/components/PackageManagerTabs.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,55 @@
<script setup lang="ts">
const selectedPM = useSelectedPackageManager()

const tablistNavigationKeys = new Set(['ArrowRight', 'ArrowLeft', 'Home', 'End'])

function onTabListKeydown(event: KeyboardEvent) {
if (!tablistNavigationKeys.has(event.key)) return
const tablist = event.currentTarget as HTMLElement | null
if (!tablist) return

const tabs = Array.from(tablist.querySelectorAll<HTMLElement>('[role="tab"]'))
const count = Math.min(tabs.length, packageManagers.length)
if (!count) return

event.preventDefault()

let activeIndex = packageManagers.findIndex(pm => pm.id === selectedPM.value)
if (activeIndex < 0) activeIndex = 0

let nextIndex = activeIndex
if (event.key === 'ArrowRight') nextIndex = (activeIndex + 1) % count
if (event.key === 'ArrowLeft') nextIndex = (activeIndex - 1 + count) % count
if (event.key === 'Home') nextIndex = 0
if (event.key === 'End') nextIndex = count - 1

const nextTab = tabs[nextIndex]
const nextId = packageManagers[nextIndex]?.id
if (nextId && nextId !== selectedPM.value) {
selectedPM.value = nextId
}

nextTick(() => nextTab?.focus())
}
</script>

<template>
<div
class="flex items-center gap-1 p-0.5 bg-bg-subtle border border-border-subtle rounded-md overflow-x-auto"
role="tablist"
:aria-label="$t('package.get_started.pm_label')"
@keydown="onTabListKeydown"
>
<button
v-for="pm in packageManagers"
:key="pm.id"
:id="`pm-tab-${pm.id}`"
role="tab"
:data-pm-tab="pm.id"
:aria-selected="selectedPM === pm.id"
:aria-controls="`pm-panel-${pm.id}`"
:tabindex="selectedPM === pm.id ? 0 : -1"
type="button"
class="pm-tab 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 hover:text-fg"
@click="selectedPM = pm.id"
>
Expand Down
40 changes: 27 additions & 13 deletions app/pages/[...package].vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ definePageMeta({
const router = useRouter()

const { packageName, requestedVersion, orgName } = usePackageRoute()
const selectedPM = useSelectedPackageManager()
const activePmId = computed(() => selectedPM.value ?? 'npm')

if (import.meta.server) {
assertValidPackageName(packageName.value)
Expand Down Expand Up @@ -845,11 +847,17 @@ function handleClick(event: MouseEvent) {
<!-- Package manager tabs -->
<PackageManagerTabs />
</div>
<ExecuteCommandTerminal
:package-name="pkg.name"
:jsr-info="jsrInfo"
:is-create-package="isCreatePkg"
/>
<div
role="tabpanel"
:id="`pm-panel-${activePmId}`"
:aria-labelledby="`pm-tab-${activePmId}`"
>
<ExecuteCommandTerminal
:package-name="pkg.name"
:jsr-info="jsrInfo"
:is-create-package="isCreatePkg"
/>
</div>
</section>

<!-- Regular packages: Install command with optional run command -->
Expand All @@ -873,14 +881,20 @@ function handleClick(event: MouseEvent) {
<!-- Package manager tabs -->
<PackageManagerTabs />
</div>
<InstallCommandTerminal
:package-name="pkg.name"
:requested-version="requestedVersion"
:jsr-info="jsrInfo"
:types-package-name="typesPackageName"
:executable-info="executableInfo"
:create-package-info="createPackageInfo"
/>
<div
role="tabpanel"
:id="`pm-panel-${activePmId}`"
:aria-labelledby="`pm-tab-${activePmId}`"
>
<InstallCommandTerminal
:package-name="pkg.name"
:requested-version="requestedVersion"
:jsr-info="jsrInfo"
:types-package-name="typesPackageName"
:executable-info="executableInfo"
:create-package-info="createPackageInfo"
/>
</div>
</section>

<div class="area-vulns space-y-6">
Expand Down
35 changes: 35 additions & 0 deletions tests/package-manager-tabs.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect, test } from '@nuxt/test-utils/playwright'

test.describe('Package Page', () => {
test('/vue → package manager tabs use roving tabindex', async ({ page, goto }) => {
await goto('/vue', { waitUntil: 'domcontentloaded' })

const tablist = page.locator('[role="tablist"]').first()
await expect(tablist).toBeVisible()

const tabs = tablist.locator('[role="tab"]')
const tabCount = await tabs.count()
expect(tabCount).toBeGreaterThan(1)

const firstTab = tabs.first()
await firstTab.focus()
await expect(firstTab).toBeFocused()

await page.keyboard.press('ArrowRight')

const secondTab = tabs.nth(1)
await expect(secondTab).toBeFocused()
await expect(secondTab).toHaveAttribute('aria-selected', 'true')
await expect(secondTab).toHaveAttribute('tabindex', '0')
await expect(firstTab).toHaveAttribute('tabindex', '-1')

const tabpanel = page.locator('[role="tabpanel"]').first()
const controls = await secondTab.getAttribute('aria-controls')
const panelId = await tabpanel.getAttribute('id')
expect(controls).toBe(panelId)

const labelledBy = await tabpanel.getAttribute('aria-labelledby')
const tabId = await secondTab.getAttribute('id')
expect(labelledBy).toBe(tabId)
})
})
Loading