diff --git a/__tests__/e2e/multi-sidebar/index.test.ts b/__tests__/e2e/multi-sidebar/index.test.ts index b6482bdbb..7c198d44d 100644 --- a/__tests__/e2e/multi-sidebar/index.test.ts +++ b/__tests__/e2e/multi-sidebar/index.test.ts @@ -18,6 +18,31 @@ describe('test multi sidebar sort root', () => { 'Markdown Extensions' ]) }) + + test('collapsible sidebar headings do not contain nested buttons', async () => { + const sidebarItem = page.locator('.VPSidebarItem.level-0').first() + const item = sidebarItem.locator('> .item') + const caret = item.locator('> .caret') + + expect(await item.getAttribute('role')).toBe('button') + expect(await item.getAttribute('tabindex')).toBe('0') + expect(await item.getAttribute('aria-expanded')).toBe('true') + expect(await caret.getAttribute('role')).toBeNull() + expect(await caret.getAttribute('tabindex')).toBeNull() + expect(await caret.getAttribute('aria-hidden')).toBe('true') + + await caret.click() + expect(await sidebarItem.getAttribute('class')).toContain('collapsed') + expect(await item.getAttribute('aria-expanded')).toBe('false') + + await item.press('Enter') + expect(await sidebarItem.getAttribute('class')).not.toContain('collapsed') + expect(await item.getAttribute('aria-expanded')).toBe('true') + + await item.press(' ') + expect(await sidebarItem.getAttribute('class')).toContain('collapsed') + expect(await item.getAttribute('aria-expanded')).toBe('false') + }) }) describe('test multi sidebar sort order', () => { diff --git a/src/client/theme-default/components/VPSidebarItem.vue b/src/client/theme-default/components/VPSidebarItem.vue index d71d10621..e09808ca4 100644 --- a/src/client/theme-default/components/VPSidebarItem.vue +++ b/src/client/theme-default/components/VPSidebarItem.vue @@ -31,7 +31,13 @@ const textTag = computed(() => { : `h${props.depth + 2}` }) -const itemRole = computed(() => (isLink.value ? undefined : 'button')) +const isItemButton = computed(() => { + return !isLink.value && collapsible.value && hasChildren.value +}) + +const itemRole = computed(() => (isItemButton.value ? 'button' : undefined)) + +const itemTabIndex = computed(() => (isItemButton.value ? 0 : undefined)) const classes = computed(() => [ [`level-${props.depth}`], @@ -42,15 +48,34 @@ const classes = computed(() => [ { 'has-active': hasActiveLink.value } ]) -function onItemInteraction(e: MouseEvent | Event) { - if ('key' in e && e.key !== 'Enter') { - return +function onItemInteraction(e: MouseEvent | KeyboardEvent) { + if ('key' in e) { + if (e.key !== 'Enter' && e.key !== ' ') { + return + } + + e.preventDefault() } - !props.item.link && toggle() + + toggle() } -function onCaretClick() { - props.item.link && toggle() +function onCaretClick(e: MouseEvent | Event) { + if (props.item.link) { + e.stopPropagation() + toggle() + } +} + +function onCaretKeyDown(e: KeyboardEvent) { + if (!props.item.link) { + return + } + + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onCaretClick(e) + } } @@ -60,12 +85,13 @@ function onCaretClick() { v-if="item.text" class="item" :role="itemRole" + :aria-expanded="isItemButton ? !collapsed : undefined" v-on=" - item.items + isItemButton ? { click: onItemInteraction, keydown: onItemInteraction } : {} " - :tabindex="item.items && 0" + :tabindex="itemTabIndex" >
@@ -84,11 +110,13 @@ function onCaretClick() {