Skip to content

Commit

Permalink
Ariakit vue exploration with port of Heading
Browse files Browse the repository at this point in the history
  • Loading branch information
sarayourfriend committed Sep 24, 2024
1 parent 081d174 commit c72ccdb
Show file tree
Hide file tree
Showing 11 changed files with 369 additions and 13 deletions.
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)
},
})
31 changes: 31 additions & 0 deletions frontend/src/components/ariakit/heading/meta/AHeading.stories.ts
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"]),
]),
]),
])
},
}),
}
Loading

0 comments on commit c72ccdb

Please sign in to comment.