Manipuler les attributs HTML
Présentation
Après avoir sélectionné un élément du DOM, il devient possible de lire ou de modifier les informations écrites directement dans sa balise HTML. Ces informations s'appellent des attributs.
Modifier un attribut permet souvent de faire évoluer l'interface sans recharger la page. Le fichier HTML ne change pas, mais l'état des balises présentes dans le DOM est mis à jour. Cela influence ce que l'utilisateur voit ou ce que l'utilisateur peut faire.
Cette page présente d'abord les cas d'usage les plus fréquents, puis les méthodes génériques pour lire, modifier et supprimer des attributs. Une page suivante sera consacrée à des outils plus spécialisés, adaptés à certains attributs utilisés très souvent, comme class, style et data-*.
Les attributs HTML
Les attributs sont les informations écrites dans la balise (ex.: id, class, src, disabled ou encore data-*). Ils servent à identifier une balise, décrire son état, pointer vers une ressource, ou fournir des informations utiles au navigateur et aux scripts.
Dans une interface dynamique, certains attributs sont fréquemment modifiés afin d'adapter l'affichage ou le comportement d'un élément. Ces exemples montrent les situations les plus courantes.
- class : Piloter l'apparence via le CSS. JavaScript change un état (actif, erreur, sélectionné), puis le CSS applique une mise en forme prévue à l'avance.
- style : Appliquer une valeur inline calculée. Ce choix reste ponctuel, par exemple pour une largeur en pourcentage (width: 43%) ou une position dynamique.
- src : Remplacer un média à la volée. Par exemple changer l'image principale d'une galerie ou mettre à jour une source vidéo.
- href : Modifier la destination d'un lien. Par exemple mettre à jour un lien de téléchargement selon un format choisi.
- disabled : Verrouiller ou déverrouiller une interaction. Très utilisé pour désactiver un bouton tant qu'une condition n'est pas remplie.
- hidden : Masquer un élément. Tant que l'attribut est présent, l'élément n'est pas rendu à l'écran. Quand l'attribut est retiré, l'élément redevient affichable selon sa mise en forme habituelle.
- aria-* : Informer les technologies d'assistance. Par exemple, passer aria-expanded de "false" à "true" indique qu'un contenu vient d'être déployé.
- data-* : Attacher des données non visibles à une balise. Par exemple stocker l'identifiant d'un produit ou le type d'action associé à un bouton.
Lire un attribut avec getAttribute()
La méthode getAttribute() s'applique sur un élément déjà sélectionné. Elle reçoit en argument le nom de l'attribut dont on souhaite lire la valeur, sous forme de chaîne de caractères.
Dans l'exemple suivant, la valeur de l'attribut HTML type est lue avant d'être affichée dans la console. Cette opération permet de connaître l'état d'un élément avant de décider quoi faire ensuite.
Code HTML :
<input id="mdp" type="password" placeholder="Votre mot de passe">
Code Javascript :
// Stocker la référence vers la balise dont l'attribut "id" vaut "mdp".
const mdpInput = document.querySelector('#mdp');
// Lire la valeur actuelle de l'attribut HTML "type".
const typeActuel = mdpInput.getAttribute('type');
// Vérifier la valeur récupérée.
console.log(typeActuel); // Affiche : "password"
Modifier ou créer un attribut avec setAttribute()
La méthode setAttribute() permet de modifier la valeur d'un attribut HTML ou de le créer s'il n'existe pas encore. Elle reçoit deux arguments le nom de l'attribut à modifier et la nouvelle valeur.
Dans l'exemple suivant, l'attribut HTML type est modifié afin de passer d'un champ mot de passe à un champ texte.
Code HTML :
<input id="mdp" type="password" placeholder="Votre mot de passe">
Code Javascript :
// Stocker la référence vers la balise dont l'attribut "id" vaut "mdp".
const mdpInput = document.querySelector('#mdp');
// Modifier la valeur de l'attribut HTML "type".
// Le champ passe de "password" à "text".
mdpInput.setAttribute('type', 'text');
// Vérifier que l'attribut a bien été modifié.
console.log(mdpInput.getAttribute('type')); // Affiche : "text"
Supprimer un attribut avec removeAttribute()
La méthode removeAttribute() permet de supprimer complètement un attribut HTML d'une balise. Elle reçoit le nom de l'attribut à retirer. Si l'attribut n'existe pas, aucun changement n'est effectué et aucune erreur n'est générée.
Dans l'exemple suivant, l'attribut disabled est retiré d'un bouton afin de le rendre cliquable.
Code HTML :
<button id="btn-payer" disabled>Payer la commande</button>
Code Javascript :
// Stocker la référence vers le bouton de paiement.
const bouton = document.querySelector('#btn-payer');
// Retirer l'attribut HTML "disabled" pour rendre le bouton cliquable.
bouton.removeAttribute('disabled');
// Vérifier que l'attribut a bien disparu.
console.log(bouton.getAttribute('disabled')); // Affiche : null
Exercices
JS Attributs DOM - Série 01 (Exo 01 à 07)
Cette série reprend 7 exercices indépendants, basés sur le même fichier index.html et le même fichier css/style.css. Le travail se fait dans js/app.js et dans les modules js/modules/exo01.js à exo07.js. Les fichiers HTML et CSS ne sont à copier qu'une seule fois au début.
Structure
Créer un dossier nommé JS-attributs-dom-serie-01, puis organiser les fichiers et dossiers en respectant l'arborescence suivante :
JS-attributs-dom-serie-01/
|-- index.html
|-- css/
| └── style.css
└── js/
|-- app.js
└── modules/
|-- exo01.js
|-- exo02.js
|-- exo03.js
|-- exo04.js
|-- exo05.js
|-- exo06.js
└── exo07.js
Fichiers fournis
Copier les fichiers suivants à l'identique. Ils servent pour toute la série.
index.html
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="./css/style.css">
<title>JS Attributs DOM - Série 01 (Exo 01 à 07)</title>
</head>
<body>
<header>
<h1>JS Attributs DOM - Série 01 (Exo 01 à 07)</h1>
</header>
<main>
<!-- Exo 01 -->
<section>
<h2>01. Validation des conditions générales d'utilisation pour activer le bouton de confirmation</h2>
<p>Vous devez accepter les conditions pour continuer.</p>
<form>
<div>
<!--
aria-describedby
Sert à relier un champ à un texte d'aide déjà présent dans la page.
Un lecteur d'écran lira ce texte en plus du label du champ.
Cela ne modifie pas le visuel, cela ajoute une description accessible.
-->
<input type="checkbox" id="checkConditions" aria-describedby="conditionsAide">
<label for="checkConditions">J'accepte les conditions générales</label>
</div>
<p id="conditionsAide" class="box">
Tant que la case n'est pas cochée, le bouton reste désactivé.
</p>
<button id="btnInscription" type="button" disabled>Confirmer l'inscription</button>
</form>
</section>
<!-- Exo 02 -->
<section>
<h2>02. Afficher ou masquer un mot de passe (type)</h2>
<label for="mdp">Votre mot de passe</label>
<input type="password" id="mdp" autocomplete="current-password">
<label>
<!--
aria-controls
Indique quel élément est piloté par cette chekbox.
Cela crée un lien explicite entre le contrôle et la zone impactée.
Les outils d'assistance peuvent utiliser ce lien pour donner du contexte à l'utilisateur.
-->
<input type="checkbox" id="toggleMdp" aria-controls="mdp">
Voir le mot de passe
</label>
<p class="box">Vérifier le changement de l'attribut <b>type</b> dans l'inspecteur.</p>
</section>
<!-- Exo 03 -->
<section>
<h2>03. Affichage (hidden) avec toggle</h2>
<div id="messageSecret" class="box" hidden>
Ceci est un message qui était caché
</div>
<!--
aria-expanded
Indique l'état ouvert ou fermé du contenu piloté.
false signifie replié ou masqué, true signifie déployé ou visible.
Le JavaScript doit mettre à jour cette valeur en même temps que hidden.
-->
<button id="btnToggleSecret" type="button" aria-controls="messageSecret" aria-expanded="false">
Afficher le message
</button>
</section>
<!-- Exo 04 -->
<section id="exo04-section" data-theme="light">
<h2>04. Basculer entre Mode Clair et Mode Sombre</h2>
<!--
aria-checked
Représente l'état d'un composant à deux états quand on utilise le modèle switch.
false signifie désactivé, true signifie activé.
Le JavaScript doit synchroniser le texte du bouton et aria-checked à chaque bascule.
-->
<button id="btnTheme" type="button" role="switch" aria-checked="false" aria-controls="exo04-section">
Activer le mode sombre
</button>
</section>
<!-- Exo 05 -->
<section>
<h2>05. Barre de progression</h2>
<div class="progress-bg">
<!--
aria-valuemin
Définit la valeur minimale possible pour une barre de progression.
Ici, la progression ne peut pas descendre en dessous de 0.
aria-valuemax
Définit la valeur maximale possible pour une barre de progression.
Ici, 100 représente la progression complète.
aria-valuenow
Indique la valeur courante de la progression.
Le JavaScript met à jour ce nombre quand la progression avance.
aria-valuetext
Fournit une version textuelle de la valeur courante.
Utile quand on veut annoncer un format plus parlant qu'un simple nombre.
Exemple 25 % plutôt que 25.
aria-label
Donne un nom accessible à un élément quand il n'a pas de texte visible qui le décrit.
Ce nom est annoncé par les lecteurs d'écran.
Cela ne change pas l'affichage.
-->
<div
id="progressBar"
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="0"
aria-valuetext="0 %"
aria-label="Progression de l'exercice">
</div>
</div>
<p id="progressTexte" class="box" role="status" aria-live="polite">
Progression 0 %
</p>
<button id="btnProgress" type="button">Étape suivante (+25%)</button>
</section>
<!-- Exo 06 -->
<section id="exo06-conteneur">
<h2>06. Modifier l'attribut href d'un lien en fonction de la langue sélectionnée</h2>
<p>
<a id="lienDynamique" href="/contact">Aller à la page de contact</a>
</p>
<label for="langueSelecteur">Choisir la langue</label>
<p id="exo06-aide" class="box">
Le choix de langue met à jour le lien de contact.
</p>
<select id="langueSelecteur" name="langue" aria-describedby="exo06-aide">
<option value="fr" selected>Français</option>
<option value="en">English</option>
<option value="nl">Nederlands</option>
</select>
<!--
aria-live
Transforme une zone en zone vivante.
Quand son texte change via JavaScript, un lecteur d'écran peut l'annoncer automatiquement.
La valeur polite demande une annonce non intrusive, sans couper la lecture en cours.
-->
<p id="exo06-statut" role="status" aria-live="polite" class="box">
Le lien pointe vers la version française.
</p>
<p class="box">Vérifier le changement de l'attribut <b>href</b> dans l'inspecteur.</p>
</section>
<!-- Exo 07 -->
<section>
<h2>07. Visionneuse de produits</h2>
<img
id="mainImage"
src="https://placehold.co/400x300/blue/white?text=Produit+Bleu"
alt="Produit Bleu"
width="200">
<div role="group" aria-label="Choisir une variante de couleur">
<!--
aria-pressed
Indique l'état actif d'un bouton à état.
false signifie non sélectionné, true signifie sélectionné.
Le JavaScript doit mettre aria-pressed à true sur le bouton choisi
et à false sur les autres pour refléter la sélection.
-->
<button
class="thumb"
type="button"
aria-controls="mainImage"
aria-pressed="true"
data-full-img="https://placehold.co/400x300/blue/white?text=Produit+Bleu"
data-alt="Produit Bleu">
Bleu
</button>
<button
class="thumb"
type="button"
aria-controls="mainImage"
aria-pressed="false"
data-full-img="https://placehold.co/400x300/red/white?text=Produit+Rouge"
data-alt="Produit Rouge">
Rouge
</button>
<button
class="thumb"
type="button"
aria-controls="mainImage"
aria-pressed="false"
data-full-img="https://placehold.co/400x300/green/white?text=Produit+Vert"
data-alt="Produit Vert">
Vert
</button>
</div>
<p id="produitStatut" class="box" role="status" aria-live="polite">
Variante sélectionnée Bleu
</p>
</section>
</main>
<script src="./js/app.js" type="module"></script>
</body>
</html>
css/style.css
/* --- VARIABLES --- */
:root
{
/* Couleurs de base */
--fond-principal: #ffffff;
--texte-principal: #333333;
--bordure-douce: #cccccc;
--bordure-forte: #999999;
--fond-gris: #f4f4f4;
/* États (Boutons et interactions) */
--fond-desactive: #e0e0e0;
--texte-desactive: #95a5a6;
/* Espaces et Rayons */
--espace-m: 20px;
--espace-s: 12px;
--arrondi: 8px;
--interligne: 1.5;
}
/* --- RESET & BOX MODEL CIBLÉ --- */
/* Uniquement les balises présentes dans ton HTML */
html, body, section, h1, h2, p, div, ul, li, a, label, select, button, input
{
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* --- BASE STYLES --- */
body
{
font-family: sans-serif;
padding: var(--espace-m);
line-height: var(--interligne);
color: var(--texte-principal);
}
section
{
border: 1px solid var(--bordure-douce);
padding: var(--espace-m);
margin-bottom: var(--espace-m);
border-radius: var(--arrondi);
}
h2
{
margin-top: 0;
margin-bottom: var(--espace-s);
}
/* --- ÉLÉMENTS DE FORMULAIRE --- */
button {
padding: 8px 12px;
cursor: pointer;
margin-right: 6px;
}
/* Style pour l'exercice 02 (CGU) */
button:disabled {
background-color: var(--fond-desactive);
border-color: var(--bordure-douce);
color: var(--texte-desactive);
cursor: not-allowed;
}
input[type="checkbox"] {
cursor: pointer;
margin-right: 5px;
}
/* --- UTILS --- */
.box {
padding: var(--espace-s);
background: var(--fond-gris);
border: 1px dashed var(--bordure-forte);
}
.badge {
display: inline-block;
padding: 2px 8px;
border: 1px solid var(--bordure-forte);
border-radius: 999px;
}
ul {
padding-left: 18px;
}
/* --- EXO 03 : BASCULER AFFICHAGE MESSAGE SECRET (hidden) --- */
[hidden] {
display: none !important;
}
/* --- EXO 05 : DARK MODE (via Data Attributes) --- */
section[data-theme="dark"] {
background-color: #2c3e50;
color: #ecf0f1;
border-color: #34495e;
}
section[data-theme="dark"] .box {
background-color: #34495e;
color: #bdc3c7;
border-color: #7f8c8d;
}
/* --- EXO 06 : BARRE DE PROGRESSION --- */
.progress-bg {
width: 100%;
height: 24px;
background-color: var(--fond-gris);
border: 1px solid var(--bordure-douce);
border-radius: var(--arrondi);
overflow: hidden;
margin-bottom: var(--espace-s);
}
#progressBar {
width: 0%;
height: 100%;
background-color: #3498db;
transition: width 0.4s ease-in-out;
}
Exo-01
Cet exercice porte sur la synchronisation entre une case à cocher et un bouton, via la propriété checked d'une checkbox et la propriété disabled d'un bouton. L'objectif est de rendre le bouton utilisable uniquement lorsque l'utilisateur a accepté les conditions générales.
Accessibilité
Dans cet exercice, le texte d'aide est relié à la case à cocher grâce à aria-describedby. Cela permet à un lecteur d'écran de lire le texte d'aide en plus du libellé, sans modifier l'affichage.
Le bouton doit être désactivé avec la propriété native disabled. Sur un élément button, ajouter en plus un attribut aria-disabled est inutile, car disabled est déjà compris par les navigateurs et les technologies d'assistance. En pratique, garder un seul mécanisme évite les incohérences.
Attendu
Tant que la case "J'accepte les conditions générales" n'est pas cochée, le bouton de confirmation doit rester désactivé. Dès que la case est cochée, le bouton devient cliquable. Si la page est rafraîchie et que le navigateur conserve l'état de la case, le bouton doit être synchronisé dès le chargement.
Instructions
Le travail se fait dans js/modules/exo01.js et dans js/app.js. Dans js/app.js, il faut importer la fonction de l'exercice, sélectionner les éléments HTML nécessaires, puis appeler la fonction exportée en lui passant ces éléments.
Étape 01
- Créer le fichier js/modules/exo01.js. Exporter une fonction nommée execExo01.
-
La fonction execExo01 doit recevoir deux arguments et utiliser exactement ces noms
- checkboxElem pour la case à cocher #checkConditions
- boutonElem pour le bouton #btnInscription
- Si checkboxElem ou boutonElem est absent, arrêter la fonction avec return. Cela évite une erreur qui bloquerait tout le JavaScript de la page.
- Créer une fonction utilitaire nommée rafraichirInterface qui reçoit checkboxElem et boutonElem. Son rôle est de synchroniser l'état du bouton avec l'état de la case.
- Dans rafraichirInterface, lire l'état de la case avec checkboxElem.checked. Cette propriété vaut true quand la case est cochée, et false sinon.
-
Stocker dans une constante nommée estDesactive la valeur qui indique si le bouton doit être désactivé.
L'objectif est d'obtenir l'inverse de la case
- si la case n'est pas cochée, le bouton doit être désactivé
- si la case est cochée, le bouton doit être activé
- Appliquer l'état au bouton via la propriété JavaScript disabled. Affecter estDesactive à boutonElem.disabled. Quand disabled vaut true, le navigateur empêche le clic et retire le focus clavier.
Étape 02
- Dans execExo01, créer une fonction gestionnaire nommée gererCheckboxElemChange. Elle doit appeler rafraichirInterface avec checkboxElem et boutonElem.
- Poser un écouteur change sur checkboxElem en utilisant gererCheckboxElemChange. L'événement change se déclenche quand l'état final de la case a changé.
- Appeler une première fois rafraichirInterface au chargement, afin de synchroniser l'interface immédiatement. Cela permet aussi de gérer le cas où le navigateur conserve l'état de la checkbox après un rafraîchissement.
Étape 03
- Dans js/app.js, importer la fonction execExo01 depuis ./modules/exo01.js.
-
Repérer la section de js/app.js qui correspond à cet exercice, et vérifier les sélections attendues
const checkboxCguElem = document.querySelector('#checkConditions'); const btnInscriptionElem = document.querySelector('#btnInscription'); execExo01(checkboxCguElem, btnInscriptionElem); - Vérifier que l'appel à execExo01 reçoit bien checkboxCguElem et btnInscriptionElem, dans cet ordre.
Test
- Vérifier qu'au chargement, le bouton est désactivé tant que la case n'est pas cochée.
- Cocher la case et vérifier que le bouton devient cliquable.
- Décocher la case et vérifier que le bouton redevient désactivé.
- Rafraîchir la page après avoir coché la case et vérifier que le bouton reste cohérent avec l'état conservé.
Exo-02
Cet exercice porte sur la lecture de l'etat d'une checkbox via la propriete checked, puis sur la modification de l'attribut HTML type d'un champ input. L'objectif est d'alterner entre password et text en utilisant getAttribute() et setAttribute().
Accessibilite
Dans le HTML, la checkbox possede aria-controls="mdp". Cet attribut indique quel element est pilote, ici le champ #mdp. Il s'agit d'un lien de contexte pour les outils d'assistance. Il n'execute aucune action tout seul, c'est le JavaScript qui doit realiser la bascule.
Attendu
Quand la case "Voir le mot de passe" est cochee, le champ doit passer en type="text" pour rendre le contenu lisible. Quand la case est decochee, le champ doit repasser en type="password" pour masquer le contenu. Si la page est rafraichie et que le navigateur conserve l'etat de la case, le type du champ doit etre synchronise des le chargement.
Instructions
Le travail se fait dans js/modules/exo02.js et dans js/app.js. Dans js/app.js, importer la fonction de l'exercice, selectionner les elements HTML necessaires, puis appeler la fonction exportee en lui passant ces elements.
Etape 01
- Creer le fichier js/modules/exo02.js. Exporter une fonction nommee execExo02.
-
La fonction execExo02 doit recevoir deux arguments et utiliser exactement ces noms
- mdpInputElem pour le champ #mdp
- checkVoirElem pour la checkbox #toggleMdp
- Si mdpInputElem ou checkVoirElem est absent, arreter la fonction avec return. Cela evite une erreur qui bloquerait tout le JavaScript de la page.
-
Creer une fonction utilitaire nommee determinerTypeInput
qui recoit un booleen nomme doitAfficher.
Elle doit renvoyer
- "text" si doitAfficher vaut true
- "password" si doitAfficher vaut false
- Creer une fonction utilitaire nommee rafraichirVisibiliteMdp qui recoit inputElem et checkElem. Son role est de decider du nouveau type puis de mettre a jour le champ si necessaire.
- Dans rafraichirVisibiliteMdp, lire l'etat de la case via checkElem.checked, puis calculer le type attendu avec determinerTypeInput(...).
- Lire le type actuel du champ avec inputElem.getAttribute("type"). Si le type actuel est deja correct, ne rien faire. Si le type est different, modifier l'attribut avec inputElem.setAttribute("type", nouveauType). L'objectif est d'eviter une ecriture inutile.
Etape 02
- Dans execExo02, creer une fonction gestionnaire nommee gererChangementVisibilite. Elle doit appeler rafraichirVisibiliteMdp avec mdpInputElem et checkVoirElem.
- Poser un ecouteur change sur checkVoirElem en utilisant gererChangementVisibilite. L'evenement change se declenche quand l'utilisateur coche ou decoche la case.
- Appeler une premiere fois rafraichirVisibiliteMdp au chargement, afin de synchroniser immediatement l'interface. Cela permet aussi de gerer le cas ou le navigateur conserve l'etat de la checkbox apres un rafraichissement.
Etape 03
- Dans js/app.js, importer la fonction execExo02 depuis ./modules/exo02.js.
-
Selectionner les deux elements necessaires et stocker chaque reference dans une constante
- mdpInputElem pour #mdp
- checkboxToggleMdpElem pour #toggleMdp
- Appeler execExo02 en lui passant mdpInputElem et checkboxToggleMdpElem, dans cet ordre.
Extrait concerne dans js/app.js
const mdpInputElem = document.querySelector('#mdp');
const checkboxToggleMdpElem = document.querySelector('#toggleMdp');
execExo02(mdpInputElem, checkboxToggleMdpElem);
Test
- Cocher la case et verifier que l'attribut type passe a text.
- Decocher la case et verifier que l'attribut type repasse a password.
- Rafraichir la page apres avoir coche la case et verifier que le type reste coherent avec l'etat conserve.
Exo-03
Cet exercice porte sur le masquage et l'affichage d'un bloc via l'attribut HTML hidden, et sur la synchronisation de l'etat d'un bouton avec aria-expanded. L'objectif est de basculer l'interface au clic, sans modifier le CSS.
Accessibilite
Le masquage est fait avec hidden, ce qui retire l'element de l'affichage et de la navigation clavier. Le bouton doit en plus annoncer l'etat du contenu via aria-expanded. Dans le HTML, aria-controls="messageSecret" relie deja le bouton a la zone pilotee et donne du contexte aux technologies d'assistance.
Rappel sur aria-expanded. Cet attribut represente l'etat "deploye" du contenu pilote. Il doit valoir "true" quand le message est visible, et "false" quand il est cache.
Attendu
Au chargement, le message est cache car il possede l'attribut hidden. Au clic sur le bouton, le message devient visible et le texte du bouton change pour proposer l'action inverse. Au clic suivant, le message redevient cache. A chaque bascule, aria-expanded doit etre mis a jour pour rester coherent avec l'etat reel.
Instructions
Le travail se fait dans js/modules/exo03.js et dans js/app.js. Dans js/app.js, importer la fonction de l'exercice, selectionner les elements HTML necessaires, puis appeler la fonction exportee en lui passant ces elements.
Etape 01
- Creer le fichier js/modules/exo03.js. Exporter une fonction nommee execExo03.
-
La fonction execExo03 doit recevoir deux arguments et utiliser exactement ces noms
- messageElem pour le bloc #messageSecret
- boutonElem pour le bouton #btnToggleSecret
- Si messageElem ou boutonElem est absent, arreter la fonction avec return. Cela evite une erreur qui bloquerait tout le JavaScript de la page.
-
Creer une fonction de logique pure nommee calculerProchainEtat qui recoit un booleen
estActuellementCache.
Elle doit renvoyer un objet contenant
- doitEtreCache, l'etat cible du message
- texteBouton, le prochain texte a afficher sur le bouton
- ariaExpanded, l'etat "deploye" a ecrire dans aria-expanded
-
Dans calculerProchainEtat, determiner si le contenu doit etre visible.
ariaExpanded doit representer la visibilite du message
- message visible, ariaExpanded vaut true
- message cache, ariaExpanded vaut false
Etape 02
- Creer une fonction utilitaire nommee basculerAffichage qui recoit messageElem et boutonElem. Son role est de lire l'etat actuel, de calculer l'etat suivant, puis d'appliquer les changements au DOM.
- Dans basculerAffichage, verifier si le message est cache en testant la presence de hidden avec hasAttribute("hidden"). Cela renvoie true si l'attribut est present, sinon false.
- Appeler calculerProchainEtat pour obtenir les valeurs a appliquer. Si le message doit etre cache, ajouter hidden via setAttribute("hidden", ""). Si le message doit etre visible, retirer hidden via removeAttribute("hidden").
- Mettre a jour le texte du bouton en affectant prochain.texteBouton a boutonElem.textContent.
- Mettre a jour aria-expanded sur le bouton avec setAttribute. Un attribut HTML recoit du texte, donc convertir le booleen en chaine avec String(...). L'objectif est d'obtenir exactement "true" ou "false" dans le HTML.
Etape 03
- Dans execExo03, creer une fonction gestionnaire nommee gererBoutonElemClic. Elle doit appeler basculerAffichage avec messageElem et boutonElem.
- Poser un ecouteur click sur boutonElem en utilisant gererBoutonElemClic.
- Initialiser aria-expanded au chargement, en le calquant sur l'etat reel du message. L'etat reel se deduit de la presence de hidden. Si le message a hidden, alors aria-expanded doit valoir "false".
Extrait concerne dans js/app.js
const messageSecretElem = document.querySelector('#messageSecret');
const btnToggleSecretElem = document.querySelector('#btnToggleSecret');
execExo03(messageSecretElem, btnToggleSecretElem);
Test
- Au chargement, verifier que le message est cache et que aria-expanded vaut "false".
- Cliquer sur le bouton, verifier que le message devient visible, que le texte du bouton change, et que aria-expanded vaut "true".
- Cliquer a nouveau, verifier que le message se recache et que aria-expanded"false".
Exo-04
Cet exercice porte sur un bouton qui bascule un theme clair ou sombre. Le CSS se base sur un attribut personnalise data-theme present sur une section. Le JavaScript doit modifier cet attribut via getAttribute() et setAttribute(), puis synchroniser un bouton accessible base sur le modele ARIA switch avec role="switch" et aria-checked.
Accessibilite
Dans ce modele, le bouton n'est pas un "toggle button" classique. Il utilise le modele switch, donc l'etat doit etre porte par aria-checked, pas par aria-pressed.
Premiere rencontre avec role="switch" et aria-checked. role="switch" indique aux technologies d'assistance que ce bouton se comporte comme un interrupteur a deux etats. aria-checked represente l'etat de l'interrupteur "true" quand l'option est activee, "false" quand elle est desactivee. Le JavaScript doit maintenir cette valeur coherente avec le theme reel.
Rappel sur data-theme. C'est un attribut personnalise utilise comme point d'accroche pour le CSS. Exemple, un selecteur du type section[data-theme="dark"] applique un style specifique quand la valeur vaut dark. Changer data-theme ne declenche pas d'annonce lecteur d'ecran. C'est pour cela que le bouton doit rester explicite via son texte et via aria-checked.
Attendu
Au clic sur le bouton, la section bascule entre data-theme="light" et data-theme="dark". Le texte du bouton doit aussi basculer, pour rester comprehensible. L'attribut aria-checked doit valoir "true" en mode sombre, et "false" en mode clair. Au chargement, l'interface doit etre synchronisee avec l'etat reel present dans le HTML.
Instructions
Le travail se fait dans js/modules/exo04.js et dans js/app.js. Dans js/app.js, importer la fonction de l'exercice, selectionner les elements HTML necessaires, puis appeler la fonction exportee en lui passant ces elements.
Etape 01
- Creer le fichier js/modules/exo04.js. Exporter une fonction nommee execExo04.
-
La fonction execExo04 doit recevoir deux arguments et utiliser exactement ces noms
- conteneurElem pour la section #exo04-section
- boutonElem pour le bouton #btnTheme
- Si conteneurElem ou boutonElem est absent, arreter la fonction avec return. Cela evite une erreur qui bloquerait tout le JavaScript de la page.
- Creer une fonction de logique pure nommee determinerProchainTheme qui recoit themeActuel et renvoie "dark" si le theme actuel vaut "light", ou "light" si le theme actuel vaut "dark".
Etape 02
- Creer une fonction utilitaire nommee rafraichirInterfaceTheme qui recoit conteneurElem et boutonElem. Son role est de lire le theme actuel, de calculer le prochain theme, puis d'appliquer toutes les mises a jour au DOM.
- Lire le theme actuel dans l'attribut data-theme du conteneur via conteneurElem.getAttribute("data-theme"). Si la valeur est absente, considerer "light" comme valeur par defaut.
- Calculer le prochain theme avec determinerProchainTheme, puis ecrire la nouvelle valeur dans data-theme avec conteneurElem.setAttribute("data-theme", prochain).
- Determiner si le theme devient sombre en comparant prochain a "dark", et stocker le resultat dans une constante nommee estSombre.
- Mettre a jour le texte du bouton avec un libelle clair, par exemple "Desactiver le mode sombre" quand le theme est sombre, et "Activer le mode sombre" quand le theme est clair.
- Mettre a jour aria-checked sur le bouton afin qu'il reflète l'etat du switch. Convertir le booleen en texte avec String(estSombre) pour obtenir "true" ou "false". Ecrire l'attribut avec setAttribute.
Etape 03
- Dans execExo04, creer une fonction gestionnaire nommee gererClicTheme. Elle doit appeler rafraichirInterfaceTheme avec conteneurElem et boutonElem.
- Poser un ecouteur click sur boutonElem en utilisant gererClicTheme.
- Appeler une premiere fois rafraichirInterfaceTheme au chargement, afin de synchroniser data-theme, le texte du bouton, et aria-checked avec l'etat reel present dans le HTML.
Extrait concerne dans js/app.js
const sectionThemeElem = document.querySelector('#exo04-section');
const btnThemeElem = document.querySelector('#btnTheme');
execExo04(sectionThemeElem, btnThemeElem);
Test
- Au chargement, verifier que aria-checked correspond a la valeur initiale de data-theme.
- Cliquer sur le bouton, verifier que data-theme bascule entre light et dark.
- Verifier que le texte du bouton bascule aussi et reste coherent avec l'etat actuel.
- Verifier que aria-checked vaut "true" en mode sombre et "false" en mode clair.
Exo-05
Cet exercice porte sur une barre de progression accessible. La valeur courante est stockée dans des attributs ARIA sur l'element de barre, et l'affichage visuel est synchronise via style.width. L'exercice introduit aussi une annonce fiable pour lecteur d'ecran via une zone vivante role="status" et aria-live="polite".
Accessibilite
Premiere rencontre avec le composant ARIA role="progressbar". Sur un element avec role="progressbar", les attributs suivants decrivent l'etat de progression
- aria-valuemin indique la valeur minimale possible, ici 0
- aria-valuemax indique la valeur maximale possible, ici 100
- aria-valuenow indique la valeur courante, mise a jour par JavaScript
- aria-valuetext fournit une version textuelle plus parlante, par exemple "25 %"
- aria-label donne un nom accessible au composant quand il n'a pas de libelle visible
Rappel sur aria-live et role="status". Mettre a jour du texte dans une zone vivante est annonce de facon plus fiable qu'un simple changement d'attribut. Ici, un paragraphe de statut annonce "Progression 25 %" etc.
Attendu
Au clic sur le bouton, la progression augmente par bonds de 25 %. Quand la valeur depasse 100 %, elle repart a 0 %. La barre doit rester coherente sur trois plans la valeur ARIA (aria-valuenow et aria-valuetext), l'affichage visuel (largeur en %), et le texte annonce dans la zone de statut. Au chargement, l'interface doit se synchroniser avec la valeur deja presente dans le HTML.
Instructions
Le travail se fait dans js/modules/exo05.js et dans js/app.js. Dans js/app.js, importer la fonction de l'exercice, selectionner les elements HTML necessaires, puis appeler la fonction exportee en lui passant ces elements.
Etape 01
- Creer le fichier js/modules/exo05.js. Exporter une fonction nommee execExo05.
-
La fonction execExo05 doit recevoir trois arguments et utiliser exactement ces noms
- barreElem pour l'element #progressBar
- boutonElem pour le bouton #btnProgress
- progressTexteElem pour le paragraphe #progressTexte
- Si barreElem ou boutonElem est absent, arreter la fonction avec return. Le texte progressTexteElem peut etre optionnel, car on peut afficher la barre sans annonce.
-
Creer une fonction de logique pure nommee calculerProgression qui recoit
valeurActuelle et renvoie la valeur suivante
- ajouter 25
- si le resultat depasse 100, renvoyer 0
- Creer une fonction utilitaire nommee bornerEntre0Et100. Elle recoit une valeur et renvoie toujours un nombre compris entre 0 et 100. Cela rend le module plus robuste si l'attribut HTML a une valeur inattendue.
Etape 02
- Creer une fonction nommee lireValeurCourante qui recoit barreElem. Son role est de lire aria-valuenow dans le HTML, de convertir en nombre, puis de borner entre 0 et 100.
- Lire la valeur avec barreElem.getAttribute("aria-valuenow"). Comme un attribut est toujours du texte, convertir cette chaine en nombre avec Number.parseInt(texte, 10). Si la conversion echoue, utiliser 0.
-
Creer une fonction nommee ecrireProgression qui recoit
barreElem, progressTexteElem et une valeur.
Son role est d'ecrire toutes les informations coherentes au meme moment
- mettre a jour aria-valuenow avec setAttribute
- mettre a jour aria-valuetext avec un texte du type "25 %"
- mettre a jour la largeur via barreElem.style.width = "25%"
- mettre a jour le texte de statut, si progressTexteElem existe
- Pour l'annonce lecteur d'ecran, ecrire par exemple Progression 25 % dans #progressTexte. Comme cet element a role="status" et aria-live="polite" dans le HTML, son changement de texte peut etre annonce automatiquement.
-
Creer une fonction nommee rafraichirProgression qui recoit
barreElem et progressTexteElem.
Elle doit
- lire la valeur actuelle via lireValeurCourante
- calculer la suivante via calculerProgression
- appliquer le tout via ecrireProgression
Etape 03
- Dans execExo05, creer une fonction gestionnaire nommee gererClicProgres. Elle doit appeler rafraichirProgression avec barreElem et progressTexteElem.
- Poser un ecouteur click sur boutonElem en utilisant gererClicProgres.
- Au chargement, lire la valeur initiale dans le HTML, puis appeler ecrireProgression pour synchroniser aria-valuenow, aria-valuetext, style.width, et le texte de #progressTexte.
Extrait concerne dans js/app.js
const barreProgresElem = document.querySelector('#progressBar');
const btnProgresElem = document.querySelector('#btnProgress');
const progressTexteElem = document.querySelector('#progressTexte');
execExo05(barreProgresElem, btnProgresElem, progressTexteElem);
Test
- Au chargement, verifier que la largeur correspond a aria-valuenow et que aria-valuetext correspond au format "X %".
- Cliquer sur Etape suivante (+25%) et verifier que la valeur passe a 25, 50, 75, 100, puis revient a 0.
- Verifier que le texte Progression X % est mis a jour a chaque clic.
- Verifier dans l'inspecteur que aria-valuenow et aria-valuetext changent a chaque clic.
Exo-06
Cet exercice porte sur la mise à jour d'un lien en fonction d'une langue choisie dans un select. Il utilise getAttribute, setAttribute, hasAttribute et removeAttribute pour manipuler les attributs tels qu'ils existent dans le HTML. L'exercice ajoute aussi une annonce fiable côté lecteur d'écran en mettant à jour une zone vivante role="status" et aria-live="polite".
Accessibilite
Premiere rencontre avec hreflang et lang sur un lien. Quand le texte visible d'un lien est dans une langue différente de la page, l'attribut lang sur le lien aide les technologies d'assistance a prononcer correctement ce fragment. L'attribut hreflang indique la langue attendue de la ressource cible du lien.
Rappel sur les zones vivantes. Changer uniquement href n'est pas annonce de maniere fiable par les lecteurs d'ecran. Une annonce plus fiable consiste a mettre a jour du texte dans une zone vivante, ici #exo06-statut qui a role="status" et aria-live="polite" dans le HTML.
Attendu
Quand la langue change dans le menu deroulant, l'attribut href du lien de contact doit etre reconstruit. La convention de l'exercice est la suivante la version francaise n'ajoute pas de prefixe, la version anglaise et neerlandaise ajoutent respectivement /en et /nl devant le chemin. Le texte du lien doit aussi changer selon la langue, et une zone de statut doit annoncer la mise a jour.
Instructions
Le travail se fait dans js/modules/exo06.js et dans js/app.js. Dans js/app.js, importer la fonction de l'exercice, selectionner les elements HTML necessaires, puis appeler la fonction exportee en lui passant ces elements.
Etape 01
- Creer le fichier js/modules/exo06.js. Exporter une fonction nommee execExo06.
-
La fonction execExo06 doit recevoir deux arguments et utiliser exactement ces noms
- exo06ConteneurElem pour le conteneur #exo06-conteneur
- langueSelecteurElem pour le select #langueSelecteur
- Si exo06ConteneurElem ou langueSelecteurElem est absent, arreter avec return. Cela evite une erreur qui bloquerait tout le JavaScript de la page.
- Depuis exo06ConteneurElem, selectionner le lien avec querySelector("#lienDynamique") et le stocker dans une constante nommee lienElem. Si le lien est introuvable, arreter.
- Toujours depuis exo06ConteneurElem, selectionner la zone de statut #exo06-statut et stocker la reference dans une constante nommee statutElem. Cet element sert a annoncer la mise a jour.
Etape 02
-
Creer une fonction utilitaire nommee obtenirHrefBase.
Elle recoit lienElem et doit memoriser la valeur initiale du href.
Strategie utilisee
- si lienElem n'a pas l'attribut data-href-base, lire href via getAttribute puis l'enregistrer via setAttribute
- toujours renvoyer la valeur de data-href-base
- Creer une fonction utilitaire nommee retirerPrefixeLangue qui recoit un chemin. Si le chemin commence par /fr/, /en/ ou /nl/, retirer ce prefixe. Sinon, renvoyer le chemin tel quel. Une boucle for sur un tableau ["fr", "en", "nl"] suffit.
-
Creer une fonction de logique pure nommee construireHref.
Elle recoit hrefBase et nouvelleLangue, puis renvoie le nouveau chemin.
Regles
- retirer d'abord un prefixe eventuel via retirerPrefixeLangue
- forcer un chemin qui commence par /
- si la langue vaut "fr", renvoyer le chemin sans prefixe
- sinon, prefixer avec /${langue}
Etape 03
-
Creer une fonction nommee mettreAJourLienEtAnnonce qui recoit
lienElem, langueSelecteurElem et statutElem.
Son role est de synchroniser
- l'attribut href
- le texte du lien
- les attributs lang et hreflang
- le texte de la zone de statut
- Lire la langue selectionnee via langueSelecteurElem.value. Recuperer ensuite le href de base via obtenirHrefBase(lienElem), puis construire le nouveau href via construireHref(hrefBase, nouvelleLangue).
- Mettre a jour l'attribut href avec lienElem.setAttribute("href", nouveauHref). L'objectif est de travailler sur la valeur d'attribut telle qu'elle est ecrite dans le HTML.
-
Mettre a jour la langue du texte du lien
- si la langue vaut "fr", supprimer l'attribut lang avec removeAttribute
- sinon, ajouter lang avec la langue courante via setAttribute
- Mettre a jour hreflang sur le lien avec la meme langue. Cela indique la langue attendue de la page cible.
- Mettre a jour le texte visible du lien avec un libelle humain selon la langue. Eviter d'afficher une URL. Un texte court et comprehensible est plus stable et plus accessible.
- Mettre a jour la zone statutElem si elle existe. Ecrire une phrase qui mentionne la langue et, si tu veux, le chemin calcule. Comme statutElem est une zone vivante, ce changement de texte peut etre annonce automatiquement.
Etape 04
- Dans execExo06, creer un gestionnaire nomme gererChangement. Il doit appeler mettreAJourLienEtAnnonce avec lienElem, langueSelecteurElem et statutElem.
- Poser un ecouteur change sur langueSelecteurElem. L'evenement se declenche quand la valeur selectionnee change.
- Appeler une premiere fois gererChangement au chargement afin de synchroniser l'etat immediatement.
Extrait concerne dans js/app.js
const exo01ConteneurElem = document.querySelector('#exo06-conteneur');
const selecteurLangueElem = document.querySelector('#langueSelecteur');
execExo06(exo01ConteneurElem, selecteurLangueElem);
Test
- Changer la langue et verifier que href change dans l'inspecteur.
- Verifier que le texte du lien change selon la langue selectionnee.
- Verifier que hreflang est mis a jour avec la langue selectionnee.
- En choisissant fr, verifier que l'attribut lang est absent. En choisissant en ou nl, verifier qu'il est present.
- Verifier que la zone #exo06-statut est mise a jour a chaque changement.
Exo-07
Cet exercice porte sur une visionneuse de produits. Il utilise des attributs data-* pour stocker la source et le texte alternatif d'une image, met a jour src et alt via setAttribute, et synchronise l'etat d'une serie de boutons avec aria-pressed. Pour une annonce fiable cote lecteur d'ecran, il met aussi a jour une zone de statut role="status" et aria-live="polite".
Accessibilite
Premiere rencontre avec aria-pressed. Cet attribut transforme un bouton en bouton a etat. Il doit indiquer quelle variante est active "true" sur le bouton selectionne et "false" sur les autres.
Rappel sur les annonces. Une modification de l'attribut alt d'une image n'est pas annoncee de maniere fiable. Pour annoncer un changement, mettre a jour un texte dans une zone vivante. Ici, #produitStatut a role="status" et aria-live="polite" dans le HTML.
Attendu
Un clic sur une miniature doit changer l'image principale. Cela implique de mettre a jour src et alt. Le bouton de la variante selectionnee doit passer a aria-pressed="true" et tous les autres a aria-pressed="false". La zone #produitStatut doit annoncer la variante selectionnee. Au chargement, l'etat initial doit etre coherent avec le bouton deja marque actif dans le HTML.
Instructions
Le travail se fait dans js/modules/exo07.js et dans js/app.js. Dans js/app.js, importer la fonction de l'exercice, selectionner les elements HTML necessaires, puis appeler la fonction exportee en lui passant ces elements.
Etape 01
- Creer le fichier js/modules/exo07.js. Exporter une fonction nommee execExo07.
-
La fonction execExo07 doit recevoir trois arguments et utiliser exactement ces noms
- imagePrincipaleElem pour l'image #mainImage
- listeBoutonsThumbs pour la liste des boutons .thumb
- statutElem pour la zone #produitStatut
- Si imagePrincipaleElem est absent ou si listeBoutonsThumbs est vide, arreter avec return.
Etape 02
-
Creer deux fonctions de logique pure pour lire les donnees depuis un bouton
- extraireSourceImage(boutonElem) qui lit data-full-img avec getAttribute
- extraireAltImage(boutonElem) qui lit data-alt avec getAttribute
-
Creer une fonction utilitaire nommee mettreAJourEtatBoutons.
Elle recoit listeBoutonsThumbs et boutonClique.
Parcourir tous les boutons avec une boucle for et, pour chacun, ecrire
aria-pressed
- "true" si le bouton courant est le bouton clique
- "false" sinon
Etape 03
- Creer une fonction nommee rafraichirApercu qui recoit imagePrincipaleElem, listeBoutonsThumbs, boutonClique et statutElem.
-
Dans rafraichirApercu, recuperer la nouvelle source et le nouvel alt via les deux fonctions de lecture,
puis mettre a jour l'image
- si la source n'est pas vide, ecrire src avec setAttribute
- ecrire alt avec setAttribute
- Appeler ensuite mettreAJourEtatBoutons pour synchroniser aria-pressed sur tous les boutons.
- Si statutElem existe, mettre a jour son texte. Utiliser une phrase courte et stable, par exemple en reutilisant le texte du bouton clique via boutonClique.textContent.trim(). Cette mise a jour sert d'annonce fiable.
Etape 04
- Dans execExo07, poser un ecouteur click sur chaque bouton miniature. Une boucle for suffit.
- Au clic, appeler une fonction de gestion qui declenche rafraichirApercu(imagePrincipaleElem, listeBoutonsThumbs, boutonClique, statutElem). L'objectif est que la meme logique soit reutilisee pour les clics et pour l'initialisation.
-
Ajouter une initialisation au chargement
- chercher un bouton dont aria-pressed vaut "true"
- si aucun n'est trouve, prendre le premier bouton de la liste
- appeler rafraichirApercu avec ce bouton actif
Extrait concerne dans js/app.js
const imageProduitElem = document.querySelector('#mainImage');
const lesMiniaturesElements = document.querySelectorAll('.thumb');
execExo07(imageProduitElem, lesMiniaturesElements);
Dans cette serie, execExo07 doit recevoir aussi #produitStatut. Il faut donc selectionner #produitStatut dans js/app.js et le passer en 3e argument.
Test
- Cliquer sur Rouge puis Vert et verifier que l'image change a chaque fois.
- Verifier que l'attribut alt de #mainImage correspond a la variante.
- Verifier que seul le bouton actif a aria-pressed="true".
- Verifier que #produitStatut est mis a jour a chaque clic.
- Au chargement, verifier que l'etat initial est coherent avec le bouton deja marque actif dans le HTML.
Exo-08-ampoule-couleurs
Cet exercice consiste à piloter une ampoule visuelle en cliquant sur des boutons de couleur. Le clic copie une classe CSS depuis le bouton vers l'ampoule, met à jour un texte de statut, et synchronise l'état des boutons via ARIA. La mise en place utilise la délégation d'événement, un seul écouteur sur le conteneur des boutons.
Attendu
Cliquer sur un bouton allume l'ampoule avec la couleur correspondante. Le texte de #ampoule-statut devient "Lumière (couleur)". Le bouton cliqué passe à aria-pressed="true" et les autres repassent à false. Recliquer sur le bouton déjà actif éteint l'ampoule, remet le statut à "Lumière éteinte", et remet tous les boutons à false.
Structure
Créer un dossier nommé Exo-08-ampoule-couleurs, puis y organiser les fichiers et dossiers en respectant l'arborescence suivante
Exo-08-ampoule-couleurs/
|-- index.html
|-- assets/
| └── css/
| └── style.css
└── js/
|-- app.js
└── modules/
└── copieClasseCssVersAutreBalise.js
Fichiers a copier
index.html
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JS Attributs DOM - Exo 08</title>
<link rel="stylesheet" href="./assets/css/style.css">
</head>
<body>
<main>
<h1>JS Attributs DOM - Exo 08</h1>
<!--
aria-label
Donne un nom accessible à un élément quand il n'a pas de texte visible.
Ce nom est annoncé par les lecteurs d'écran quand l'élément est rencontré.
-->
<!--
aria-describedby
Relie l'élément à un autre élément qui contient un texte explicatif.
Un lecteur d'écran peut lire ce texte en complément du nom (aria-label ou texte visible).
-->
<div
id="ampoule"
class="ampoule inactive"
role="img"
aria-label="Lumière éteinte"
aria-describedby="ampoule-statut"
></div>
<!--
aria-live
Rend la zone "vivante".
Quand le texte de cette zone change via JavaScript, un lecteur d'écran peut l'annoncer.
polite demande une annonce non intrusive.
-->
<p id="ampoule-statut" role="status" aria-live="polite">
Lumière éteinte
</p>
<!--
aria-label sur un group
Donne un nom au groupe de boutons, utile pour comprendre le contexte.
-->
<div id="btn-conteneur" class="btn-conteneur" role="group" aria-label="Choisir une couleur de lumière">
<!--
aria-controls
Indique quel élément est piloté par ce bouton.
Ici, chaque bouton pilote l'ampoule (id="ampoule").
-->
<!--
aria-pressed
Transforme le bouton en bouton à état.
"true" signifie actif, "false" signifie inactif.
Le JavaScript doit synchroniser cet état avec la réalité.
-->
<button class="btn-1" type="button" data-couleur="jaune" aria-controls="ampoule" aria-pressed="false">Jaune</button>
<button class="btn-2" type="button" data-couleur="rose" aria-controls="ampoule" aria-pressed="false">Rose</button>
<button class="btn-3" type="button" data-couleur="bleu" aria-controls="ampoule" aria-pressed="false">Bleu</button>
</div>
</main>
<script src="./js/app.js" type="module"></script>
</body>
</html>
assets/css/style.css
/* VARIABLES */
:root {
/* Couleurs de base (Sémantiques) */
--fond-principal: #0e1319;
--texte-principal: #e7e1ca;
--accent-visuel: orange;
/* Nouvelles couleurs ajoutées précédemment */
--texte-secondaire: #9ca3af; /* Gris clair */
--etat-succes: #2ecc71; /* Vert */
/* Couleurs de boutons (Tests renommés par thème) */
--couleur-primaire: rgb(233, 194, 20); /* Ancien test1 (Jaune/Or) */
--couleur-secondaire: rgb(236, 20, 228); /* Ancien test2 (Rose/Magenta) */
--couleur-tertiaire: rgb(19, 123, 228); /* Ancien test3 (Bleu) */
/* Espaces */
--espace-s: 1vw;
--espace-m: 2rem;
--espace-l: 8rem;
/* Typo */
--police-corps: 'Arial', sans-serif;
}
/* RESET */
html, body, div, h1, button {
margin: 0;
padding: 0;
border: 0;
box-sizing: border-box;
}
/* BASES */
html {
color: var(--texte-principal);
font-family: var(--police-corps);
background-color: var(--fond-principal);
}
body {
min-height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
}
main {
padding: var(--espace-m) 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
width: 100%;
}
h1 {
margin-bottom: var(--espace-m);
text-align: center;
}
/* Ampoule */
#ampoule {
margin-bottom: var(--espace-m);
border-radius: 50%;
display: inline-block;
width: 3rem;
height: 3rem;
}
#ampoule.inactive {
background-image: radial-gradient(var(--texte-principal), var(--fond-principal) 80%);
}
/* Boutons */
.btn-conteneur {
display: flex;
gap: 0 3rem;
}
button {
position: relative;
border: 2px solid var(--fond-principal);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
width: 5rem;
height: 5rem;
background: radial-gradient(var(--texte-principal), var(--accent-visuel) 50%, var(--fond-principal) 80%);
transition: transform ease-in-out 0.2s, box-shadow ease-in-out 0.2s;
box-shadow: 0 5px 4px #353537, 5px 0 4px #c8c9ce, 0 -5px 4px #232325, -5px 0 4px #7a7b81;
transform: scale3d(0.9, 0.9, 1);
}
button:hover {
cursor: pointer;
box-shadow: 0 10px 4px #353537, 10px 0 4px #c8c9ce, 0 -10px 4px #232325, -10px 0 4px #7a7b81;
transform: scale3d(0.8, 0.8, 1);
}
/* Variantes de boutons */
.btn-1 {
background: radial-gradient(var(--texte-principal), var(--couleur-primaire) 50%, var(--fond-principal) 80%);
}
.btn-2 {
background: radial-gradient(var(--texte-principal), var(--couleur-secondaire) 50%, var(--fond-principal) 80%);
}
.btn-3 {
background: radial-gradient(var(--texte-principal), var(--couleur-tertiaire) 50%, var(--fond-principal) 80%);
}
Instructions
Le travail se fait dans js/modules/copieClasseCssVersAutreBalise.js et dans js/app.js. Dans js/app.js, il faut importer la fonction creerCopieurDeClasse, sélectionner les éléments HTML nécessaires, créer une instance du copieur, puis appeler cette instance lors des clics sur le conteneur des boutons.
Etape 01
- Dans js/modules/copieClasseCssVersAutreBalise.js, exporter une fonction nommée creerCopieurDeClasse.
- Dans creerCopieurDeClasse, déclarer une variable nommée dernierBoutonActif initialisée à null. Cette variable doit rester privée dans la fermeture, afin de mémoriser le dernier bouton actif.
-
Retourner une fonction qui reçoit quatre arguments
- sourceElem
- cibleElem
- statutElem
- listeBoutons
Etape 02
- Créer une fonction majEtatBoutons qui met à jour aria-pressed sur tous les boutons. Pour chaque bouton, écrire aria-pressed à "true" uniquement pour le bouton actif, et "false" pour les autres.
-
Créer une fonction eteindreAmpoule qui remet l'ampoule à l'état initial
- classe "ampoule inactive"
- aria-label à "Lumière éteinte"
- texte de #ampoule-statut à "Lumière éteinte"
- tous les boutons à aria-pressed="false"
-
Créer une fonction allumerAmpoule qui applique l'état issu du bouton cliqué
- copier la classe du bouton sur l'ampoule en conservant la classe ampoule
- lire data-couleur du bouton et mettre à jour aria-label de l'ampoule
- mettre à jour #ampoule-statut avec "Lumière (couleur)"
- mettre à jour l'état des boutons via majEtatBoutons
Etape 03
-
Dans la fonction retournée, gérer le toggle
- si sourceElem est le même que dernierBoutonActif, appeler eteindreAmpoule puis remettre dernierBoutonActif à null
- sinon, appeler allumerAmpoule puis mémoriser dernierBoutonActif = sourceElem
Etape 04
- Dans js/app.js, importer creerCopieurDeClasse depuis ./modules/copieClasseCssVersAutreBalise.js.
-
Sélectionner les éléments nécessaires dans des constantes
- ampouleElem via #ampoule
- ampouleStatutElem via #ampoule-statut
- btnConteneurAmpoule via #btn-conteneur
- boutonsCouleurElems via une sélection de tous les boutons du conteneur
- Créer une instance en appelant creerCopieurDeClasse() et stocker le résultat dans copierPourAmpoule.
- Poser un écouteur click sur btnConteneurAmpoule. Dans le gestionnaire, récupérer le bouton cliqué avec e.target.closest('button'). Appeler ensuite copierPourAmpoule en lui passant boutonSource, ampouleElem, ampouleStatutElem, boutonsCouleurElems, dans cet ordre.
Test
- Cliquer sur Jaune, vérifier #ampoule, #ampoule-statut et aria-pressed.
- Cliquer sur Rose, vérifier que Rose passe à true et Jaune repasse à false.
- Recliquer sur Rose, vérifier le retour à "Lumière éteinte" et tous les boutons à false.
Exo-09-menu-burger-accessible
Cet exercice consiste à créer un menu burger accessible en version mobile. Le menu doit s'ouvrir et se fermer en synchronisant la visibilité réelle (hidden) et l'état annoncé (aria-expanded). Le bouton burger ne contient pas de texte visible, donc son nom accessible est fourni par aria-label. Le comportement doit inclure une fermeture via la touche Escape, une gestion de focus cohérente, et un piège de focus minimal tant que le menu est ouvert.
Attendu
En mobile, cliquer sur le bouton ouvre le menu, retire hidden du ul#mainMenu, met aria-expanded à "true" et remplace le libellé du bouton par "Cacher le menu". Le focus est placé dans le menu. Tant que le menu est ouvert, Tab ne doit pas permettre de sortir du menu. Appuyer sur Escape, cliquer sur un lien, ou cliquer en dehors du nav ferme le menu, remet hidden, remet aria-expanded à "false", et replace le focus sur le bouton.
Structure
Créer un dossier nommé Exo-09-menu-burger-accessible, puis y organiser les fichiers et dossiers en respectant l'arborescence suivante
Exo-09-menu-burger-accessible/
|-- index.html
|-- assets/
| └── css/
| └── style.css
└── js/
|-- app.js
└── modules/
└── burger.js
Fichiers a copier
index.html
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JS Attributs DOM - Exo 09</title>
<link rel="stylesheet" href="./assets/css/style.css">
</head>
<body>
<header>
<nav aria-label="Navigation principale">
<!--
aria-controls
Lie le bouton à l'élément qu'il pilote.
La valeur est un id existant dans la page, ici mainMenu.
Cela ne fait rien tout seul, c'est une information pour les technologies d'assistance.
-->
<!--
aria-expanded
Indique si le contenu piloté est déployé.
false quand le menu est fermé, true quand il est ouvert.
Le JavaScript doit synchroniser cette valeur avec l'état réel du menu.
-->
<!--
aria-label
Donne un nom accessible à un élément sans texte visible utile.
Ici, le bouton ne contient que des spans décoratifs.
-->
<button
id="mainMenu-btn-toggleCollapse"
type="button"
aria-controls="mainMenu"
aria-expanded="false"
aria-label="Afficher le menu"
>
<!-- Décoratif -->
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</button>
<!--
hidden
Attribut HTML booléen.
Sa simple présence masque l'élément et le retire de l'ordre de tabulation.
Le JavaScript doit l'ajouter ou le retirer en même temps que aria-expanded.
-->
<ul id="mainMenu" hidden>
<li><a href="#">accueil</a></li>
<li><a href="#">tarifs</a></li>
<li><a href="#">contact</a></li>
</ul>
</nav>
</header>
<main>
<h1>JS Attributs DOM - Exo 09</h1>
</main>
<script type="module" src="./js/app.js"></script>
</body>
</html>
assets/css/style.css
/* --- VARIABLES --- */
:root {
/* Couleurs de base */
--fond-principal: #0e1319; /* Noir de ton exo 11 */
--texte-principal: #e7e1ca; /* Blanc cassé de ton exo 11 */
--accent-chaud: orange;
--bordure-douce: #cccccc;
--bordure-forte: #999999;
--fond-gris: #1e2a37;
--fond-overlay: rgba(14, 19, 25, 0.8);
/* États */
--fond-desactive: #e0e0e0;
--texte-desactive: #95a5a6;
/* Espaces et Rayons */
--espace-xl: 8rem;
--espace-m: 20px;
--espace-s: 12px;
--arrondi: 8px;
--interligne: 1.5;
/* Animation */
--vitesse-transition: 0.3s;
}
/* --- RESET & BOX MODEL CIBLÉ --- */
html, body, header, nav, main, h1, div, ul, li, a, button, span {
margin: 0;
padding: 0;
border: 0;
box-sizing: border-box;
}
/* --- BASE STYLES --- */
body {
font-family: sans-serif;
line-height: var(--interligne);
color: var(--texte-principal);
background-color: var(--fond-principal);
min-height: 100vh;
}
header {
padding: var(--espace-m) var(--espace-xl);
}
/* --- NAVIGATION DESKTOP --- */
header > nav > ul {
list-style: none;
display: flex;
justify-content: flex-end;
gap: var(--espace-m);
}
header > nav > ul > li > a {
text-decoration: none;
text-transform: uppercase;
color: var(--accent-chaud);
letter-spacing: 0.1rem;
transition: opacity var(--vitesse-transition);
}
h1 {
text-align: center;
}
/* Le bouton est masqué par défaut (Desktop) */
#mainMenu-btn-toggleCollapse {
display: none;
}
/* --- VERSION MOBILE (< 900px) --- */
@media (max-width: 900px) {
header { padding: var(--espace-m); }
/* BOUTON BURGER */
#mainMenu-btn-toggleCollapse {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: auto;
width: 30px;
height: 22px;
background: transparent;
cursor: pointer;
z-index: 100; /* Reste au dessus de l'overlay */
position: relative;
}
#mainMenu-btn-toggleCollapse > span {
display: block;
width: 100%;
height: 3px;
background-color: var(--accent-chaud);
transition: all var(--vitesse-transition) ease-in-out;
}
/* Transformation en Croix (ARIA-driven) */
#mainMenu-btn-toggleCollapse[aria-expanded="true"] > span:nth-child(1) { transform: translateY(9.5px) rotate(45deg); }
#mainMenu-btn-toggleCollapse[aria-expanded="true"] > span:nth-child(2) { opacity: 0; transform: translateX(-10px); }
#mainMenu-btn-toggleCollapse[aria-expanded="true"] > span:nth-child(3) { transform: translateY(-9.5px) rotate(-45deg); }
/* MENU OVERLAY */
header > nav > ul {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background-color: var(--fond-overlay);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
flex-direction: column;
justify-content: center;
align-items: center;
/* État initial caché */
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all var(--vitesse-transition) ease;
}
/* Ouverture du menu */
#mainMenu-btn-toggleCollapse[aria-expanded="true"] + ul {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
/* ANIMATION CASCADE (STAGGER) */
header > nav > ul > li {
opacity: 0;
transform: translateX(-30px);
transition: all 0.4s ease-out;
}
#mainMenu-btn-toggleCollapse[aria-expanded="true"] + ul > li {
opacity: 1;
transform: translateX(0);
}
/* Délais pour l'effet de cascade sur les 3 liens */
#mainMenu-btn-toggleCollapse[aria-expanded="true"] + ul > li:nth-child(1) { transition-delay: 0.1s; }
#mainMenu-btn-toggleCollapse[aria-expanded="true"] + ul > li:nth-child(2) { transition-delay: 0.2s; }
#mainMenu-btn-toggleCollapse[aria-expanded="true"] + ul > li:nth-child(3) { transition-delay: 0.3s; }
}
Instructions
Le travail se fait dans js/modules/burger.js et dans js/app.js. Dans js/app.js, il faut importer initMenuBurger, sélectionner le bouton burger, puis appeler la fonction d'initialisation en lui passant ce bouton.
Etape 01
- Créer le fichier js/modules/burger.js. Exporter une fonction nommée initMenuBurger.
- La fonction initMenuBurger doit recevoir un argument nommé btnBasculerElem qui correspond au bouton #mainMenu-btn-toggleCollapse.
- Si btnBasculerElem est absent, arrêter avec return.
- Lire aria-controls sur le bouton pour obtenir l'id du menu à piloter, puis récupérer le menu avec document.getElementById(menuId). Si l'id ou le menu est absent, arrêter avec return.
- Récupérer l'élément nav parent avec btnBasculerElem.closest('nav'). Si absent, arrêter avec return.
Etape 02
- Créer une fonction estMenuOuvert qui lit aria-expanded sur le bouton et renvoie un booléen. aria-expanded contient du texte, donc comparer à "true".
-
Créer une fonction ouvrirMenu qui
- met aria-expanded à "true"
- met aria-label à "Cacher le menu"
- retire hidden sur ul#mainMenu avec removeAttribute
- bloque le scroll du fond via document.body.style.overflow = "hidden"
-
Créer une fonction fermerMenu qui fait l'inverse
- met aria-expanded à "false"
- met aria-label à "Afficher le menu"
- ajoute hidden sur ul#mainMenu avec setAttribute
- restaure le scroll du fond
- replace le focus sur le bouton
- Initialiser l'état au chargement en appelant fermerMenu, afin de garantir un état stable.
Etape 03
- Dans initMenuBurger, créer un gestionnaire de clic sur le bouton. Il doit lire l'état actuel et appeler ouvrirMenu ou fermerMenu.
- Poser addEventListener('click', ...) sur btnBasculerElem.
Etape 04
- Ajouter une écoute globale clavier sur document pour gérer Escape. Si le menu est ouvert et que la touche est Escape ou Esc, fermer le menu.
-
Ajouter un piège de focus minimal avec Tab tant que le menu est ouvert
- récupérer la liste des éléments focusables dans le nav
- si Shift + Tab sur le premier élément, boucler vers le dernier
- si Tab sur le dernier élément, boucler vers le premier
-
Pour trouver les éléments focusables, utiliser un querySelectorAll avec un sélecteur combiné
- a[href]
- button:not([disabled])
- [tabindex]:not([tabindex="-1"])
Etape 05
- Ajouter un écouteur pointerdown sur document. Si le menu est ouvert et que le clic ne se trouve pas dans nav (via closest('nav')), fermer le menu.
- Ajouter une délégation de clic sur ul#mainMenu. Si le clic provient d'un lien (via closest('a')), fermer le menu.
Etape 06
- Dans js/app.js, importer initMenuBurger depuis ./modules/burger.js.
- Sélectionner le bouton burger dans une constante nommée btnBasculerElem avec document.querySelector('button#mainMenu-btn-toggleCollapse').
- Appeler initMenuBurger en lui passant btnBasculerElem.
Test
- Réduire la fenêtre sous 900px, vérifier que le bouton burger apparaît.
- Cliquer sur le bouton, vérifier aria-expanded et la disparition de hidden sur #mainMenu.
- Appuyer sur Escape, vérifier la fermeture et le retour du focus sur le bouton.
- Ouvrir le menu et naviguer au clavier avec Tab, vérifier que le focus boucle dans le menu.
- Ouvrir puis cliquer en dehors du nav, vérifier la fermeture.
- Ouvrir puis cliquer sur un lien, vérifier la fermeture.