/**
* Directive Vue pour corriger les attributs d'accessibilité des SVG dans les icônes
*
* ## Pourquoi ce fichier a été créé
*
* Les icônes SVG nécessitent des attributs d'accessibilité spécifiques pour être correctement
* interprétées par les technologies d'assistance (lecteurs d'écran, etc.).
*
* Cette directive applique automatiquement les bonnes pratiques d'accessibilité aux icônes SVG
* en fonction de leur type (décorative, fonctionnelle ou interactive).
*
* ## Comment utiliser ce fichier
*
* ### Utilisation simple (booléen)
* ```vue
*
*
*
*
* ```
*
* Cette directive respecte les normes RGAA (Référentiel Général d'Amélioration de l'Accessibilité).
*
* ## Comment fonctionne cette directive
*
* 1. La directive prend un paramètre qui peut être :
* - Un booléen indiquant si l'icône est décorative (true) ou fonctionnelle (false)
* - Un objet de configuration avec des options avancées (isDecorative, role, autoDetectButton, onlyInner)
*
* 2. Elle recherche tous les éléments SVG à l'intérieur de l'élément auquel elle est appliquée
*
* 3. Pour chaque SVG trouvé, elle corrige les attributs :
*
* - Pour tous les éléments SVG, indépendamment du type d'icône :
* - Supprime systématiquement l'attribut `role="img"` du SVG
* - Ajoute systématiquement `aria-hidden="true"` au SVG
*
* - Pour le conteneur parent, selon le type d'icône et la configuration :
* - Icônes décoratives :
* - Ajoute `role="presentation"` et `aria-hidden="true"` au conteneur parent
*
* - Icônes fonctionnelles (non décoratives) :
* - Détermine le rôle approprié (img, button) selon la configuration ou la détection automatique
* - Ajoute le rôle approprié au conteneur parent
* - Ajoute un aria-label par défaut si aucun n'est présent
* - Supprime `aria-hidden="true"` du conteneur parent
*
* - Cas particulier `onlyInner: true` :
* - Rend uniquement les SVG internes décoratifs (`role` supprimé, `aria-hidden="true"`)
* - Ne modifie AUCUN attribut du conteneur attaché à la directive
* (utile lorsque le conteneur porte déjà son propre rôle/label)
*
* 4. La directive s'applique au montage du composant et à chaque mise à jour
*
* ## Utilisation
*
* ```vue
*
* mdi-account
*
*
* mdi-account
*
*
*
* mdi-delete
*
*
*
*
* mdi-close
*
*
*
*
* ```
*
* ## Recommandation
*
* Il est recommandé d'utiliser le composant `SyIcon` qui intègre déjà cette directive,
* plutôt que d'appliquer manuellement la directive sur des `v-icon`.
*/
import type { Directive, DirectiveBinding } from 'vue'
/**
* Type pour la configuration de la directive rgaaSvgFix
*/
interface RgaaSvgFixConfig {
isDecorative?: boolean
role?: 'presentation' | 'img' | 'button'
autoDetectButton?: boolean
/**
* Si `true`, rend uniquement les SVG internes décoratifs
* (`role` supprimé, `aria-hidden="true"`) sans modifier les attributs
* du conteneur attaché à la directive.
*/
onlyInner?: boolean
}
/**
* Extrait et normalise la configuration à partir de la valeur de binding
*/
function parseConfig(binding: DirectiveBinding): RgaaSvgFixConfig {
let config: RgaaSvgFixConfig = {
isDecorative: true,
autoDetectButton: false,
}
if (typeof binding.value === 'boolean') {
config.isDecorative = binding.value
}
else if (typeof binding.value === 'object' && binding.value !== null) {
config = { ...config, ...binding.value }
// Fournir un rôle fonctionnel explicite implique une icône non décorative,
// sauf si isDecorative est explicitement précisé par l'appelant.
if (
config.role
&& config.role !== 'presentation'
&& binding.value.isDecorative === undefined
) {
config.isDecorative = false
}
}
return config
}
/**
* Balises HTML nativement interactives (recevant le focus / déclenchant une action)
*/
const INTERACTIVE_TAGS = new Set([
'button',
'a',
'input',
'select',
'textarea',
'summary',
])
/**
* Rôles ARIA correspondant à un widget interactif
*/
const INTERACTIVE_ROLES = new Set([
'button',
'link',
'menuitem',
'menuitemcheckbox',
'menuitemradio',
'tab',
'checkbox',
'radio',
'switch',
'option',
'slider',
'spinbutton',
])
/**
* Sélecteur CSS regroupant les ancêtres considérés comme interactifs
*/
const INTERACTIVE_ANCESTOR_SELECTOR = 'button, a[href], [role="button"], [role="link"], [role="menuitem"], [role="tab"], summary'
/**
* Détecte si un élément (ou son contexte) est interactif.
*
* Note : Vue compile les liaisons `@click` / `v-on:*` en écouteurs JavaScript
* ajoutés via `addEventListener`. Il n'existe aucune API standard pour les
* inspecter à l'exécution : ces liaisons ne sont donc pas détectables ici.
* La détection se base sur tout ce qui est réellement observable dans le DOM :
* balise native, rôle ARIA, focusabilité, édition, gestionnaires inline et
* appartenance à un ancêtre interactif.
*/
function detectInteractivity(element: HTMLElement): boolean {
const tag = element.tagName.toLowerCase()
// Balise nativement interactive
if (INTERACTIVE_TAGS.has(tag)) {
// Un lien n'est interactif que s'il possède une cible (href)
if (tag === 'a') {
return element.hasAttribute('href')
}
return true
}
// Rôle ARIA interactif déjà présent
const role = element.getAttribute('role')
if (role !== null && INTERACTIVE_ROLES.has(role)) {
return true
}
// Focusable explicitement
if (element.hasAttribute('tabindex')) {
return true
}
// Contenu éditable
if (element.isContentEditable) {
return true
}
// Gestionnaires d'événements inline (rares avec Vue mais possibles)
if (
element.onclick !== null
|| element.onkeydown !== null
|| element.onkeyup !== null
|| element.onkeypress !== null
) {
return true
}
// Élément imbriqué dans un ancêtre interactif (ex :