From c2a991e7a3885e67ef8153f19e21e4fc83da54c9 Mon Sep 17 00:00:00 2001 From: paul Date: Mon, 2 Sep 2024 11:25:16 +0200 Subject: [PATCH 1/3] fix(a11y): add `aria-current="page"` attribute to links closes #20399 --- .../VBreadcrumbs/VBreadcrumbsItem.tsx | 2 +- packages/vuetify/src/components/VBtn/VBtn.tsx | 1 + packages/vuetify/src/components/VCard/VCard.tsx | 1 + packages/vuetify/src/components/VChip/VChip.tsx | 1 + .../vuetify/src/components/VList/VListItem.tsx | 1 + packages/vuetify/src/composables/router.tsx | 17 ++++++++++------- 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbsItem.tsx b/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbsItem.tsx index 3d85047d7e6..1a746c57ac3 100644 --- a/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbsItem.tsx +++ b/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbsItem.tsx @@ -56,7 +56,7 @@ export const VBreadcrumbsItem = genericComponent()({ { slots.default?.() ?? props.title } diff --git a/packages/vuetify/src/components/VBtn/VBtn.tsx b/packages/vuetify/src/components/VBtn/VBtn.tsx index fb8166d7919..09bcf5bf95b 100644 --- a/packages/vuetify/src/components/VBtn/VBtn.tsx +++ b/packages/vuetify/src/components/VBtn/VBtn.tsx @@ -213,6 +213,7 @@ export const VBtn = genericComponent()({ aria-busy={ props.loading ? true : undefined } disabled={ isDisabled.value || undefined } href={ link.href.value } + aria-current={ link['aria-current']?.value } tabindex={ props.loading || props.readonly ? -1 : undefined } onClick={ onClick } value={ valueAttr.value } diff --git a/packages/vuetify/src/components/VCard/VCard.tsx b/packages/vuetify/src/components/VCard/VCard.tsx index ba0d8303f07..81e9fe7556d 100644 --- a/packages/vuetify/src/components/VCard/VCard.tsx +++ b/packages/vuetify/src/components/VCard/VCard.tsx @@ -150,6 +150,7 @@ export const VCard = genericComponent()({ props.style, ]} href={ link.href.value } + aria-current={ link['aria-current']?.value } onClick={ isClickable.value && link.navigate } v-ripple={ isClickable.value && props.ripple } tabindex={ props.disabled ? -1 : undefined } diff --git a/packages/vuetify/src/components/VChip/VChip.tsx b/packages/vuetify/src/components/VChip/VChip.tsx index 958332a03f0..0f9dd9763c7 100644 --- a/packages/vuetify/src/components/VChip/VChip.tsx +++ b/packages/vuetify/src/components/VChip/VChip.tsx @@ -205,6 +205,7 @@ export const VChip = genericComponent()({ disabled={ props.disabled || undefined } draggable={ props.draggable } href={ link.href.value } + aria-current={ link['aria-current']?.value } tabindex={ isClickable.value ? 0 : undefined } onClick={ onClick } onKeydown={ isClickable.value && !isLink.value && onKeyDown } diff --git a/packages/vuetify/src/components/VList/VListItem.tsx b/packages/vuetify/src/components/VList/VListItem.tsx index 510a30e91a3..51495775dd2 100644 --- a/packages/vuetify/src/components/VList/VListItem.tsx +++ b/packages/vuetify/src/components/VList/VListItem.tsx @@ -244,6 +244,7 @@ export const VListItem = genericComponent()({ props.style, ]} href={ link.href.value } + aria-current={ link['aria-current']?.value } tabindex={ isClickable.value ? (list ? -2 : 0) : undefined } onClick={ onClick } onKeydown={ isClickable.value && !isLink.value && onKeyDown } diff --git a/packages/vuetify/src/composables/router.tsx b/packages/vuetify/src/composables/router.tsx index 09742bf0537..3635effccf3 100644 --- a/packages/vuetify/src/composables/router.tsx +++ b/packages/vuetify/src/composables/router.tsx @@ -47,6 +47,7 @@ export interface UseLink extends Omit>, 'hre isLink: ComputedRef isClickable: ComputedRef href: Ref + 'aria-current'?: ComputedRef<'page' | undefined> } export function useLink (props: LinkProps & LinkListeners, attrs: SetupContext['attrs']): UseLink { @@ -74,20 +75,22 @@ 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) + }) 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: computed(() => props.to ? link.value?.route.value.href : props.href), + 'aria-current': computed(() => isActive.value ? 'page' : undefined), } } From 8aec1052c8fc4ca8b6686eb0c9649086dc020ea9 Mon Sep 17 00:00:00 2001 From: Kael Date: Tue, 3 Sep 2024 17:14:38 +1000 Subject: [PATCH 2/3] refactor: reusable linkProps --- .../components/VBreadcrumbs/VBreadcrumbsItem.tsx | 3 +-- packages/vuetify/src/components/VBtn/VBtn.tsx | 3 +-- packages/vuetify/src/components/VCard/VCard.tsx | 3 +-- packages/vuetify/src/components/VChip/VChip.tsx | 3 +-- .../vuetify/src/components/VList/VListItem.tsx | 3 +-- packages/vuetify/src/composables/router.tsx | 16 +++++++++++----- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbsItem.tsx b/packages/vuetify/src/components/VBreadcrumbs/VBreadcrumbsItem.tsx index 1a746c57ac3..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 09bcf5bf95b..cf87067a7a8 100644 --- a/packages/vuetify/src/components/VBtn/VBtn.tsx +++ b/packages/vuetify/src/components/VBtn/VBtn.tsx @@ -212,11 +212,10 @@ export const VBtn = genericComponent()({ ]} aria-busy={ props.loading ? true : undefined } disabled={ isDisabled.value || undefined } - href={ link.href.value } - aria-current={ link['aria-current']?.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 81e9fe7556d..6141643375b 100644 --- a/packages/vuetify/src/components/VCard/VCard.tsx +++ b/packages/vuetify/src/components/VCard/VCard.tsx @@ -149,11 +149,10 @@ export const VCard = genericComponent()({ locationStyles.value, props.style, ]} - href={ link.href.value } - aria-current={ link['aria-current']?.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 0f9dd9763c7..bbaedfd2842 100644 --- a/packages/vuetify/src/components/VChip/VChip.tsx +++ b/packages/vuetify/src/components/VChip/VChip.tsx @@ -204,12 +204,11 @@ export const VChip = genericComponent()({ ]} disabled={ props.disabled || undefined } draggable={ props.draggable } - href={ link.href.value } - aria-current={ link['aria-current']?.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 51495775dd2..fae894279cd 100644 --- a/packages/vuetify/src/components/VList/VListItem.tsx +++ b/packages/vuetify/src/components/VList/VListItem.tsx @@ -243,12 +243,11 @@ export const VListItem = genericComponent()({ dimensionStyles.value, props.style, ]} - href={ link.href.value } - aria-current={ link['aria-current']?.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 3635effccf3..c7b2fba586f 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,7 +47,7 @@ export interface UseLink extends Omit>, 'hre isLink: ComputedRef isClickable: ComputedRef href: Ref - 'aria-current'?: ComputedRef<'page' | undefined> + linkProps: Record } export function useLink (props: LinkProps & LinkListeners, attrs: SetupContext['attrs']): UseLink { @@ -59,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 @@ -82,6 +84,7 @@ export function useLink (props: LinkProps & LinkListeners, attrs: SetupContext[' 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, @@ -89,8 +92,11 @@ export function useLink (props: LinkProps & LinkListeners, attrs: SetupContext[' isActive, route: link.value?.route, navigate: link.value?.navigate, - href: computed(() => props.to ? link.value?.route.value.href : props.href), - 'aria-current': computed(() => isActive.value ? 'page' : undefined), + href, + linkProps: reactive({ + href, + ariaCurrent: computed(() => isActive.value ? 'page' : undefined), + }), } } From d028058d13fed12096805bfe4509014060fdb683 Mon Sep 17 00:00:00 2001 From: Kael Date: Tue, 3 Sep 2024 17:26:21 +1000 Subject: [PATCH 3/3] kebab-case --- packages/vuetify/src/composables/router.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vuetify/src/composables/router.tsx b/packages/vuetify/src/composables/router.tsx index c7b2fba586f..c3e086f126b 100644 --- a/packages/vuetify/src/composables/router.tsx +++ b/packages/vuetify/src/composables/router.tsx @@ -95,7 +95,7 @@ export function useLink (props: LinkProps & LinkListeners, attrs: SetupContext[' href, linkProps: reactive({ href, - ariaCurrent: computed(() => isActive.value ? 'page' : undefined), + 'aria-current': computed(() => isActive.value ? 'page' : undefined), }), } }