From bc647f6853c6415fc928aff2774134f7eed26b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Th=C3=A9baud?= Date: Tue, 3 Sep 2024 09:27:24 +0200 Subject: [PATCH] fix(a11y): add `aria-current="page"` attribute to links (#20413) closes #20399 Co-authored-by: Kael --- .../VBreadcrumbs/VBreadcrumbsItem.tsx | 3 +-- packages/vuetify/src/components/VBtn/VBtn.tsx | 2 +- .../vuetify/src/components/VCard/VCard.tsx | 2 +- .../vuetify/src/components/VChip/VChip.tsx | 2 +- .../src/components/VList/VListItem.tsx | 2 +- packages/vuetify/src/composables/router.tsx | 27 ++++++++++++------- 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbsItem.tsx b/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbsItem.tsx index 3d85047d7e6..403bd0c6487 100644 --- a/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbsItem.tsx +++ b/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbsItem.tsx @@ -55,9 +55,8 @@ export const VBreadcrumbsItem = genericComponent()({ { !link.isLink.value ? slots.default?.() ?? props.title : ( { slots.default?.() ?? props.title } diff --git a/packages/vuetify/src/components/VBtn/VBtn.tsx b/packages/vuetify/src/components/VBtn/VBtn.tsx index fb8166d7919..cf87067a7a8 100644 --- a/packages/vuetify/src/components/VBtn/VBtn.tsx +++ b/packages/vuetify/src/components/VBtn/VBtn.tsx @@ -212,10 +212,10 @@ export const VBtn = genericComponent()({ ]} aria-busy={ props.loading ? true : undefined } disabled={ isDisabled.value || undefined } - href={ link.href.value } tabindex={ props.loading || props.readonly ? -1 : undefined } onClick={ onClick } value={ valueAttr.value } + { ...link.linkProps } > { genOverlays(true, 'v-btn') } diff --git a/packages/vuetify/src/components/VCard/VCard.tsx b/packages/vuetify/src/components/VCard/VCard.tsx index ba0d8303f07..6141643375b 100644 --- a/packages/vuetify/src/components/VCard/VCard.tsx +++ b/packages/vuetify/src/components/VCard/VCard.tsx @@ -149,10 +149,10 @@ export const VCard = genericComponent()({ locationStyles.value, props.style, ]} - href={ link.href.value } onClick={ isClickable.value && link.navigate } v-ripple={ isClickable.value && props.ripple } tabindex={ props.disabled ? -1 : undefined } + { ...link.linkProps } > { hasImage && (
diff --git a/packages/vuetify/src/components/VChip/VChip.tsx b/packages/vuetify/src/components/VChip/VChip.tsx index 958332a03f0..bbaedfd2842 100644 --- a/packages/vuetify/src/components/VChip/VChip.tsx +++ b/packages/vuetify/src/components/VChip/VChip.tsx @@ -204,11 +204,11 @@ export const VChip = genericComponent()({ ]} disabled={ props.disabled || undefined } draggable={ props.draggable } - href={ link.href.value } tabindex={ isClickable.value ? 0 : undefined } onClick={ onClick } onKeydown={ isClickable.value && !isLink.value && onKeyDown } v-ripple={[isClickable.value && props.ripple, null]} + { ...link.linkProps } > { genOverlays(isClickable.value, 'v-chip') } diff --git a/packages/vuetify/src/components/VList/VListItem.tsx b/packages/vuetify/src/components/VList/VListItem.tsx index 510a30e91a3..fae894279cd 100644 --- a/packages/vuetify/src/components/VList/VListItem.tsx +++ b/packages/vuetify/src/components/VList/VListItem.tsx @@ -243,11 +243,11 @@ export const VListItem = genericComponent()({ dimensionStyles.value, props.style, ]} - href={ link.href.value } tabindex={ isClickable.value ? (list ? -2 : 0) : undefined } onClick={ onClick } onKeydown={ isClickable.value && !isLink.value && onKeyDown } v-ripple={ isClickable.value && props.ripple } + { ...link.linkProps } > { genOverlays(isClickable.value || isActive.value, 'v-list-item') } diff --git a/packages/vuetify/src/composables/router.tsx b/packages/vuetify/src/composables/router.tsx index 09742bf0537..c3e086f126b 100644 --- a/packages/vuetify/src/composables/router.tsx +++ b/packages/vuetify/src/composables/router.tsx @@ -2,7 +2,7 @@ import { computed, nextTick, - onScopeDispose, + onScopeDispose, reactive, resolveDynamicComponent, toRef, } from 'vue' @@ -47,6 +47,7 @@ export interface UseLink extends Omit>, 'hre isLink: ComputedRef isClickable: ComputedRef href: Ref + linkProps: Record } export function useLink (props: LinkProps & LinkListeners, attrs: SetupContext['attrs']): UseLink { @@ -58,10 +59,12 @@ export function useLink (props: LinkProps & LinkListeners, attrs: SetupContext[' }) if (typeof RouterLink === 'string' || !('useLink' in RouterLink)) { + const href = toRef(props, 'href') return { isLink, isClickable, - href: toRef(props, 'href'), + href, + linkProps: reactive({ href }), } } // vue-router useLink `to` prop needs to be reactive and useLink will crash if undefined @@ -74,20 +77,26 @@ export function useLink (props: LinkProps & LinkListeners, attrs: SetupContext[' // Actual link needs to be undefined when to prop is not used const link = computed(() => props.to ? routerLink : undefined) const route = useRoute() + const isActive = computed(() => { + if (!link.value) return false + if (!props.exact) return link.value.isActive?.value ?? false + if (!route.value) return link.value.isExactActive?.value ?? false + + return link.value.isExactActive?.value && deepEqual(link.value.route.value.query, route.value.query) + }) + const href = computed(() => props.to ? link.value?.route.value.href : props.href) return { isLink, isClickable, + isActive, route: link.value?.route, navigate: link.value?.navigate, - isActive: computed(() => { - if (!link.value) return false - if (!props.exact) return link.value.isActive?.value ?? false - if (!route.value) return link.value.isExactActive?.value ?? false - - return link.value.isExactActive?.value && deepEqual(link.value.route.value.query, route.value.query) + href, + linkProps: reactive({ + href, + 'aria-current': computed(() => isActive.value ? 'page' : undefined), }), - href: computed(() => props.to ? link.value?.route.value.href : props.href), } }