a11y —— Bâtisseurs d'attributs aria
L’écriture et la valorisation des attributs aria sont une corvée : aria-haspopup ou ariaHaspopup ? 'true' ou true ? Faut-il rendre la valeur fausse ? Chaque composant qui les assemble lui-même finit par se tromper, ou par oublier d’omettre la propriété lorsque la valeur est vide (le lecteur d’écran se retrouve devant une flopée de aria-controls="undefined"). shared/a11y fige ces conventions dans un ensemble de fonctions pures.
Convention centrale : omission des valeurs fausses
Section intitulée « Convention centrale : omission des valeurs fausses »Tous les bâtisseurs aria booléens respectent la même règle :
| Attribut | Comportement à faux | Raison |
|---|---|---|
aria-required: false |
renvoie {}, non rendu |
une déclaration explicite de « non obligatoire » est du bruit pour le lecteur d’écran |
aria-haspopup: false |
renvoie {}, non rendu |
idem |
aria-current: false |
ne se présente jamais | le type du bâtisseur n’accepte pas false (la plupart des lecteurs d’écran l’ignorent) |
aria-controls / aria-labelledby / aria-describedby |
id vide → renvoie {} |
rendre aria-controls="undefined" est un bug classique |
aria-expanded: false |
renvoyé tel quel | l’état replié doit être annoncé explicitement |
aria-invalid: false |
renvoyé tel quel | « validation réussie » doit être annoncé explicitement |
Les deux « exceptions » sont délibérées : les valeurs fausses de aria-expanded / aria-invalid portent de l’information sémantique, et le lecteur d’écran dira en conséquence « replié » ou « saisie valide ».
import { generateAriaId, ariaExpanded, ariaControls, ariaLabelledby, ariaDescribedby, ariaInvalid, ariaRequired, ariaCurrent, ariaHaspopup, getPopupTriggerAria, getDecorativeAria, type LhAriaAttrs, type LhAriaHaspopup,} from '@lh-ui/share/core/shared/a11y'// Forme du paquet d'attributs aria : les clés sont 'aria-*' ou 'role', à utiliser directement en v-bind / {...spread}export type LhAriaAttrs = Record<string, string | number | boolean | undefined>
// Valeurs légales d'aria-haspopup (APG)export type LhAriaHaspopup = | 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'combobox'Tableau des fonctions
Section intitulée « Tableau des fonctions »| Fonction | Signature | Rôle |
|---|---|---|
generateAriaId |
(prefix = 'lh-aria') => string |
génère un id unique pour les liaisons aria |
ariaExpanded |
(expanded: boolean) => LhAriaAttrs |
état déployé / replié |
ariaControls |
(id: string | undefined) => LhAriaAttrs |
référence au panneau contrôlé |
ariaLabelledby |
(id: string | undefined) => LhAriaAttrs |
référence à un texte visible comme nom accessible |
ariaDescribedby |
(id: string | undefined) => LhAriaAttrs |
référence à une description complémentaire |
ariaInvalid |
(invalid: boolean) => LhAriaAttrs |
marqueur d’échec de validation |
ariaRequired |
(required: boolean) => LhAriaAttrs |
sémantique de champ obligatoire |
ariaCurrent |
(current: 'page' | 'step' | 'location' | 'date' | 'time' | true | undefined) => LhAriaAttrs |
marqueur de position courante |
ariaHaspopup |
(kind: LhAriaHaspopup | boolean) => LhAriaAttrs |
déclare quelle couche flottante va surgir |
getPopupTriggerAria |
(options: { expanded; popupId?; haspopup? }) => LhAriaAttrs |
paquet d’attributs commun au déclencheur de couche flottante |
getDecorativeAria |
(decorative = true) => LhAriaAttrs |
masque un élément décoratif aux technologies d’assistance |
Les deux paquets combinés
Section intitulée « Les deux paquets combinés »getPopupTriggerAria
Section intitulée « getPopupTriggerAria »Les déclencheurs de popover / popconfirm / select / cascader / date-picker / dropdown l’utilisent tous, et obtiennent trois choses d’un seul coup :
getPopupTriggerAria({ expanded: true, popupId: 'lh-popover-1' })// {// 'aria-expanded': true,// 'aria-controls': 'lh-popover-1',// 'aria-haspopup': 'true', // valeur par défaut// }
getPopupTriggerAria({ expanded: false, popupId: 'lh-popover-1', haspopup: 'menu' })// { 'aria-expanded': false, 'aria-controls': 'lh-popover-1', 'aria-haspopup': 'menu' }
getPopupTriggerAria({ expanded: false }) // popupId non transmis// { 'aria-expanded': false, 'aria-haspopup': 'true' } —— pas d'aria-controls produithaspopup vaut 'true' par défaut. Une valeur sémantiquement plus précise (listbox pour un sélecteur, menu pour un sélecteur en cascade, dialog pour un calendrier) rendra l’annonce du lecteur d’écran plus exacte ; chaque composant la transmet selon sa propre sémantique.
getDecorativeAria
Section intitulée « getDecorativeAria »getDecorativeAria() // { 'aria-hidden': true }getDecorativeAria(false) // {} —— démasquage expliciteLes éléments décoratifs (un simple trait de séparation, la décoration d’un loading, les formes géométriques d’un squelette de chargement) sont du bruit pour les technologies d’assistance. Attention, à réserver aux éléments véritablement dépourvus d’information — appliquer aria-hidden à un élément contenant du texte est une violation d’accessibilité. Transmettre false renvoie un objet vide, pour les branches conditionnelles « masqué par défaut, visible dans certains cas ».
generateAriaId
Section intitulée « generateAriaId »const id = generateAriaId('lh-popover') // 'lh-popover-1', 'lh-popover-2' ……Le préfixe est fixé à la valeur transmise, et un compteur interne au module s’incrémente pour garantir l’absence de collisions entre plusieurs instances dans un même processus. Sans risque en SSR (aucun accès au DOM).
Note : le compteur est un état mutable de niveau module, cette fonction n’est donc pas pure — une même entrée renvoie des sorties différentes à des instants différents du processus. Lors du rendu côté serveur, le compteur part de 0 ; lors de l’hydratation côté client, il part également de 0 : tant que l’ordre de rendu est identique, les id le sont aussi ; mais si l’ordre de rendu SSR / CSR diffère (composants asynchrones, différences de rendu conditionnel), des inadéquations d’id peuvent apparaître. Les composants à couche flottante des différentes bibliothèques UI génèrent les id côté client uniquement (après onMounted), ce qui évite le problème.
Exemples d’utilisation
Section intitulée « Exemples d’utilisation »<template> <button v-bind="getPopupTriggerAria({ expanded: open, popupId: panelId, haspopup: 'dialog' })" @click="toggle" > Ouvrir </button> <div :id="panelId" role="dialog" v-show="open">……</div></template>
<script setup lang="ts">const panelId = generateAriaId('lh-popover')</script>Faire un v-bind sur une fonction renvoyant un objet est une écriture idiomatique de Vue — les valeurs undefined du paquet d’attributs sont automatiquement ignorées par Vue, ce qui correspond naturellement à la convention d’omission des valeurs fausses.
const panelId = generateAriaId('lh-popover')
return ( <> <button {...getPopupTriggerAria({ expanded: open, popupId: panelId })} onClick={toggle} /> {open && <div id={panelId} role="dialog">……</div>} </>)La validation de formulaires
Section intitulée « La validation de formulaires »// composant inputconst aria = { ...ariaRequired(props.required), // champ non obligatoire : l'attribut n'est pas du tout rendu ...ariaInvalid(!!errorMessage.value), // validation réussie également annoncée explicitement ...ariaDescribedby(errorMessage.value ? errorId : undefined),}La position courante de la pagination / du fil d’Ariane
Section intitulée « La position courante de la pagination / du fil d’Ariane »ariaCurrent(isActive ? 'page' : undefined) // pas d'attribut émis pour la page non courante, pour éviter le bruitariaCurrent(isActive ? 'page' : undefined) // 'page' pour la pagination, 'step' pour la barre d'étapes, 'date' pour les datesPoints d’attention
Section intitulée « Points d’attention »- L’id référencé par
aria-controlsdoit vraiment exister : ce module ne fait qu’assembler les attributs, la validité de l’id relève de la responsabilité du composant. Lorsqu’une couche flottante est rendue en différé parv-if, le panneau n’est pas dans le DOM à l’état fermé —aria-controlspointe alors vers un élément inexistant : c’est acceptable (le lecteur d’écran l’ignorera), et préférable à un pointage vers un mauvais élément. - Ne pas mettre
aria-hiddensur un élément contenant du texte : c’est une violation absolue d’accessibilité. - Choisir la valeur d’
ariaCurrentselon la sémantique :'page'pour la pagination,'step'pour la barre d’étapes,'location'pour le fil d’Ariane,'date'pour le calendrier. Le type accepte ces littéraux +true. - Les paquets combinés ne se mélangent pas avec les fonctions atomiques :
getPopupTriggerAriacontient déjàaria-haspopup; étendreariaHaspopup(...)ensuite serait écrasé par la propriété étalée en dernier — choisissez l’une des deux selon le besoin.
@lh-ui/share/core/a11y → getPopupTriggerAria( )(填好参数后点运行)Copyright © 2024-2026 FengMo WenXue