Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ariakit vue exploration with port of Heading #4984

Closed
wants to merge 2 commits into from
Closed
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
19 changes: 10 additions & 9 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"doc:media-props": "node ./scripts/document-media.js"
},
"dependencies": {
"@ariakit/core": "^0.4.10",
"@floating-ui/dom": "^1.6.8",
"@nuxtjs/plausible": "^1.0.2",
"@nuxtjs/robots": "^4.0.2",
Expand Down Expand Up @@ -91,6 +92,13 @@
"@nuxtjs/storybook": "npm:@nuxtjs/storybook@nightly",
"@playwright/test": "1.46.1",
"@storybook-vue/nuxt": "npm:@storybook-vue/nuxt@nightly",
"@storybook/addon-essentials": "8.2.9",
"@storybook/addon-interactions": "8.2.9",
"@storybook/addon-links": "8.2.9",
"@storybook/blocks": "8.2.9",
"@storybook/builder-vite": "8.2.9",
"@storybook/test": "8.2.9",
"@storybook/vue3": "8.2.9",
"@testing-library/user-event": "^14.5.2",
"@testing-library/vue": "^8.1.0",
"@vitest/coverage-v8": "^2.0.5",
Expand All @@ -105,21 +113,14 @@
"npm-run-all2": "^6.2.2",
"nuxt": "3.13.1",
"rimraf": "^6.0.1",
"storybook": "8.2.9",
"talkback": "^4.2.0",
"typescript": "5.5.4",
"vitest": "^2.0.5",
"vitest-dom": "^0.1.1",
"vue": "3.5.0",
"vue-router": "^4.4.0",
"vue-tsc": "2.1.4",
"storybook": "8.2.9",
"@storybook/vue3": "8.2.9",
"@storybook/addon-links": "8.2.9",
"@storybook/builder-vite": "8.2.9",
"@storybook/addon-essentials": "8.2.9",
"@storybook/addon-interactions": "8.2.9",
"@storybook/test": "8.2.9",
"@storybook/blocks": "8.2.9"
"vue-tsc": "2.1.4"
},
"browserslist": [
"> 1%",
Expand Down
11 changes: 7 additions & 4 deletions frontend/src/components/VFourOhFour.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { useAnalytics } from "~/composables/use-analytics"
import { ALL_MEDIA } from "~/constants/media"
import { skipToContentTargetId } from "~/constants/window"

import AHeadingLevel from "~/components/ariakit/heading/AHeadingLevel.vue"
import AHeading from "~/components/ariakit/heading/AHeading.vue"
import VLink from "~/components/VLink.vue"
import VStandaloneSearchBar from "~/components/VHeader/VSearchBar/VStandaloneSearchBar.vue"
import VSvg from "~/components/VSvg/VSvg.vue"
Expand Down Expand Up @@ -47,14 +49,15 @@ useHead({
>
<!-- Push content by 1/4th height without absolute positioning. -->
<div class="spacer grow" />
<main
<AHeadingLevel
:id="skipToContentTargetId"
as="main"
tabindex="-1"
class="z-10 grow-[3] space-y-4 lg:space-y-6"
>
<h1 class="heading-5 lg:heading-2 mb-6 lg:mb-10 lg:leading-tight">
<AHeading class="heading-5 lg:heading-2 mb-6 lg:mb-10 lg:leading-tight">
{{ $t("404.title") }}
</h1>
</AHeading>
<p class="sr-only">{{ error }}</p>
<p class="label-bold lg:heading-6">
<i18n-t scope="global" keypath="404.main" tag="span">
Expand All @@ -68,7 +71,7 @@ useHead({
</i18n-t>
</p>
<VStandaloneSearchBar route="404" @submit="handleSearch" />
</main>
</AHeadingLevel>
</div>
</div>
</template>
40 changes: 40 additions & 0 deletions frontend/src/components/ariakit/heading/AHeading.composable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { computed, MaybeRef, onUnmounted } from "vue"

import { useMemoize } from "@vueuse/core"

import { useTagName } from "~/composables/ariakit"

import type { Renderable } from "~/types/ariakit"

import { HeadingContext } from "./HeadingContext"

export interface UseHeadingOptions {
templateRef: MaybeRef<Element | null>
element?: Renderable
}

export function useHeading({ templateRef, element }: UseHeadingOptions) {
const level = HeadingContext.inject() || 1
const Element = element || (`h${level}` as const)
const tagName = useTagName(
templateRef,
typeof element === "string" ? element : `h${level}`
)
const getIsNativeHeading = useMemoize((tagName: string) =>
/^h\d$/.test(tagName)
)
const isNativeHeading = computed(
() => !!tagName.value && getIsNativeHeading(tagName.value)
)
onUnmounted(() => getIsNativeHeading.clear())

const attributes = computed(() => ({
role: !isNativeHeading.value ? "heading" : undefined,
"aria-level": !isNativeHeading.value ? level : undefined,
}))

return {
Element,
attributes,
}
}
24 changes: 24 additions & 0 deletions frontend/src/components/ariakit/heading/AHeading.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup lang="ts" generic="T extends Renderable = 'h1'">
import { ref, mergeProps, computed } from "vue"

import type { Renderable, AsProps } from "~/types/ariakit"

import { useHeading } from "./AHeading.composable"

const { as, asProps } = defineProps<AsProps<T>>()
const templateRef = ref<Element | null>(null)
const { Element, attributes } = useHeading({ element: as, templateRef })
const mergedProps = computed(() => mergeProps(asProps ?? {}, attributes.value))
</script>

<template>
<component :is="Element" v-bind="mergedProps" :ref="templateRef">
<slot
v-bind="{
Element,
attributes,
setTemplateRef: (e: Element) => (templateRef = e),
}"
/>
</component>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { HeadingContext, type HeadingLevels } from "./HeadingContext"

export interface HeadingLevelOptions {
level?: HeadingLevels
}

export function useHeadingLevel({ level }: HeadingLevelOptions) {
const contextLevel = HeadingContext.inject() || 0
const nextLevel = Math.max(
Math.min(level || contextLevel + 1, 6),
1
) as HeadingLevels
HeadingContext.provide(nextLevel)
return nextLevel
}
19 changes: 19 additions & 0 deletions frontend/src/components/ariakit/heading/AHeadingLevel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup lang="ts" generic="T extends Renderable = never">
import type { Renderable, AsProps } from "~/types/ariakit"

import {
useHeadingLevel,
type HeadingLevelOptions,
} from "./AHeadingLevel.composable"

const props = defineProps<HeadingLevelOptions & AsProps<T>>()

useHeadingLevel(props)
</script>

<template>
<component :is="as" v-if="as" v-bind="asProps ?? {}">
<slot />
</component>
<slot v-else />
</template>
17 changes: 17 additions & 0 deletions frontend/src/components/ariakit/heading/HeadingContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { provide, inject, type InjectionKey } from "vue"

export type HeadingLevels = 1 | 2 | 3 | 4 | 5 | 6

export type HeadingContext = HeadingLevels
const HeadingContextKey: InjectionKey<HeadingContext> =
Symbol.for("ariakit-heading")

export const HeadingContext = Object.freeze({
key: HeadingContextKey,
provide(context: HeadingContext) {
provide(HeadingContextKey, context)
},
inject() {
return inject(HeadingContextKey, 0)
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { h } from "vue"

import AHeading from "~/components/ariakit/heading/AHeading.vue"

import AHeadingLevel from "~/components/ariakit/heading/AHeadingLevel.vue"

import type { Meta, StoryObj } from "@storybook/vue3"

const meta = {
title: "Ariakit/Heading",
} satisfies Meta<typeof AHeading>

export default meta
type Story = StoryObj<typeof meta>

export const AHeadingStory: Story = {
render: () => ({
components: { AHeading, AHeadingLevel },
setup() {
return () =>
h("div", { class: "wrapper" }, [
h(AHeadingLevel, null, [
h(AHeading, { class: "heading" }, ["First heading"]),
h(AHeadingLevel, null, [
h(AHeading, { as: { element: "div" } }, ["Second heading"]),
]),
]),
])
},
}),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { computed, MaybeRef, unref } from "vue"

export interface SeparatorOptions {
orientation?: MaybeRef<"horizontal" | "vertical">
}

export function useSeparator({ orientation = "horizontal" }: SeparatorOptions) {
const attributes = computed(() => ({
role: "separator",
"aria-orientation": unref(orientation),
}))

return { attributes }
}
20 changes: 20 additions & 0 deletions frontend/src/components/ariakit/separator/ASeparator.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script setup lang="ts" generic="TIs extends Renderable = 'hr'">
import { computed, mergeProps } from "vue"

import type { Renderable, AsProps } from "~/types/ariakit"

import { useSeparator, type SeparatorOptions } from "./ASeparator.composable"

const {
as = "hr",
asProps,
orientation,
} = defineProps<SeparatorOptions & AsProps<TIs>>()

const { attributes } = useSeparator({ orientation })
const mergedProps = computed(() => mergeProps(attributes.value, asProps ?? {}))
</script>

<template>
<component :is="as" v-if="as" v-bind="mergedProps" />
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { h } from "vue"

import ASeparator from "~/components/ariakit/separator/ASeparator.vue"

import type { Meta, StoryObj } from "@storybook/vue3"

const meta = {
title: "Ariakit/Separator",
} satisfies Meta<typeof ASeparator>

export default meta
type Story = StoryObj<typeof meta>

export const AHeadingStory: Story = {
render: () => ({
components: { ASeparator },
setup() {
return () =>
h("p", { class: "wrapper" }, [
"Content before the separator",
h(ASeparator, null, []),
"Content after the separator",
])
},
}),
}

export const AHeadingAsStory: Story = {
render: () => ({
components: { ASeparator },
setup() {
return () =>
h("p", { class: "wrapper" }, [
"The separator below is rendered as a styled div",
h(ASeparator, { as: "div", class: "border border-dashed" }, []),
"If you inspect it, you'll see that AHeading set the correct role and aria attributes to make the div a separator",
])
},
}),
}
Loading
Loading