feat(borne): police OpenDyslexic auto-hebergee + bascule accessibilite (RGAA Cr 1.c.2)
This commit is contained in:
parent
7ab9a5a8cf
commit
01f9078174
13 changed files with 334 additions and 2 deletions
|
|
@ -12,8 +12,11 @@
|
|||
* - Card border : 2px solid #FFC72C when selected / active
|
||||
*
|
||||
* Kiosk target: 1080x1920 portrait (touch screen).
|
||||
* Font stack: system-ui fallback — school font is not available as a web asset.
|
||||
* OpenDys is loaded conditionally for accessibility (RGAA Cr 1.c.4).
|
||||
* Base font stack: system-ui fallback (the school font is not available as a web asset).
|
||||
* Accessibility (RGAA Cr 1.c.2): the OpenDyslexic font (OFL 1.1) is self-hosted under
|
||||
* assets/fonts (see @font-face in section 12). a11y.js adds the .dys-font class on
|
||||
* <html> on demand, which redefines --font-family-base for the whole interface, and
|
||||
* persists the choice in localStorage. Base stack is the default.
|
||||
*/
|
||||
|
||||
/* ============================================================
|
||||
|
|
@ -2149,3 +2152,69 @@ button {
|
|||
text-align: center;
|
||||
margin: var(--space-4) 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
12. ACCESSIBILITY — dyslexia-friendly font + toggle (RGAA Cr 1.c.2)
|
||||
============================================================ */
|
||||
|
||||
/* OpenDyslexic (OFL 1.1), self-hosted under assets/fonts (see LICENSE-OpenDyslexic.txt).
|
||||
font-display: swap keeps text visible while the face downloads. */
|
||||
@font-face {
|
||||
font-family: "OpenDyslexic";
|
||||
src: url("../fonts/opendyslexic-latin-400-normal.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "OpenDyslexic";
|
||||
src: url("../fonts/opendyslexic-latin-700-normal.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* a11y.js sets .dys-font on <html>; the whole interface then inherits the
|
||||
dyslexia-friendly stack via --font-family-base. Higher specificity than :root,
|
||||
so it overrides the base token. */
|
||||
html.dys-font {
|
||||
--font-family-base: "OpenDyslexic", system-ui, -apple-system, "Segoe UI", Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* Fixed accessibility control, present on every screen (injected by a11y.js). */
|
||||
.a11y-toggle {
|
||||
position: fixed;
|
||||
top: var(--space-3);
|
||||
right: var(--space-3);
|
||||
z-index: 1000;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--color-bg-card);
|
||||
color: var(--color-text-primary);
|
||||
border: 2px solid var(--color-border-default);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-card);
|
||||
font-family: var(--font-family-base);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-bold);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.a11y-toggle__icon {
|
||||
font-size: var(--font-size-md);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Active state: not signalled by colour alone (RGAA 1.4.1) — aria-pressed exposes
|
||||
the state to assistive tech, and the label text stays visible. */
|
||||
.a11y-toggle[aria-pressed="true"] {
|
||||
background: var(--color-brand-yellow);
|
||||
border-color: var(--color-brand-yellow-dk);
|
||||
}
|
||||
|
||||
.a11y-toggle:focus-visible {
|
||||
outline: 3px solid var(--color-brand-yellow-dk);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
|
|
|||
21
src/public/borne/assets/fonts/LICENSE-OpenDyslexic.txt
Normal file
21
src/public/borne/assets/fonts/LICENSE-OpenDyslexic.txt
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
OpenDyslexic
|
||||
============
|
||||
|
||||
Files:
|
||||
opendyslexic-latin-400-normal.woff2
|
||||
opendyslexic-latin-700-normal.woff2
|
||||
|
||||
Copyright (c) Abelardo Gonzalez, with Reserved Font Name "OpenDyslexic".
|
||||
|
||||
OpenDyslexic is licensed under the SIL Open Font License, Version 1.1 (OFL-1.1).
|
||||
This license is available with a FAQ at: https://openfontlicense.org
|
||||
|
||||
These web font files (latin subset, woff2) were obtained from the npm package
|
||||
@fontsource/opendyslexic (jsDelivr mirror) and are redistributed here unmodified,
|
||||
as permitted by the OFL, to be served self-hosted by the Wakdo kiosk for the
|
||||
dyslexia-friendly display option (RGAA Cr 1.c.2).
|
||||
|
||||
Under the OFL: the font may be used, studied, modified and redistributed freely
|
||||
as long as it is not sold by itself; redistributions must retain this notice and
|
||||
the license; and the Reserved Font Name may not be used to promote derivative
|
||||
fonts. The full license text accompanies the upstream font package.
|
||||
Binary file not shown.
Binary file not shown.
129
src/public/borne/assets/js/a11y.js
Normal file
129
src/public/borne/assets/js/a11y.js
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* a11y.js — Bascule de police adaptee aux personnes dyslexiques (front borne).
|
||||
*
|
||||
* Accessibilite RGAA Cr 1.c.2 : une police specifique pour les personnes
|
||||
* dyslexiques est prevue ET integree. La police OpenDyslexic (OFL 1.1) est
|
||||
* auto-hebergee (assets/fonts, @font-face dans style.css). Ce module ajoute un
|
||||
* bouton fixe present sur chaque ecran : au clic, il pose la classe .dys-font sur
|
||||
* <html> (qui redefinit --font-family-base, applique a tout le texte) et persiste
|
||||
* le choix dans localStorage pour le conserver d'un ecran a l'autre.
|
||||
*
|
||||
* CSP 'self' : aucun script inline, aucun handler inline. Le DOM est construit par
|
||||
* l'API (createElement/textContent). Les fonctions sont exportees pour etre testees
|
||||
* sans navigateur (jsdom) ; l'auto-init au chargement est gardee pour ne pas
|
||||
* s'executer a l'import en environnement de test (document absent a ce moment-la).
|
||||
*/
|
||||
|
||||
export const STORAGE_KEY = 'wakdo_dyslexia_font';
|
||||
export const ROOT_CLASS = 'dys-font';
|
||||
const TOGGLE_SELECTOR = '[data-a11y-dys-toggle]';
|
||||
|
||||
/**
|
||||
* Lit la preference persistee. Tolere l'absence de storage (mode prive, quota) :
|
||||
* toute erreur d'acces renvoie false (police de base par defaut).
|
||||
* @param {Storage|null} storage
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isDyslexiaEnabled(storage) {
|
||||
try {
|
||||
return storage != null && storage.getItem(STORAGE_KEY) === '1';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique (ou retire) la classe .dys-font sur l'element racine fourni.
|
||||
* @param {boolean} enabled
|
||||
* @param {HTMLElement} root typiquement document.documentElement
|
||||
*/
|
||||
export function applyDyslexiaPreference(enabled, root) {
|
||||
if (root && root.classList) {
|
||||
root.classList.toggle(ROOT_CLASS, Boolean(enabled));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persiste la preference. Silencieux si le storage est indisponible.
|
||||
* @param {boolean} enabled
|
||||
* @param {Storage|null} storage
|
||||
*/
|
||||
export function persistDyslexiaPreference(enabled, storage) {
|
||||
try {
|
||||
if (storage != null) {
|
||||
storage.setItem(STORAGE_KEY, enabled ? '1' : '0');
|
||||
}
|
||||
} catch {
|
||||
/* storage indisponible : la preference reste valable pour la session en cours */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit le bouton de bascule (aria-pressed reflete l'etat). `onToggle` est
|
||||
* appele au clic avec le nouvel etat booleen ; l'appelant persiste et applique.
|
||||
* @param {boolean} initialEnabled
|
||||
* @param {(next: boolean) => void} onToggle
|
||||
* @returns {HTMLButtonElement}
|
||||
*/
|
||||
export function buildDyslexiaToggle(initialEnabled, onToggle) {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'a11y-toggle';
|
||||
btn.setAttribute('data-a11y-dys-toggle', '');
|
||||
btn.setAttribute('aria-pressed', initialEnabled ? 'true' : 'false');
|
||||
btn.setAttribute('aria-label', 'Activer la police adaptee aux personnes dyslexiques');
|
||||
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'a11y-toggle__icon';
|
||||
icon.setAttribute('aria-hidden', 'true');
|
||||
icon.textContent = 'Aa';
|
||||
btn.appendChild(icon);
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.className = 'a11y-toggle__label';
|
||||
label.textContent = 'Police adaptee';
|
||||
btn.appendChild(label);
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
const next = btn.getAttribute('aria-pressed') !== 'true';
|
||||
btn.setAttribute('aria-pressed', next ? 'true' : 'false');
|
||||
if (typeof onToggle === 'function') {
|
||||
onToggle(next);
|
||||
}
|
||||
});
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise la bascule : applique la preference persistee, puis injecte le bouton
|
||||
* dans le conteneur (idempotent : ne reinjecte pas si un bouton existe deja).
|
||||
* @param {{storage?: Storage|null, root?: HTMLElement, container?: HTMLElement}} [options]
|
||||
* @returns {HTMLButtonElement|null} le bouton injecte, ou null si deja present
|
||||
*/
|
||||
export function initDyslexiaToggle(options = {}) {
|
||||
const storage = options.storage ?? (typeof window !== 'undefined' ? window.localStorage : null);
|
||||
const root = options.root ?? (typeof document !== 'undefined' ? document.documentElement : null);
|
||||
const container = options.container ?? (typeof document !== 'undefined' ? document.body : null);
|
||||
|
||||
const enabled = isDyslexiaEnabled(storage);
|
||||
applyDyslexiaPreference(enabled, root);
|
||||
|
||||
if (!container || container.querySelector(TOGGLE_SELECTOR)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const btn = buildDyslexiaToggle(enabled, (next) => {
|
||||
applyDyslexiaPreference(next, root);
|
||||
persistDyslexiaPreference(next, storage);
|
||||
});
|
||||
|
||||
container.appendChild(btn);
|
||||
return btn;
|
||||
}
|
||||
|
||||
/* Auto-init au chargement. Gardee : a l'import en test (document absent), ne
|
||||
s'enregistre pas ; en navigateur, s'execute une fois le DOM pret. */
|
||||
if (typeof document !== 'undefined') {
|
||||
document.addEventListener('DOMContentLoaded', () => initDyslexiaToggle());
|
||||
}
|
||||
|
|
@ -95,5 +95,6 @@
|
|||
|
||||
<script type="module" src="assets/js/nav.js"></script>
|
||||
<script type="module" src="assets/js/page-cart.js"></script>
|
||||
<script type="module" src="assets/js/a11y.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -175,5 +175,6 @@
|
|||
</nav>
|
||||
</main>
|
||||
|
||||
<script type="module" src="assets/js/a11y.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -65,5 +65,6 @@
|
|||
</main>
|
||||
|
||||
<script type="module" src="assets/js/page-confirmation.js"></script>
|
||||
<script type="module" src="assets/js/a11y.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -77,5 +77,6 @@
|
|||
|
||||
</main>
|
||||
|
||||
<script type="module" src="assets/js/a11y.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -87,5 +87,6 @@
|
|||
|
||||
<script type="module" src="assets/js/page-payment.js"></script>
|
||||
<script type="module" src="assets/js/nav.js"></script>
|
||||
<script type="module" src="assets/js/a11y.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -66,5 +66,6 @@
|
|||
<script type="module" src="assets/js/nav.js"></script>
|
||||
<script type="module" src="assets/js/page-product.js"></script>
|
||||
<script type="module" src="assets/js/order-panel.js"></script>
|
||||
<script type="module" src="assets/js/a11y.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -72,5 +72,6 @@
|
|||
<script type="module" src="assets/js/page-products.js"></script>
|
||||
<script type="module" src="assets/js/category-strip.js"></script>
|
||||
<script type="module" src="assets/js/order-panel.js"></script>
|
||||
<script type="module" src="assets/js/a11y.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
106
tests/js/a11y.test.js
Normal file
106
tests/js/a11y.test.js
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Tests du module a11y du front borne (node:test + jsdom).
|
||||
*
|
||||
* Couvre la bascule de police adaptee aux dyslexiques (RGAA Cr 1.c.2) : lecture de
|
||||
* la preference persistee, application de la classe .dys-font sur la racine,
|
||||
* injection du bouton (idempotente, aria-pressed reflete l'etat), et le cycle de
|
||||
* clic (flip de l'etat + persistance). DOM simule par jsdom : aucun navigateur requis.
|
||||
*/
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
import {
|
||||
STORAGE_KEY,
|
||||
ROOT_CLASS,
|
||||
isDyslexiaEnabled,
|
||||
applyDyslexiaPreference,
|
||||
initDyslexiaToggle,
|
||||
} from '../../src/public/borne/assets/js/a11y.js';
|
||||
|
||||
function setupDom() {
|
||||
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
|
||||
global.window = dom.window;
|
||||
global.document = dom.window.document;
|
||||
return dom;
|
||||
}
|
||||
|
||||
/** Storage en memoire, contrat compatible localStorage. */
|
||||
function fakeStorage(initial = {}) {
|
||||
const m = new Map(Object.entries(initial));
|
||||
return {
|
||||
getItem: (k) => (m.has(k) ? m.get(k) : null),
|
||||
setItem: (k, v) => { m.set(k, String(v)); },
|
||||
removeItem: (k) => { m.delete(k); },
|
||||
_dump: () => Object.fromEntries(m),
|
||||
};
|
||||
}
|
||||
|
||||
test('isDyslexiaEnabled : vrai seulement si la cle vaut "1"', () => {
|
||||
assert.equal(isDyslexiaEnabled(fakeStorage({ [STORAGE_KEY]: '1' })), true);
|
||||
assert.equal(isDyslexiaEnabled(fakeStorage({ [STORAGE_KEY]: '0' })), false);
|
||||
assert.equal(isDyslexiaEnabled(fakeStorage()), false);
|
||||
assert.equal(isDyslexiaEnabled(null), false);
|
||||
});
|
||||
|
||||
test('isDyslexiaEnabled : un storage qui jette renvoie false (mode prive)', () => {
|
||||
const throwing = { getItem() { throw new Error('denied'); } };
|
||||
assert.equal(isDyslexiaEnabled(throwing), false);
|
||||
});
|
||||
|
||||
test('applyDyslexiaPreference : ajoute/retire la classe sur la racine', () => {
|
||||
setupDom();
|
||||
const root = document.documentElement;
|
||||
applyDyslexiaPreference(true, root);
|
||||
assert.ok(root.classList.contains(ROOT_CLASS));
|
||||
applyDyslexiaPreference(false, root);
|
||||
assert.ok(!root.classList.contains(ROOT_CLASS));
|
||||
});
|
||||
|
||||
test('initDyslexiaToggle : applique la preference persistee et reflete aria-pressed', () => {
|
||||
setupDom();
|
||||
const storage = fakeStorage({ [STORAGE_KEY]: '1' });
|
||||
const btn = initDyslexiaToggle({ storage, root: document.documentElement, container: document.body });
|
||||
|
||||
assert.ok(btn, 'un bouton est injecte');
|
||||
assert.equal(btn.getAttribute('aria-pressed'), 'true');
|
||||
assert.ok(document.documentElement.classList.contains(ROOT_CLASS), 'classe appliquee au chargement');
|
||||
assert.ok(btn.getAttribute('aria-label'));
|
||||
});
|
||||
|
||||
test('initDyslexiaToggle : defaut = police de base (pas de preference)', () => {
|
||||
setupDom();
|
||||
const storage = fakeStorage();
|
||||
const btn = initDyslexiaToggle({ storage, root: document.documentElement, container: document.body });
|
||||
|
||||
assert.equal(btn.getAttribute('aria-pressed'), 'false');
|
||||
assert.ok(!document.documentElement.classList.contains(ROOT_CLASS));
|
||||
});
|
||||
|
||||
test('initDyslexiaToggle : idempotent (pas de doublon de bouton)', () => {
|
||||
setupDom();
|
||||
const storage = fakeStorage();
|
||||
initDyslexiaToggle({ storage, root: document.documentElement, container: document.body });
|
||||
const second = initDyslexiaToggle({ storage, root: document.documentElement, container: document.body });
|
||||
|
||||
assert.equal(second, null, 'le second appel ne reinjecte pas');
|
||||
assert.equal(document.querySelectorAll('[data-a11y-dys-toggle]').length, 1);
|
||||
});
|
||||
|
||||
test('clic sur le bouton : bascule l etat, la classe et persiste', () => {
|
||||
setupDom();
|
||||
const storage = fakeStorage();
|
||||
const btn = initDyslexiaToggle({ storage, root: document.documentElement, container: document.body });
|
||||
|
||||
// Activation.
|
||||
btn.click();
|
||||
assert.equal(btn.getAttribute('aria-pressed'), 'true');
|
||||
assert.ok(document.documentElement.classList.contains(ROOT_CLASS));
|
||||
assert.equal(storage.getItem(STORAGE_KEY), '1');
|
||||
|
||||
// Desactivation.
|
||||
btn.click();
|
||||
assert.equal(btn.getAttribute('aria-pressed'), 'false');
|
||||
assert.ok(!document.documentElement.classList.contains(ROOT_CLASS));
|
||||
assert.equal(storage.getItem(STORAGE_KEY), '0');
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue