-
Notifications
You must be signed in to change notification settings - Fork 0
feat(2417): add AvSkipLinks #344
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # Navigation - `AvSkipLinks` | ||
|
|
||
| ## ✨ Introduction | ||
|
|
||
| The `AvSkipLinks` component is ideal for providing skip links to improve navigation and accessibility. | ||
|
|
||
| ## 🏗️ Structure | ||
|
|
||
| - The component is a `nav` element with the `av-skip-links` class. | ||
| - Props allow you to define the skip links with their labels and target IDs. | ||
|
|
||
| ## 🏷️ Props | ||
|
|
||
| | Name | Type | Default | Mandatory | Description | | ||
| | --- | --- | --- | --- | --- | | ||
| | `skipLinks` | `Array<{ label: string, targetId: string }>` | `[]` | ✅ | The list of skip links to display. Each skip link should have a `label` and a `targetId`. | | ||
|
|
||
| ## 🔊 Events | ||
|
|
||
| None. | ||
|
|
||
| ## 🎨 Slots | ||
|
|
||
| None. | ||
|
|
||
| ## 🚀 Storybook demos | ||
|
|
||
| You can find examples of use and demo of the component on its dedicated [Storybook page](https://avenirs-esr.github.io/avenirs-dsav/storybook/?path=/docs/components-navigation-avskiplinks--docs). | ||
|
|
||
| ## 💡 Examples of use | ||
|
|
||
| ```vue | ||
| <template> | ||
| <AvSkipLinks | ||
| :skip-links="[ | ||
| { label: 'Aller au contenu principal', targetId: 'main' }, | ||
| { label: 'Aller au pied de page', targetId: 'footer' }, | ||
| ]" | ||
| /> | ||
| </template> | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import type { Meta, StoryFn } from '@storybook/vue3' | ||
| import AvSkipLinks, { type AvSkipLinksProps } from '@/components/navigation/AvSkipLinks/AvSkipLinks.vue' | ||
|
|
||
| /** | ||
| * <h1 class="n1">Navigation - <code>AvSkipLinks</code></h1> | ||
| * | ||
| * <h2 class="n2">✨ Introduction</h2> | ||
| * | ||
| * <p> | ||
| * <span class="b2-regular"> | ||
| * The <code>AvSkipLinks</code> component provides skip links to improve navigation and accessibility. | ||
| * </span> | ||
| * </p> | ||
| * | ||
| * <h2 class="n2">🏗️ Structure</h2> | ||
| * | ||
| * <ul> | ||
| * <li> | ||
| * <span class="b2-regular"> | ||
| * The component is a <code>nav</code> element with the <code>av-skip-links</code> class. | ||
| * </span> | ||
| * </li> | ||
| * <li> | ||
| * <span class="b2-regular"> | ||
| * Props allow you to define the skip links with their labels and target IDs. | ||
| * </span> | ||
| * </li> | ||
| * </ul> | ||
| */ | ||
| const meta: Meta<AvSkipLinksProps> = { | ||
| title: 'Components/Navigation/AvSkipLinks', | ||
| component: AvSkipLinks, | ||
| tags: ['autodocs'], | ||
| argTypes: { | ||
| skipLinks: { | ||
| control: 'object', | ||
| description: 'Array of skip links with label and targetId.', | ||
| }, | ||
| }, | ||
| args: { | ||
| skipLinks: [ | ||
| { label: 'Go to main content', targetId: 'main' }, | ||
| { label: 'Go to footer', targetId: 'footer' }, | ||
| ], | ||
| }, | ||
| } | ||
|
|
||
| export default meta | ||
|
|
||
| const Template: StoryFn<AvSkipLinksProps> = args => ({ | ||
| components: { AvSkipLinks }, | ||
| setup () { | ||
| return { args } | ||
| }, | ||
| template: ` | ||
| <AvSkipLinks v-bind="args" /> | ||
| <p>Press tab to navigate through the skip links.</p> | ||
| `, | ||
| }) | ||
|
|
||
| export const Default = Template.bind({}) | ||
| Default.args = {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import type { PropType } from 'vue' | ||
| import type { SkipLink } from '@/components/navigation/AvSkipLinks/AvSkipLinks.types' | ||
|
|
||
| export const AvSkipLinksStub = defineComponent({ | ||
| name: 'AvSkipLinksStub', | ||
| props: { | ||
| skipLinks: { | ||
| type: Array as PropType<SkipLink[]>, | ||
| default: [], | ||
| }, | ||
| }, | ||
| template: ` | ||
| <nav aria-label="Accès rapide" data-testid="av-skip-links"> | ||
| <ul> | ||
| <li v-for="(link, index) in skipLinks" :key="index"> | ||
| <a :href="'#' + link.targetId">{{ link.label }}</a> | ||
| </li> | ||
| </ul> | ||
| </nav> | ||
| `, | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import { mount, type VueWrapper } from '@vue/test-utils' | ||
| import { beforeEach } from 'vitest' | ||
| import AvSkipLinks from '@/components/navigation/AvSkipLinks/AvSkipLinks.vue' | ||
| import { BddTest } from '@/tests' | ||
|
|
||
| BddTest().given('an AvSkipLinks component', () => { | ||
| let wrapper: VueWrapper<InstanceType<typeof AvSkipLinks>> | ||
|
|
||
| const skipLinks = [ | ||
| { label: 'Aller au contenu principal', targetId: 'main' }, | ||
| { label: 'Aller au pied de page', targetId: 'footer' }, | ||
| ] | ||
|
|
||
| BddTest().when('the component is mounted with skip links', () => { | ||
| beforeEach(() => { | ||
| wrapper = mount(AvSkipLinks, { | ||
| props: { skipLinks }, | ||
| }) | ||
| }) | ||
|
|
||
| BddTest().then('it should render the nav', () => { | ||
| expect(wrapper.find('nav').exists()).toBe(true) | ||
| }) | ||
|
|
||
| BddTest().then('it should render the skip links', () => { | ||
| const skipLinksElements = wrapper.findAll('a') | ||
| expect(skipLinksElements).toHaveLength(skipLinks.length) | ||
|
|
||
| skipLinks.forEach((link, index) => { | ||
| const skipLinkElement = skipLinksElements[index] | ||
| expect(skipLinkElement.text()).toBe(link.label) | ||
| expect(skipLinkElement.attributes('href')).toBe(`#${link.targetId}`) | ||
| }) | ||
| }) | ||
| }) | ||
|
|
||
| BddTest().when('the component is mounted without skip links', () => { | ||
| beforeEach(() => { | ||
| wrapper = mount(AvSkipLinks, { | ||
| props: { skipLinks: [] }, | ||
| }) | ||
| }) | ||
|
|
||
| BddTest().then('it should not render the nav', () => { | ||
| expect(wrapper.find('nav').exists()).toBe(false) | ||
| }) | ||
|
|
||
| BddTest().then('it should not render any skip links', () => { | ||
| expect(wrapper.findAll('a')).toHaveLength(0) | ||
| }) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| export interface SkipLink { | ||
| /** | ||
| * Label of the skip link | ||
| */ | ||
| label: string | ||
|
|
||
| /** | ||
| * Id of the target element to skip to | ||
| */ | ||
| targetId: string | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| <script setup lang="ts"> | ||
| import type { SkipLink } from '@/components/navigation/AvSkipLinks/AvSkipLinks.types' | ||
|
|
||
| export interface AvSkipLinksProps { | ||
| /** | ||
| * Skip links configuration for accessibility. | ||
| * @default [] | ||
| */ | ||
| skipLinks?: SkipLink[] | ||
| } | ||
|
|
||
| const { skipLinks = [] } = defineProps<AvSkipLinksProps>() | ||
| </script> | ||
|
|
||
| <template> | ||
| <nav | ||
| v-if="skipLinks.length > 0" | ||
| class="av-col av-skip-links" | ||
| data-testid="skip-links" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ariaLabel et role manquant je pense
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @elrhourha on s'était dit que c'était à la personne qui instancie le composant de penser à mettre le aria pour ne pas avoir cette prop superflue. Ce n'est plus d'actualité ? Concernant le role, on est déjà sur un <nav, le role navigation est vraiment utile ? C'est pas redondant ? |
||
| > | ||
| <ul class="av-list-reset av-row av-gap-sm av-px-sm av-py-xs"> | ||
| <li | ||
| v-for="link in skipLinks" | ||
| :key="link.targetId" | ||
| > | ||
| <a | ||
| :href="`#${link.targetId}`" | ||
| class="av-button av-button--primary av-button--sm" | ||
| > | ||
| {{ link.label }} | ||
| </a> | ||
| </li> | ||
| </ul> | ||
| </nav> | ||
| </template> | ||
|
|
||
| <style scoped lang="scss"> | ||
| .av-skip-links { | ||
| position: relative; | ||
| z-index: 1000; | ||
| max-height: 0; | ||
| overflow: hidden; | ||
| transition: max-height 0.2s; | ||
| } | ||
|
|
||
| .av-skip-links:focus-within { | ||
| max-height: var(--dimension-5xl); | ||
| } | ||
|
|
||
| @media (prefers-reduced-motion: reduce) { | ||
| .av-skip-links { transition: none; } | ||
| } | ||
| </style> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
C'est si important que ça ? 😅