Enrichit le reporting SCORM/xAPI et met en conformité RGAA le player
SCORM/xAPI : - Ajoute l'export SCORM 2004 (3rd/4th edition) au choix, en plus du 1.2 par défaut : sépare completion_status/success_status (un échec reste "completed" au lieu de retomber à tort en "incomplete" comme force la 1.2), remonte aussi cmi.interactions.n.* par question répondue. - Fournit cmi.core.score.min/max (1.2 et 2004), calculé depuis les récompenses de quiz du jeu, pour que le LMS affiche un vrai pourcentage au lieu du score brut à tort étiqueté "%". - Libellés de verbes xAPI en français en plus de l'anglais. Accessibilité (RGAA/WCAG 2.1 AA) sur le player : - Navigation clavier des objets de scène "au clic"/"au survol" (tabindex, role, Entrée/Espace, focus/blur). - alt sur les images (nom auteur ou décoratif), aria-hidden sur les icônes seules, role="dialog"/aria-live sur les boîtes de dialogue/quiz. - Landmark <main> + titre de page, respect de prefers-reduced-motion. - Le quiz n'avance plus automatiquement après un délai fixe : bouton "Continuer →" explicite (RGAA 2.2.1). - Avertissement de contraste dans l'éditeur de style de dialogue. - Déclaration d'accessibilité téléchargeable depuis la modale d'export.
This commit is contained in:
@@ -96,6 +96,13 @@ function bindHoverTexts() {
|
||||
});
|
||||
}
|
||||
|
||||
// RGAA/WCAG 2.1.1 (clavier) : un objet "Au clic" n'était auparavant
|
||||
// actionnable qu'à la souris (simple <div>/<img>, jamais un <button>/<a>
|
||||
// natif — voir screens/scenes/render_scene_object.py) — inatteignable et
|
||||
// inactivable au clavier. tabindex="0" + role="button" le rendent
|
||||
// focusable (sauf s'il l'est déjà — un <button>/<a> auteur, ex. widget
|
||||
// "bouton"/"lien", ne doit pas se voir écraser son rôle natif) ; Entrée/
|
||||
// Espace déclenchent le même comportement que le clic.
|
||||
function bindClicks() {
|
||||
document.querySelectorAll('[data-element-id]').forEach(elDiv => {
|
||||
const elementId = parseInt(elDiv.dataset.elementId, 10);
|
||||
@@ -112,7 +119,12 @@ function bindClicks() {
|
||||
if (elDiv.dataset.clickBound) return;
|
||||
elDiv.dataset.clickBound = '1';
|
||||
elDiv.style.cursor = 'pointer';
|
||||
elDiv.addEventListener('click', (ev) => {
|
||||
if (!elDiv.hasAttribute('tabindex')) elDiv.setAttribute('tabindex', '0');
|
||||
const tag = elDiv.tagName;
|
||||
if (!elDiv.hasAttribute('role') && tag !== 'BUTTON' && tag !== 'A' && tag !== 'INPUT') {
|
||||
elDiv.setAttribute('role', 'button');
|
||||
}
|
||||
const activate = (ev) => {
|
||||
ev.stopPropagation();
|
||||
// Si l'élément cliqué (ou un de ses parents, jusqu'au Répéteur
|
||||
// qui reçoit le gestionnaire) est en fait une LIGNE de Répéteur,
|
||||
@@ -126,6 +138,12 @@ function bindClicks() {
|
||||
// Exécute CHAQUE déclencheur "Au clic" posé sur cet élément, pas
|
||||
// seulement le premier (voir le commentaire de findTriggerNodess).
|
||||
triggers.forEach(trigger => runFlowFrom(trigger.node.id, trigger.screenId));
|
||||
};
|
||||
elDiv.addEventListener('click', activate);
|
||||
elDiv.addEventListener('keydown', (ev) => {
|
||||
if (ev.key !== 'Enter' && ev.key !== ' ' && ev.key !== 'Spacebar') return;
|
||||
ev.preventDefault();
|
||||
activate(ev);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -138,6 +156,12 @@ function bindClicks() {
|
||||
// (voir le commentaire sur TRIGGER_EVENTS dans screens/flow/constants.py
|
||||
// — pas de retour automatique à l'état d'avant survol, un créateur qui en
|
||||
// veut un doit poser l'action inverse sur "Fin du survol" lui-même).
|
||||
// RGAA/WCAG 2.1.1 (clavier) : "Au survol"/"Fin du survol" ne réagissaient
|
||||
// qu'à la souris (mouseenter/mouseleave) — focus/blur est l'équivalent
|
||||
// clavier standard (recevoir le focus = "survoler", le perdre = "fin du
|
||||
// survol"), donc un élément SEULEMENT concerné par le survol (jamais par
|
||||
// bindClicks() ci-dessus) doit aussi devenir focusable pour rester
|
||||
// atteignable au clavier.
|
||||
function bindHoverTriggers() {
|
||||
document.querySelectorAll('[data-element-id]').forEach(elDiv => {
|
||||
if (elDiv.dataset.hoverTriggerBound) return;
|
||||
@@ -146,11 +170,20 @@ function bindHoverTriggers() {
|
||||
const leaveTriggers = findTriggerNodess(elementId, 'fin_survol');
|
||||
if (!enterTriggers.length && !leaveTriggers.length) return;
|
||||
elDiv.dataset.hoverTriggerBound = '1';
|
||||
if (!elDiv.hasAttribute('tabindex')) elDiv.setAttribute('tabindex', '0');
|
||||
// Exécute CHAQUE déclencheur trouvé, pas seulement le premier — voir
|
||||
// le commentaire de findTriggerNodess (plusieurs déclencheurs
|
||||
// indépendants sur le même élément/événement doivent tous s'exécuter).
|
||||
if (enterTriggers.length) elDiv.addEventListener('mouseenter', () => enterTriggers.forEach(t => runFlowFrom(t.node.id, t.screenId)));
|
||||
if (leaveTriggers.length) elDiv.addEventListener('mouseleave', () => leaveTriggers.forEach(t => runFlowFrom(t.node.id, t.screenId)));
|
||||
if (enterTriggers.length) {
|
||||
const onEnter = () => enterTriggers.forEach(t => runFlowFrom(t.node.id, t.screenId));
|
||||
elDiv.addEventListener('mouseenter', onEnter);
|
||||
elDiv.addEventListener('focus', onEnter);
|
||||
}
|
||||
if (leaveTriggers.length) {
|
||||
const onLeave = () => leaveTriggers.forEach(t => runFlowFrom(t.node.id, t.screenId));
|
||||
elDiv.addEventListener('mouseleave', onLeave);
|
||||
elDiv.addEventListener('blur', onLeave);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user