Fermer

octobre 4, 2021

Comment créer une galerie accessible et extensible —13 minutes de lecture



Résumé rapide ↬

Dans cet article, nous verrons comment créer une galerie extensible et accessible.

L'un des cas d'utilisation de CSS Grid consiste à afficher une galerie d'images, mais une galerie en elle-même n'est peut-être pas si excitante. On pourrait, par exemple, ajouter un effet de clic pour agrandir l'image sans affecter la grille pour la rendre un peu plus amusante. Et bien sûr, pour n'exclure personne de profiter de cette fonctionnalité, nous devrions également la rendre accessible.

Dans cet article, je vais expliquer comment créer une galerie extensible accessible avec quelques trucs et astuces en cours de route. Voici à quoi ressemble le résultat final :

Voir le stylo [How to build accessible expandable gallery](https://codepen.io/smashingmag/pen/PojxvJr) par Silvestar Bistrović.

Voir le stylo Comment créer une galerie extensible accessible par Silvestar Bistrović.

Le HTML

Tout d'abord, nous allons définir la structure HTML. Bien sûr, nous pourrions toujours le faire de différentes manières, mais utilisons une liste d'images enveloppées dans des boutons.

  • ...

Maintenant, pour rendre la galerie accessible, nous devons faire quelques ajustements :

  • Ajouter l'attribut descriptif alt à chaque image pour aider les malvoyants à comprendre ce qu'il y a dans l'image ;
  • Utilisez l'attribut aria-expanded qui informe les technologies d'assistance si l'image est agrandie ou non ;
  • Inclure role="list" pour vous assurer que les technologies d'assistance annoncent la liste car certains écrans les lecteurs peuvent supprimer l'annonce de la liste.

« Ce n'est pas seulement en utilisant list-style : nonemais tout CSS qui supprimerait les indicateurs de puce ou de numéro des éléments d'une liste supprimera également la sémantique. »

" Fixing” ListsScott O'Hara

Enfin, ajoutons un paragraphe avec un texte utile sur la façon d'utiliser la galerie, et enveloppons le code entier dans un repère (dans ce cas, le main).

Utilisez ESC pour fermer une image plus grande.

  • ...

Pour la simplicité de la démo, j'ai décidé d'utiliser des images encapsulées avec l'attribut aria-expanded. Une meilleure solution pourrait être d'ajouter uniquement des balises d'image, puis d'utiliser JavaScript pour envelopper ces images dans un bouton avec l'attribut aria-expanded. Cela peut être considéré comme une amélioration progressive puisque l'effet d'expansion ne fonctionnerait de toute façon pas sans JavaScript.

Le CSS

Pour définir la disposition de la grille, nous pourrions utiliser CSS Grid. Nous utiliserons auto-fit pour que les éléments puissent tenir dans l'espace disponible, mais se limitent à rétrécir sous une certaine largeur. Cela signifie que nous verrons un nombre différent d'éléments sur différentes fenêtres sans écrire trop de requêtes multimédias.

:root {
  --écart : 4 pixels ;
}

ul {
  affichage : grille ;
  grid-template-columns: repeat(1, 1fr);
  grille-gap: var(--gap);
}

@media screen et (min-width : 640px) {
  ul {
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  }
}

Pour préserver le bon rapport hauteur/largeur de l'image, nous pourrions utiliser la propriété aspect-ratio. Pour réinitialiser le style du bouton, nous pourrions ajouter la déclaration all: initial. Nous devons également masquer le débordement du bouton.

Pour que l'image tienne bien dans le bouton, nous utiliserons la déclaration object-fit: cover et définirons à la fois width et hauteur à 100% :

bouton {
  tout : initiale ;
  bloc de visualisation;
  largeur : 100 % ;
  rapport d'aspect : 2/1 ;
  débordement caché;
  curseur : pointeur ;
}

img {
  hauteur : 100 % ;
  largeur : 100 % ;
  ajustement à l'objet : couverture ;
}

L'effet d'expansion se fait avec la transformation scale. La transition est activée par défaut, mais si l'utilisateur ne préfère pas les transitions et les animations, nous pouvons utiliser la requête média prefers-reduced-motion et définir la propriété transition-duration sur 0s.

:root {
  --duration-shrink : 0,5 s ;
  --duration-expand : .25 s ;
  --no-duration : 0s ;
}

li {
  propriété de transition : transformation, opacité ;
  fonction de synchronisation de transition : entrée-sortie facilitée ;
  transition-duration : var(--duration-expand);
}

li.is-zoomé {
  transition-duration: var(--duration-shrink);
}

@media (préfère-mouvement réduit) {
  li,
  li.is-zoomé {
    transition-duration: var(--no-duration);
  }
}
Plus après le saut ! Continuez à lire ci-dessous ↓

The JavaScript

Preparation

Avant de rendre l'élément extensible, nous devons préparer et calculer quelques éléments.

Tout d'abord, nous devons obtenir le durée de la transition en lisant la CSS Custom Property --duration-on.

let timeout = 0

// Récupère le timeout de la transition depuis CSS
const getTimeouts = () => {
  const durationOn = parseFloat(getComputedStyle(document.documentElement)
    .getPropertyValue('--duration-on'));
  
  timeout = parseFloat(durationOn) * 1000
}

Ensuite, nous allons définir les attributs de données pour le calcul ultérieur :

  • l'écart des éléments de la grille ;
  • la largeur d'un seul élément ;
  • le nombre de éléments par ligne.

Les deux premiers sont assez simples. Nous pourrions obtenir les valeurs à partir du style CSS calculé.

Pour trouver le nombre de colonnes, nous devons parcourir chaque mosaïque et comparer la position supérieure de chaque élément. Une fois que la position supérieure change, l'élément est dans la nouvelle ligne, ce qui nous donne le nombre d'éléments.

 // Définir les attributs de données pour les calculs.
const setDataAttrs = ($elems, $parent) => {
  // Récupère le décalage supérieur du premier élément
  let top = getTop($elems[0])

  // Définir l'espacement de la grille à partir de CSS
  const gridColumnGap = parseFloat(getComputedStyle(document.documentElement)
    .getPropertyValue('--gap'))
  $parent.setAttribute('data-gap', gridColumnGap)

  // Définir la largeur de l'élément de grille à partir de CSS
  const eStyle = getComputedStyle($elems[0])
  $parent.setAttribute('data-width', eStyle.width)

  // Itérer sur les éléments de la grille
  for (let i = 0; i < $elems.length; i++) {
    const t = getTop($elems[i])

    // Vérifie quand le décalage supérieur change
    si (t != haut) {
      // Définit le nombre de colonnes et interrompt l'arrêt de la boucle
      $parent.setAttribute('data-cols', i)
      Pause;
    }
  }
}

Direction d'expansion

Pour obtenir l'effet extensible, nous devons d'abord effectuer quelques vérifications et calculs. Tout d'abord, nous devons vérifier si l'élément est dans la dernière ligne et à la fin de la ligne. Si l'élément se trouve dans la dernière ligne, il doit s'étendre vers le haut. Cela signifie que la propriété transform-origin doit être définie sur la valeur bottom.

Important : Si l'élément doit se développer dans une direction , sa propriété transform-origin doit être définie sur une valeur « opposée ». Notez que les valeurs verticales et horizontales doivent être combinées.

// Définir l'élément actif
const activateElem = ($elems, $parent, $elem, $button, lengthOfElems, i) => {
  // Récupère les attributs de données du parent
  const cols = parseInt($parent.getAttribute('data-cols'))
  const largeur = parseFloat($parent.getAttribute('data-width'))
  const gap = parseFloat($parent.getAttribute('data-gap'))

  // Calculer le nombre de lignes
  const rows = Math.ceil(lengthOfElems / cols) - 1

  // Calcule si l'élément est dans la dernière ligne
  const isLastRow = i + 1 > lignes * colonnes
  // Définir la direction de transformation par défaut vers le haut (développer vers le bas)
  let transformOrigin = 'top'

  if (estLastRow) {
    // Si l'élément est dans la dernière ligne, définissez la direction de transformation vers le bas (développez vers le haut)
    transformOrigin = 'bas'
  }

  // Calcule si l'élément est le plus à droite
  const isRight = (i + 1) % cols !== 0

  si (est à droite) {
    // Si l'élément est le plus à droite, définissez la direction de transformation à gauche (développez à droite)
    transformOrigin += 'gauche'
  } autre {
    // Si l'élément est le plus à droite, définissez la direction de transformation à droite (développez à gauche)
    transformOrigin += 'droit'
  }

  $elem.style.transformOrigin = transformOrigin
}

Effet d'expansion

Pour agrandir l'image sans affecter la grille, nous pourrions utiliser des transformations CSS. En particulier, nous devrions utiliser la transformation d'échelle. J'ai décidé de doubler la taille de l'image, c'est-à-dire que le facteur est le rapport entre la double largeur de l'élément et l'espacement de la grille.

 // Calculer le coefficient d'échelle
échelle const = (largeur * 2 + espace) / largeur

// Définir la transformation CSS de l'élément
$elem.style.transform = `scale(${scale})`

Prise en charge du clavier

Les utilisateurs qui naviguent sur les sites à l'aide d'un clavier devraient pouvoir utiliser la galerie. Le parcours de la liste fonctionne par défaut lors de l'utilisation de la touche Tab. L'émulation du clic fonctionne par défaut en appuyant sur la touche Entrée pendant que l'élément est mis au point. Pour améliorer le comportement par défaut, nous devons ajouter la prise en charge de Esc et des touches fléchées.

Une fois que nous avons développé l'élément, appuyer sur Esc devrait le ramener à sa taille standard. Nous pourrions le faire en vérifiant le code de la touche enfoncée. Il en va de même pour les touches fléchées, mais l'action est différente. Lorsque vous appuyez sur les touches fléchées, nous voulons obtenir le frère précédent ou suivant, puis émuler le clic sur cet élément.

 // Définir le frère comme élément actif
const activateSibling = ($frère) => {
  // Trouver l'ancre
  const $siblingButton = $sibling.querySelector('bouton')

  // Unset élément actif global
  $activeElem = false

  // Concentrez-vous et cliquez sur le courant
  $siblingButton.focus()
  $siblingButton.click()
}

// Définir les événements du clavier
const setKeyboardEvents = () => {
  document.addEventListener('keydown', (e) => {
    // N'agit que si l'élément actif global existe
    si ($activeElem) {
      // Si la clé est "escape", émule le clic sur l'élément actif global
      if (e.code === 'Escape') {
        $activeElem.click()
      }

      // Si la clé est "flèche gauche", active le frère précédent
      if (e.code === 'FlècheGauche') {
        const $previousSibling = $activeElem.parentNode.previousElementSibling

        if($frèreprécédent) {
          activateSibling($previousSibling)
        }
      }

      // Si la clé est "flèche droite", active le prochain frère
      if (e.code === 'FlècheRight') {
        const $nextSibling = $activeElem.parentNode.nextElementSibling

        if($nextSibling) {
          activateSibling($nextSibling)
        }
      }
    }
  })
}

Toggle

Pour étendre l'élément de la galerie, nous devons d'abord désactiver tous les autres éléments. Ensuite, si nous cliquons sur l'élément développé, il devrait revenir à la taille standard.

let $activeElem = false

// Désactiver les éléments de la grille
const deactiveElems = ($elems, $parent, $currentElem, $button) => {
  // Classe parent non définie
  $parent.classList.remove('est-zoomé')

  pour (soit i = 0; i < $elems.length; i++) {
    // Unset item class
    $elems[i].classList.remove('is-zoomed')
    // Unset item CSS transform
    $elems[i].style.transform = 'none'

    // Skip the rest if the item is the current item
    if ($elems[i] === $currentElem) {
      continue
    }
      
    // Unset item aria expanded if element exists
    if($button) {
      $button.setAttribute('aria-expanded', false)
    }
  }
}

// Set active item
const activateElem = ($elems, $parent, $elem, $button, lengthOfElems, i) => {
  ...
  
  // Réinitialiser tous les éléments
  deactiveElems($elems, $parent, $elem, $button)

  si ($activeElem) {
    $activeElem = false
    revenir
  }

  $actifElem = $bouton
  
  ...
}


// Définir les événements de clic sur les ancres
const setClicks = ($elems, $parent) => {
  $elems.forEach(($elem, i) => {
    // Trouver l'ancre
    const $bouton = $elem.querySelector('bouton')

    $button.addEventListener('click', (e) => {
      // Définir l'élément actif au clic
      activateElem($elems, $parent, $elem, $button, $elems.length, i)
    })
  })
}

Problèmes Z-index

Pour éviter les problèmes avec z-index et le contexte d'empilement, nous devons utiliser le délai d'attente pour retarder la transformation. C'est le même timeout que nous avons calculé dans la phase de préparation.

// Désactiver les éléments de la grille
const deactiveElems = ($elems, $parent, $currentElem, $button) => {
  pour (soit i = 0; i < $elems.length; i++) {
    ...

    // After a half of the timeout, reset CSS z-index to avoid overlay issues
    setTimeout(() => {
      $elems[i].style.zIndex = 0
    }, temps libre)
  }
}

// Définir l'élément actif
const activateElem = ($elems, $parent, $elem, $button, lengthOfElems, i) => {
  ...
  setTimeout(() => {
    // Définir la classe parente
    $parent.classList.add('est-zoomé')
    // Définir la classe d'élément
    $elem.classList.add('est-zoomé')
    // Définir la transformation CSS de l'élément
    $elem.style.transform = `scale(${scale})`
    // Définir l'air de l'élément développé
    $button.setAttribute('aria-expanded', true)
    // Définir l'élément actif global
    $actifElem = $bouton
  }, temps libre)
}

Redimensionnement de la fenêtre

Si la fenêtre change de taille, nous devons recalculer les valeurs par défaut car nous avons défini une grille fluide qui permet aux éléments de remplir l'espace disponible et de se déplacer de ligne en ligne.

// Définir des événements de redimensionnement
const setResizeEvents = ($elems, $parent) => {
  window.addEventListener('redimensionner', () => {
    // Définir les attributs de données pour les calculs
    setDataAttrs($elems, $parent)
    // Désactiver les éléments de la grille
    deactiveElems($elems, $parent)
  })
}
Un aperçu de ce que la démo de galerie accessible extensible peut faire. (Essayez CodePen →)

Un mot sur l'accessibilité et les crédits

Je n'ai eu aucun problème à créer cette démo, sauf avec la partie accessibilité. Je ne savais pas trop quoi faire et quels attributs aria utiliser au début. Même après avoir déterminé les attributs à utiliser, je ne pouvais pas être sûr à 100% que c'était juste. La première étape consistait donc à tout tester avec un clavier . C'était la partie facile. Ensuite, j'ai utilisé l'application VoiceOver (puisque j'utilise un Mac) pour tester son fonctionnement pour les personnes malvoyantes. Cela me semblait assez bon.

Cependant, même après tous ces tests, je n'étais toujours pas sûr à 100%. J'ai donc décidé de demander de l'aide. Je fais partie d'une communauté Slack pour les concepteurs et les développeurs (BoagWorld), et j'y ai posté une question. Heureusement, des experts en accessibilité comme Todd Libby m'ont aidé à tester la démo sur différents appareils et à corriger le code. J'ai également demandé de l'aide à Manuel Matuzović et il m'a aidé à nettoyer le code.

Je suis reconnaissant d'avoir les communautés Internet et de développeurs où nous pouvons tous demander de l'aide, obtenir des réponses de professionnels et résolvez les problèmes ensemble. Cela est particulièrement vrai pour les questions sensibles comme l'accessibilité. L'accessibilité est difficile, et il ne faut pas grand-chose pour que ce soit faux. Less is more — du moins c'était dans mon cas.

Et enfin, je voulais partager la plus grande leçon :

« Si vous pouvez utiliser un élément HTML natif [HTML51] ou un attribut avec la sémantique et le comportement dont vous avez besoin déjà intégréau lieu de réutiliser un élément et d'ajouter un rôle, un état ou une propriété ARIA pour le rendre accessible, puis le faire."

First Rule of ARIA UseW3C Working Draft 27 (Sept. 2018)

Autres lectures sur Smashing Magazine

Smashing Editorial(vf, yk, il)




Source link

0 Partages