Publié par Fabrice le 03 juin 2019, temps de lecture estimé : 24 minutes Webmastering
Travaux d'accessibilité et dispositifs ARIA sur l'un des composants JavaScript les plus utilisés sur les sites web : le carrousel !
Au-delà de son utilité très discutable (voir l'article de Erik Runyon sur les statistiques des carrousels - il est, d'ailleurs, déconseillé d'aller au-delà de trois slides ou panneaux), la création d'un carrousel n'en demeure pas moins un excellent exercice de mise en accessibilité : gestion de contenu en mouvement, mise à jour via JavaScript, boutons de contrôle, etc. Il fait, en outre, partie des composants dont la demande est récurrente et insistante !
Le principe de fonctionnement d'un carrousel est connu de tous, inutile de s'attarder dessus. Par contre, la question de l'accessibilité et, en particulier, l'implémentation de dispositifs ARIA, n'est pas aussi simple qu'il n'y paraît.
L'accessibilité d'un carrousel
Les contenus en mouvement doivent être contrôlables
Le contrôle des contenus en mouvement fait partie des critères des WCAG, il faut donc implémenter un bouton pour arrêter le carrousel. Ce bouton doit-être, si possible, placé au début du code et être ainsi atteignable très rapidement via la navigation au clavier. Le tout est complété par des raccourcis clavier : dans ce cas la touche espace permet d'arrêter et de reprendre la rotation et les flèches "suivant" et "précédent" permettent de naviguer aux travers des différents panneaux. Les raccourcis permettant d'aller en avant ou en arrière sont doublés par les combinaisons ctrl + flèches "suivant" ou "précédent". En effet, les lecteurs d'écran réservent ces flèches de contrôle pour naviguer à l'intérieur du contenu de la page.
Évidemment, on préférera l'utilisation de la balise button
en lieu et place des liens.
Les zones live ARIA
Celles-ci servent à signaler des changements dans une page web, mais il n'est évidemment pas question de l'indiquer lorsque le carrousel est en rotation, à moins de vouloir déranger l'utilisateur d'un lecteur d'écran. L'attribut aria-live="polite"
est donc uniquement ajouté lorsque le bouton pause est activé. Ainsi, lors de l'affichage manuel d'un nouveau panneau, le lecteur d'écran lira l'intégralité de celui-ci.
Les autres attributs ARIA
aria-roledescription
sur la section
et sur les div
englobant les panneaux permet d'indiquer le type de contenu (ou plutôt le rôle) auquel on a affaire. aria-label
vient compléter ces informations avec, notamment, le numéro courant du panneau ainsi que le nombre total de panneau.
aria-controls
sur les boutons relie ces derniers au contenu sur lequel il va agir.
aria-selected
sur les boutons de la navigation par points indique la position courante aux lecteurs d'écran (en plus d'une indication visuelle).
Arrêt du carrousel au focus clavier et souris
À la prise de focus (au clavier ou à la souris), il est préférable que la rotation s'arrête et reprenne lors de la perte de celui-ci. Pour éviter la reprise de la rotation du carrousel lorsque qu'il est en pause, l'ajout ou le retrait de la classe btnpressed
s'effectue lors de l'activation du bouton "Stop/Play", permettant ainsi de connaître le statut du composant.
En supplément
La navigation par points est ajoutée via JavaScript. Cela semble anodin, mais elle permet de visualiser la position courante et le nombre de panneaux. Cette dernière information est, par ailleurs, indiquée aux lecteurs d'écran via l'attribut aria-label
de chaque panneau.
Les boutons de contrôle sont supprimés s'il n'y a qu'un seul panneau.
Le panneau actif s'implémente via la class slideactive
, on peut donc démarrer le carrousel par n'importe quel panneau.
Le JavaScript fait un peu moins de 4 kilo-octets en version minifiée
La navigation mobile par glisser ou swipe n'est pas implémentée, mais le code fournit par Kirupa fonctionne très bien et peut être facilement ajouté après les fonctions de navigation au clavier.
La base HTML du carrousel
<section id="slides" aria-roledescription="Slideshow" aria-label="Description of Carousel">
<div class="slides-control">
<button class="slides-playpause" aria-controls="slides-items" aria-label="Stop Carousel">
<svg class="playpause" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32" role="img">
<g class="pause">
<path d="m7.43 31c2.36 0 4.29-1.93 4.29-4.29v-21.4c0-2.36-1.93-4.29-4.29-4.29-2.36 0-4.29 1.93-4.29 4.29v21.4c0 2.36 1.93 4.29 4.29 4.29zm12.9-25.7v21.4c0 2.36 1.93 4.29 4.29 4.29s4.29-1.93 4.29-4.29v-21.4c0-2.36-1.93-4.29-4.29-4.29s-4.29 1.93-4.29 4.29z"></path>
</g>
<g class="play">
<path d="m6.29 0.988c1.75-0.00742 13.3 8.37 20 12.4 2.61 1.35 2.72 3.64 0.145 5.18-6.19 3.67-18.7 12.4-20.3 12.4-1.59 0.0055-2.31-1.16-2.36-2.73 0.0104-8.27-0.0208-16.5 0.0156-24.8-0.0349-1.19 0.787-2.46 2.53-2.47z"></path>
</g>
</svg>
</button>
<button class="slides-prev" aria-controls="slides-items" aria-label="Previous Slide">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32" role="img">
<path d="m22.1 31c2.03-0.108 3.01-2.54 1.62-4.03l-10.4-10.8 10.4-11c2.34-2.26-1.13-5.73-3.39-3.39l-12 12.6c-0.903 0.904-0.937 2.36-0.0775 3.3l12 12.5c0.483 0.548 1.19 0.846 1.92 0.808z"></path>
</svg>
</button>
<button class="slides-next" aria-controls="slides-items" aria-label="Next Slide">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32" role="img">
<path d="m9.9 31c-2.03-0.108-3.01-2.54-1.62-4.03l10.4-10.8-10.4-11c-2.34-2.26 1.13-5.73 3.39-3.39l12 12.6c0.903 0.904 0.937 2.36 0.0775 3.3l-12 12.5c-0.483 0.548-1.19 0.846-1.92 0.808z"></path>
</svg>
</button>
</div>
<div id="slides-items">
<!-- Slide 1 : image link + caption -->
<div id="slide-1" class="slide-item slideactive" role="group" aria-roledescription="Slide" aria-label="1 of 3">
<figure>
<a href="https://www.adilade.fr/">
<img src="https://dummyimage.com/1200x600/E2CEFA/3f3f3f.png&text=Slide+1" alt="Image 1 alternative" width="1200" height="600">
</a>
<figcaption>
<a href="https://www.adilade.fr/">Slide 1 Description</a>
</figcaption>
</figure>
</div>
<!-- Slide 2 : image + caption -->
<div id="slide-2" class="slide-item" role="group" aria-roledescription="Slide" aria-label="2 of 3">
<figure>
<img src="https://dummyimage.com/1200x600/DAFF89/3f3f3f.png&text=Slide+2" alt="Image 2 alternative" width="1200" height="600">
<figcaption>
<strong>Slide 2 Description.</strong><br><span lang="lat">Venerem elegerit sexus mercenariae tabernaculum matrimonii in conductae post species.</span>
</figcaption>
</figure>
</div>
<!-- Slide 3 : image link -->
<div id="slide-3" class="slide-item" role="group" aria-roledescription="Slide" aria-label="3 of 3">
<figure>
<a href="https://fr.wikipedia.org/wiki/Anthropophobie">
<img src="https://dummyimage.com/1200x600/FFDA7D/3f3f3f.png&text=Slide+3" alt="Image 3 alternative" width="1200" height="600">
</a>
</figure>
</div>
</div> <!-- end slides-items -->
</section>
La mise en forme en SASS
À adapter selon vos besoins, en particulier si vous utilisez un framework CSS !
/* Reset for the demo */
*, ::before, ::after {
-webkit-box-sizing: inherit;
box-sizing: inherit;
min-width: 0;
min-height: 0;
}
body {
margin: 0;
font-size: 1.4rem;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
line-height: 1.5;
}
img, figure {
max-width: 100%;
height: auto;
}
button {
padding: 0;
margin: 0;
border-style: none;
touch-action: manipulation;
display: inline-block;
border: none;
background: none;
cursor: pointer;
}
/* End Reset for the demo */
/* Sass Config */
$slides_width: 1200px;
$slides_height: 600px;
$slides_maxheight: calc(100vw / (1200 / 600));
$slides_bg: rgba(0, 0, 0, 0.7);
$slides_blue: #3A72B1;
/* End Sass Config */
#slides {
position: relative;
padding-bottom: 3rem;
}
#slides, #slides-items .slide-item, figure {
width: $slides_width;
max-width: 100%;
height: $slides_height;
max-height: $slides_maxheight;
margin: 0 auto;
}
#slides-items .slide-item {
position: absolute;
figcaption {
position: absolute;
bottom: 0;
width: $slides_width;
max-width: calc(100% - 6.8rem);
max-height: calc(100% - 3.4rem);
overflow: auto;
padding: 1.7rem 3.4rem;
text-align: center;
color: #fff;
background-color: $slides_bg;
a {
color: #fff;
&:hover, &:focus, &:active {
text-decoration: none;
}
}
}
}
/* CSS Transition */
#slides-items .slideactive {
opacity: 1;
transition: opacity 3s;
visibility: visible;
}
#slides-items .slide-item:not(.slideactive) {
opacity: 0;
transition: opacity 5s;
visibility: hidden;
}
/* Control Buttons */
.slides-control button {
background-color: $slides_bg;
svg, g {
fill: #f6f6f6;
}
&:hover, &:focus, &:active {
background-color: rgba(255, 255, 255, 0.9);
svg {
fill: #474747;
}
}
}
#slides {
.slides-control button {
position: absolute;
z-index: 1;
}
.slides-prev, .slides-next {
top: calc(50% - 3rem);
padding: 0.6rem;
svg {
width: 1.7rem; height: 1.7rem;
}
}
.slides-prev {
left: 0;
border-radius: 0px 7px 7px 0px;
}
.slides-next {
right: 0;
border-radius: 7px 0px 0px 7px;
}
.slides-playpause {
top: 0; right: 0;
width: 0.7rem; height: 0.7rem;
padding: 0.7rem;
background-color: $slides_bg;
svg {
width: 0.7rem;
height: 0.7rem;
}
.play,
.playpause.paused .pause {
display: none;
}
.playpause.paused .play {
display: block;
}
}
}
/* Dots Buttons */
#slides .slides-dots {
position: absolute;
bottom: 0;
z-index: 1;
width: $slides_width;
max-width: 100%;
text-align: center;
}
.slides-dots {
svg {
width: 1.7rem; height: 1.7rem;
fill: $slides_bg;
}
button {
&:hover, &:focus, &:active, &[aria-selected="true"] {
svg {
fill: $slides_blue;
}
}
}
}
Le code JavaScript en Vanilla JS
/*
* @Adilade Slideshow/Carousel
* @See www.adilade.fr
*
* Keyboard :
* Previous : left arrow or ctrl + left arrow
* Next : right arrow or ctrl + right arrow
* Pause/Play : space
*
* Free to use - No warranty
*/
var timeInterval = '7000'; // Time between slides
var carousel = document.querySelector('#slides-items');
var items = document.querySelectorAll('.slide-item');
if (carousel !== undefined && items !== undefined && carousel !== null && items !== null) {
var itemscount = items.length;
var btnprev = document.querySelector('.slides-prev');
var btnnext = document.querySelector('.slides-next');
var btnplaypause = document.querySelector('.slides-playpause');
var btnplaypausepath = document.querySelector('.playpause');
if (itemscount > 1) {
// Create Dots
var dotbox = document.createElement('div');
dotbox.classList.add('slides-dots');
// carousel.after(dotbox); Not supported by Edge => see next line
carousel.parentNode.insertBefore(dotbox, carousel.nextSibling);
for (var i=0; i<itemscount; i++) {
dotbox.insertAdjacentHTML('beforeend', '<button aria-controls="slide-'+(i+1)+'" aria-label="Slide number '+(i+1)+'" aria-selected="'+(document.querySelector('.slideactive').getAttribute('id').slice(-1) == (i+1) ? 'true' : 'false')+'"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32" role="img"><circle cx="16" cy="16" r="13" /></svg></button>');
}
var dots = document.querySelectorAll('.slides-dots button');
var playpause = null;
function slideprev() {
var itemcurrent = document.querySelector('.slideactive');
var dotcurrent = document.querySelector('.slides-dots button[aria-selected="true"]');
var prevslide = itemcurrent.previousElementSibling;
var prevdot = dotcurrent.previousElementSibling;
if(prevslide === null) {
prevslide = items[itemscount - 1];
prevdot = dots[itemscount - 1];
}
// Remove current
itemcurrent.classList.remove('slideactive');
dotcurrent.setAttribute('aria-selected', 'false');
// Add Next
prevslide.classList.add('slideactive');
prevdot.setAttribute('aria-selected', 'true');
}
function slidenext() {
var itemcurrent = document.querySelector('.slideactive');
var dotcurrent = document.querySelector('.slides-dots button[aria-selected="true"]');
var nextslide = itemcurrent.nextElementSibling;
var nextdot = dotcurrent.nextElementSibling;
if(nextslide === null) {
nextslide = items[0];
nextdot = dots[0];
}
// Remove current
itemcurrent.classList.remove('slideactive');
dotcurrent.setAttribute('aria-selected', 'false');
// Add Next
nextslide.classList.add('slideactive');
nextdot.setAttribute('aria-selected', 'true');
}
function slidepause() {
clearInterval(playpause);
playpause = null;
btnplaypause.setAttribute('aria-label', 'Play Carousel');
btnplaypausepath.classList.add('paused');
carousel.setAttribute('aria-live', 'polite');
}
function slideplay() {
playpause = setInterval(slidenext, timeInterval);
btnplaypause.setAttribute('aria-label', 'Stop Carousel');
btnplaypausepath.classList.remove('paused');
carousel.removeAttribute('aria-live');
}
function slideplaypause() {
if (playpause !== null) {
slidepause();
carousel.classList.add('btnpressed');
} else {
slideplay();
carousel.classList.remove('btnpressed');
}
}
// Dots Navigate
[].map.call(dots, function(dot) {
dot.addEventListener('click', function(e) {
var itemcurrent = document.querySelector('.slideactive');
var dotcurrent = document.querySelector('.slides-dots button[aria-selected="true"]');
var dotclick = dot.getAttribute('aria-controls');
var targetslide = document.querySelector('#'+ dotclick +'');
var targetdot = document.querySelector('button[aria-controls="'+dotclick+'"]');
// Remove current
itemcurrent.classList.remove('slideactive');
dotcurrent.setAttribute('aria-selected', 'false');
// Add Target
targetslide.classList.add('slideactive');
targetdot.setAttribute('aria-selected', 'true');
e.preventDefault();
},false);
},false);
// Navigate
btnprev.addEventListener('click', slideprev);
btnnext.addEventListener('click', slidenext);
btnplaypause.addEventListener('click', slideplaypause);
// Keyboard Navigate
carousel.addEventListener('keydown', keyHandler);
function keyHandler(e) {
// Left Arrow
if (e.keyCode === 37 || (e.ctrlKey && e.keyCode === 37)) {
e.preventDefault();
slideprev();
}
// Right Arrow
if (e.keyCode === 39 || (e.ctrlKey && e.keyCode === 39)) {
e.preventDefault();
slidenext();
}
// Space
if (e.keyCode === 32) {
e.preventDefault();
slideplaypause();
}
}
// Animate Slides
playpause = setInterval(slidenext, timeInterval);
// Stop Carousel on keyboard/mouse focus only when Carousel auto-rotating
function slidefocusstop() {
if(!carousel.classList.contains('btnpressed')){
slidepause();
}
}
function slidefocusplay() {
if(!carousel.classList.contains('btnpressed')){
slideplay();
}
}
carousel.addEventListener('focusin', slidefocusstop);
carousel.addEventListener('focusout', slidefocusplay);
carousel.addEventListener('mouseover', slidefocusstop);
carousel.addEventListener('mouseout', slidefocusplay);
} else { // End itemscount > 1 => Remove buttons controls
btnprev.parentNode.removeChild(btnprev);
btnnext.parentNode.removeChild(btnnext);
btnplaypause.parentNode.removeChild(btnplaypause);
}
} // End if test