From 2b7646399116114a967a5df64266c6879babb10f Mon Sep 17 00:00:00 2001 From: "Mr.Hope" Date: Fri, 4 Jun 2021 00:46:47 +0800 Subject: [PATCH] feat(theme-default): improve a11y of CodeGroup (#163) Co-authored-by: meteorlxy --- .../src/client/components/global/CodeGroup.ts | 71 +++++++++++++++---- .../components/global/CodeGroupItem.vue | 6 +- .../src/client/styles/code-group.scss | 4 ++ 3 files changed, 68 insertions(+), 13 deletions(-) diff --git a/packages/@vuepress/theme-default/src/client/components/global/CodeGroup.ts b/packages/@vuepress/theme-default/src/client/components/global/CodeGroup.ts index b30c53fc68..f7d229949c 100644 --- a/packages/@vuepress/theme-default/src/client/components/global/CodeGroup.ts +++ b/packages/@vuepress/theme-default/src/client/components/global/CodeGroup.ts @@ -8,6 +8,43 @@ export default defineComponent({ // index of current active item const activeIndex = ref(-1) + // refs of the tab buttons + const tabRefs = ref([]) + + // activate next tab + const activateNext = (i = activeIndex.value): void => { + if (i < tabRefs.value.length - 1) { + activeIndex.value = i + 1 + } else { + activeIndex.value = 0 + } + tabRefs.value[activeIndex.value].focus() + } + + // activate previous tab + const activatePrev = (i = activeIndex.value): void => { + if (i > 0) { + activeIndex.value = i - 1 + } else { + activeIndex.value = tabRefs.value.length - 1 + } + tabRefs.value[activeIndex.value].focus() + } + + // handle keyboard event + const keyboardHandler = (event: KeyboardEvent, i: number): void => { + if (event.key === ' ' || event.key === 'Enter') { + event.preventDefault() + activeIndex.value = i + } else if (event.key === 'ArrowRight') { + event.preventDefault() + activateNext(i) + } else if (event.key === 'ArrowLeft') { + event.preventDefault() + activatePrev(i) + } + } + return () => { // NOTICE: here we put the `slots.default()` inside the render function to make // the slots reactive, otherwise the slot content won't be changed once the @@ -23,13 +60,16 @@ export default defineComponent({ return vnode as VNode & { props: Exclude } }) + // clear tabRefs for HMR + tabRefs.value = [] + // do not render anything if there is no code-group-item if (items.length === 0) { return null } - if (activeIndex.value === -1) { - // initial state + if (activeIndex.value < 0 || activeIndex.value > items.length - 1) { + // if `activeIndex` is invalid // find the index of the code-group-item with `active` props activeIndex.value = items.findIndex( @@ -41,8 +81,6 @@ export default defineComponent({ activeIndex.value = 0 } } else { - // re-render triggered by modifying `activeIndex` ref - // set the active item items.forEach((vnode, i) => { vnode.props.active = i === activeIndex.value @@ -56,24 +94,33 @@ export default defineComponent({ h( 'ul', { class: 'code-group__ul' }, - items.map((vnode, i) => - h( + items.map((vnode, i) => { + const isActive = i === activeIndex.value + + return h( 'li', { class: 'code-group__li' }, h( 'button', { - class: `code-group__nav-tab${ - i === activeIndex.value - ? ' code-group__nav-tab-active' - : '' - }`, + ref: (element) => { + if (element) { + tabRefs.value[i] = element as HTMLButtonElement + } + }, + class: { + 'code-group__nav-tab': true, + 'code-group__nav-tab-active': isActive, + }, + ariaPressed: isActive, + ariaExpanded: isActive, onClick: () => (activeIndex.value = i), + onKeydown: (e) => keyboardHandler(e, i), }, vnode.props.title ) ) - ) + }) ) ), items, diff --git a/packages/@vuepress/theme-default/src/client/components/global/CodeGroupItem.vue b/packages/@vuepress/theme-default/src/client/components/global/CodeGroupItem.vue index d5d729409a..f774dbf0b5 100644 --- a/packages/@vuepress/theme-default/src/client/components/global/CodeGroupItem.vue +++ b/packages/@vuepress/theme-default/src/client/components/global/CodeGroupItem.vue @@ -1,5 +1,9 @@ diff --git a/packages/@vuepress/theme-default/src/client/styles/code-group.scss b/packages/@vuepress/theme-default/src/client/styles/code-group.scss index 39da14d5c2..02d0b3d74e 100644 --- a/packages/@vuepress/theme-default/src/client/styles/code-group.scss +++ b/packages/@vuepress/theme-default/src/client/styles/code-group.scss @@ -37,6 +37,10 @@ outline: none; } +.code-group__nav-tab:focus-visible { + outline: 1px solid rgba(255, 255, 255, 0.9); +} + .code-group__nav-tab-active { border-bottom: var(--c-brand) 1px solid; }