[IMP] card and priority editor: red circled cross, number weight field
A tag's « Retirer » text button read like plain text, and the weight of a criterion took a whole column for a number from 1 to 100. The card now removes a tag with a red circled cross, shown in write mode only, named « Retirer « tag » » for screen readers, with a 24 px target; the red has its own token, exempted from the conflict-hue rule and limited to card surfaces. The weight is a narrow number field, still read and refused as before. Checked: node 4597, navigateur 965; captures 04a and 10a republished. --- FR --- [IMP] fiche et priorités : croix rouge cerclée, poids en champ nombre Le bouton texte « Retirer » d'une étiquette se lisait comme du texte, et le poids d'un critère prenait toute une colonne pour un nombre de 1 à 100. La fiche retire désormais une étiquette par une croix rouge cerclée, en mode écriture seulement, nommée « Retirer « étiquette » » pour les lecteurs d'écran, sur une cible de 24 px ; le rouge a son jeton, exempté de la règle des teintes de conflit et borné aux surfaces de la fiche. Le poids est un champ nombre étroit, lu et refusé comme avant. Vérifié : node 4597, navigateur 965 ; captures 04a et 10a republiées. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
883dcb2519
commit
426eb911c9
26 changed files with 344 additions and 96 deletions
|
|
@ -148,8 +148,8 @@ nulle part.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
*Chaque étiquette est une pastille grise, suivie de sa commande « Retirer ».
|
*Chaque étiquette est une pastille grise ; en écriture, une petite croix
|
||||||
Tapez une ou plusieurs étiquettes séparées par une virgule, puis « Ajouter »
|
rouge cerclée la suit, qui la retire. Tapez une ou plusieurs étiquettes séparées par une virgule, puis « Ajouter »
|
||||||
ou Entrée. Ici, « FINANCE » s’enregistre sous « Finance », l’orthographe que
|
ou Entrée. Ici, « FINANCE » s’enregistre sous « Finance », l’orthographe que
|
||||||
l’événement porte déjà, et le logiciel le dit sous le champ : une même
|
l’événement porte déjà, et le logiciel le dit sous le champ : une même
|
||||||
étiquette ne s’écrit jamais de deux façons. La légende, au-dessus de la
|
étiquette ne s’écrit jamais de deux façons. La légende, au-dessus de la
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ sur les captures.
|
||||||
| `--couleur-pastille-texte` | texte | 4,5 | — | — | 14,51 | 19-ajustement |
|
| `--couleur-pastille-texte` | texte | 4,5 | — | — | 14,51 | 19-ajustement |
|
||||||
| `--couleur-poignee` | marque | 3 | 7,44 | 07-plan-tables | 11,32 | 07-plan-tables |
|
| `--couleur-poignee` | marque | 3 | 7,44 | 07-plan-tables | 11,32 | 07-plan-tables |
|
||||||
| `--couleur-refus` | texte | 4,5 | 8,34 | 01b-passer-en-ecriture | 8,14 | 01b-passer-en-ecriture |
|
| `--couleur-refus` | texte | 4,5 | 8,34 | 01b-passer-en-ecriture | 8,14 | 01b-passer-en-ecriture |
|
||||||
|
| `--couleur-retrait` | marque | 3 | 5,62 | 04a-etiquettes | 5,96 | 04a-etiquettes |
|
||||||
| `--couleur-selection` | marque | 3 | 4,57 | 08-animateurs-designes | 4,18 | 08-animateurs-designes |
|
| `--couleur-selection` | marque | 3 | 4,57 | 08-animateurs-designes | 4,18 | 08-animateurs-designes |
|
||||||
| `--couleur-table-trait` | marque | 3 | 7,44 | 07-plan-tables | 5,59 | 07-plan-tables |
|
| `--couleur-table-trait` | marque | 3 | 7,44 | 07-plan-tables | 5,59 | 07-plan-tables |
|
||||||
| `rgb(24, 33, 47)` | texte | 4,5 | — | — | 16,19 | 22-plan-brouillon |
|
| `rgb(24, 33, 47)` | texte | 4,5 | — | — | 16,19 | 22-plan-brouillon |
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
File diff suppressed because one or more lines are too long
15
spec.md
15
spec.md
|
|
@ -2751,9 +2751,10 @@ l'appartenance s'affiche en pastille de sa teinte (§ 12.7), le titre pressenti
|
||||||
et l'exclusion en pastilles à icône.
|
et l'exclusion en pastilles à icône.
|
||||||
|
|
||||||
La fiche d'une personne porte ses étiquettes, en pastilles **neutres** — la
|
La fiche d'une personne porte ses étiquettes, en pastilles **neutres** — la
|
||||||
teinte d'appartenance ne sert qu'à l'appartenance (§ 12.7) —, chacune avec sa
|
teinte d'appartenance ne sert qu'à l'appartenance (§ 12.7) —, chacune suivie,
|
||||||
commande « Retirer », et un champ qui en ajoute une ou plusieurs, séparées comme
|
en mode écriture seulement, d'une petite croix cerclée rouge qui la retire,
|
||||||
dans une cellule CSV ; la saisie passe par la même lecture que l'import (§ 4.5,
|
nommée « Retirer « étiquette » » pour les technologies d'assistance, et un
|
||||||
|
champ qui en ajoute une ou plusieurs, séparées comme dans une cellule CSV ; la saisie passe par la même lecture que l'import (§ 4.5,
|
||||||
§ 10.1). Quand l'événement porte des étiquettes, une légende des étiquettes suit
|
§ 10.1). Quand l'événement porte des étiquettes, une légende des étiquettes suit
|
||||||
celle des appartenances, chacune avec le nombre de personnes qui la portent,
|
celle des appartenances, chacune avec le nombre de personnes qui la portent,
|
||||||
exclues comprises.
|
exclues comprises.
|
||||||
|
|
@ -3622,6 +3623,9 @@ Une page de statistiques se casse par ses bords, et chacun se produira.
|
||||||
le plan de salle, et ne servent à rien d'autre ; chacune ne peint que les
|
le plan de salle, et ne servent à rien d'autre ; chacune ne peint que les
|
||||||
marques de sa règle. Elles se distinguent entre elles et des autres marques
|
marques de sa règle. Elles se distinguent entre elles et des autres marques
|
||||||
du plan **une fois les trois dichromaties simulées**.
|
du plan **une fois les trois dichromaties simulées**.
|
||||||
|
- **Le rouge du retrait** (`--couleur-retrait`) ne peint que la croix cerclée
|
||||||
|
d'une commande qui retire, sur une carte, jamais sur le plan : c'est le seul
|
||||||
|
jeton chromatique admis à moins de 30° d'une teinte de conflit.
|
||||||
- **Les couleurs d'appartenance ne vont que sur les listes**, jamais sur le
|
- **Les couleurs d'appartenance ne vont que sur les listes**, jamais sur le
|
||||||
plan, dont le canal chromatique appartient aux conflits : au plus six teintes
|
plan, dont le canal chromatique appartient aux conflits : au plus six teintes
|
||||||
catégorielles, chacune à 30° au moins des teintes de conflit dans chaque
|
catégorielles, chacune à 30° au moins des teintes de conflit dans chaque
|
||||||
|
|
@ -4023,7 +4027,7 @@ des fonctions pures du module de géométrie.
|
||||||
| tenir hors ligne | se compile en JavaScript ordinaire ; Vite est une dépendance de développement qui ne survit pas à la construction |
|
| tenir hors ligne | se compile en JavaScript ordinaire ; Vite est une dépendance de développement qui ne survit pas à la construction |
|
||||||
| rester rapide | compile des mises à jour fines, sans arbre virtuel ni mémoïsation à poser à la main — à une exception près : la table d'évaluation des propositions, gardée dans le modèle pur sous l'identité de la charge figée de la séance, qu'une écriture remplace |
|
| rester rapide | compile des mises à jour fines, sans arbre virtuel ni mémoïsation à poser à la main — à une exception près : la table d'évaluation des propositions, gardée dans le modèle pur sous l'identité de la charge figée de la séance, qu'une écriture remplace |
|
||||||
| se tester sans navigateur | n'impose d'écrire aucun calcul dans un composant ; le composant ne contient que le câblage |
|
| se tester sans navigateur | n'impose d'écrire aucun calcul dans un composant ; le composant ne contient que le câblage |
|
||||||
| être joli sans y passer des semaines | un fichier de jetons — couleurs, rayons, ombres, échelle typographique, cible principale, largeur de page — décliné en deux thèmes ; ses familles de couleur : la page, le bandeau, la carte, l'action principale, les quatre états du plan, le neutre, les appartenances, la bande de mode, les messages (avis, refus, information), le plan, le papier |
|
| être joli sans y passer des semaines | un fichier de jetons — couleurs, rayons, ombres, échelle typographique, cible principale, largeur de page — décliné en deux thèmes ; ses familles de couleur : la page, le bandeau, la carte, l'action principale, les quatre états du plan, le neutre, les appartenances, la bande de mode, les messages (avis, refus, information), le retrait, le plan, le papier |
|
||||||
|
|
||||||
**Les candidates écartées.** Vue 3 est le second choix défendable, écarté de peu
|
**Les candidates écartées.** Vue 3 est le second choix défendable, écarté de peu
|
||||||
sur le poids et sur la discipline qu'exige sa réactivité. React est écarté sur la
|
sur le poids et sur la discipline qu'exige sa réactivité. React est écarté sur la
|
||||||
|
|
@ -4243,7 +4247,8 @@ l'étage intermédiaire et laisse partir les défauts de coordonnées.
|
||||||
### 14.5 Accessibilité et thèmes
|
### 14.5 Accessibilité et thèmes
|
||||||
|
|
||||||
Le texte tient **4,5:1** de contraste, les marques porteuses d'information
|
Le texte tient **4,5:1** de contraste, les marques porteuses d'information
|
||||||
**3:1**. Chaque couleur est accompagnée du ratio **mesuré sur la couleur relue du
|
**3:1**. Une commande sans texte, à icône seule, offre une cible de 24 × 24 px
|
||||||
|
au moins et porte son libellé en nom accessible. Chaque couleur est accompagnée du ratio **mesuré sur la couleur relue du
|
||||||
rendu**, non sur le jeton source. Les deux thèmes sont éprouvés **par rendu**,
|
rendu**, non sur le jeton source. Les deux thèmes sont éprouvés **par rendu**,
|
||||||
jamais seulement par calcul, et le tableau des ratios est **engendré pour les
|
jamais seulement par calcul, et le tableau des ratios est **engendré pour les
|
||||||
deux** à partir des styles relevés au pilotage (§ 19.5), jamais recopié. Le
|
deux** à partir des styles relevés au pilotage (§ 19.5), jamais recopié. Le
|
||||||
|
|
|
||||||
|
|
@ -543,6 +543,10 @@ function lirePaliers(paliers) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Le plus grand poids d'un critère (§ 5.7), celui du moteur : l'écran en
|
||||||
|
* borne le champ du poids, que changerPriorites contrôle. */
|
||||||
|
export { POIDS_MAX };
|
||||||
|
|
||||||
// Vrai pour un poids du domaine : un entier de 1 à POIDS_MAX (§ 5.7).
|
// Vrai pour un poids du domaine : un entier de 1 à POIDS_MAX (§ 5.7).
|
||||||
const poidsAdmis = (poids) => Number.isInteger(poids) && poids >= 1 && poids <= POIDS_MAX;
|
const poidsAdmis = (poids) => Number.isInteger(poids) && poids >= 1 && poids <= POIDS_MAX;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,14 @@
|
||||||
// contre la liste, avec son remède, et décrit le groupe de l'éditeur
|
// contre la liste, avec son remède, et décrit le groupe de l'éditeur
|
||||||
// (aria-describedby), focalisable par programme (tabindex -1) : après le
|
// (aria-describedby), focalisable par programme (tabindex -1) : après le
|
||||||
// remède, le focus revient au groupe ; rien ne s'écrit. Le poids d'un
|
// remède, le focus revient au groupe ; rien ne s'écrit. Le poids d'un
|
||||||
// critère d'un palier partagé se saisit dans un Champ entier (poids.<nom>),
|
// critère d'un palier partagé se saisit dans un Champ nombre, étroit, de
|
||||||
// écrit à la sortie du champ par changerPriorites ; son refus, un poids
|
// 1 à POIDS_MAX par pas de 1 (poids.<nom>), lu par lireEntier et écrit à
|
||||||
// hors de son domaine compris, se pose contre le champ.
|
// la sortie du champ par changerPriorites ; son refus, un poids hors de
|
||||||
|
// son domaine compris, se pose contre le champ : les bornes de l'entrée
|
||||||
|
// guident ses flèches et ne refusent rien.
|
||||||
import { getContext, tick } from 'svelte';
|
import { getContext, tick } from 'svelte';
|
||||||
import { lireEntier } from '../../application/capacite.js';
|
import { lireEntier } from '../../application/capacite.js';
|
||||||
|
import { POIDS_MAX } from '../../application/commandes_mobilier.js';
|
||||||
import Champ from '../primitives/Champ.svelte';
|
import Champ from '../primitives/Champ.svelte';
|
||||||
import Commande from '../primitives/Commande.svelte';
|
import Commande from '../primitives/Commande.svelte';
|
||||||
import Refus from '../primitives/Refus.svelte';
|
import Refus from '../primitives/Refus.svelte';
|
||||||
|
|
@ -134,6 +137,10 @@
|
||||||
valeur={poids}
|
valeur={poids}
|
||||||
lire={lireEntier}
|
lire={lireEntier}
|
||||||
executer={ecrirePoids(critere)}
|
executer={ecrirePoids(critere)}
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={POIDS_MAX}
|
||||||
|
pas={1}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="gestes">
|
<div class="gestes">
|
||||||
|
|
|
||||||
|
|
@ -392,11 +392,22 @@ describe('l’ordre des priorités : l’éditeur (§ 5.7, A11, A12)', () => {
|
||||||
expect(annonce(m)).toBe(t('generation.priorites.annonce', { critere: CUMULEES, palier: 3, paliers: 7, avec: [] }));
|
expect(annonce(m)).toBe(t('generation.priorites.annonce', { critere: CUMULEES, palier: 3, paliers: 7, avec: [] }));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('le poids d’un critère d’un palier partagé : 0 refusé contre son champ, rien d’écrit ; 5 écrit, une entrée', async () => {
|
test('le poids d’un critère d’un palier partagé : un champ nombre étroit de 1 à 100 ; 0 refusé contre son champ, rien d’écrit ; 5 écrit, une entrée', async () => {
|
||||||
const m = await demonstration();
|
const m = await demonstration();
|
||||||
await cliquer(m, bouton(m, REDONDANCE, 'joindrePalier'));
|
await cliquer(m, bouton(m, REDONDANCE, 'joindrePalier'));
|
||||||
const champ = () => editeur(m).querySelector(`input[data-champ="poids.${REDONDANCE}"]`);
|
const champ = () => editeur(m).querySelector(`input[data-champ="poids.${REDONDANCE}"]`);
|
||||||
expect(editeur(m).querySelector(`label[for="${champ().id}"]`).textContent.trim()).toBe(t('generation.priorites.poids', { critere: REDONDANCE }));
|
expect(editeur(m).querySelector(`label[for="${champ().id}"]`).textContent.trim()).toBe(t('generation.priorites.poids', { critere: REDONDANCE }));
|
||||||
|
expect([champ().type, champ().min, champ().max, champ().step]).toEqual(['number', '1', '100', '1']);
|
||||||
|
// Étroit : six caractères de large, le contenu de la boîte, flèches
|
||||||
|
// comprises.
|
||||||
|
const sonde = document.createElement('span');
|
||||||
|
sonde.style.cssText = 'position:absolute;font:inherit;visibility:hidden';
|
||||||
|
sonde.textContent = '0'.repeat(6);
|
||||||
|
champ().parentElement.append(sonde);
|
||||||
|
const sixCaracteres = sonde.getBoundingClientRect().width;
|
||||||
|
sonde.remove();
|
||||||
|
const style = getComputedStyle(champ());
|
||||||
|
expect(Number.parseFloat(style.width)).toBeCloseTo(sixCaracteres, 0);
|
||||||
const e0 = journal(m);
|
const e0 = journal(m);
|
||||||
await cliquer(m, champ());
|
await cliquer(m, champ());
|
||||||
champ().select();
|
champ().select();
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,18 @@
|
||||||
appartenances), --couleur-conflit-voisins (nouveaux voisins) et
|
appartenances), --couleur-conflit-voisins (nouveaux voisins) et
|
||||||
--couleur-conflit-table (nouvelle table). Chacune ne peint que la trame de
|
--couleur-conflit-table (nouvelle table). Chacune ne peint que la trame de
|
||||||
sa règle ; tout autre jeton chromatique se tient à 30° au moins de chacune,
|
sa règle ; tout autre jeton chromatique se tient à 30° au moins de chacune,
|
||||||
et aucun ne porte la valeur de l'une. Elles se distinguent entre elles et
|
sauf le rouge du retrait, et aucun ne porte la valeur de l'une. Elles se
|
||||||
des autres marques du plan sous les trois dichromaties (SEPARATIONS,
|
distinguent entre elles et des autres marques du plan sous les trois
|
||||||
marques.js), et tiennent 3:1 sur le fond du plateau, qui borde leurs
|
dichromaties (SEPARATIONS, marques.js), et tiennent 3:1 sur le fond du
|
||||||
traits. Le contour épaissi d'une table en conflit, commun aux trois, est
|
plateau, qui borde leurs traits. Le contour épaissi d'une table en
|
||||||
de l'encre forte du plan, --couleur-contour-fort.
|
conflit, commun aux trois, est de l'encre forte du plan,
|
||||||
|
--couleur-contour-fort.
|
||||||
|
|
||||||
|
Le rouge du retrait, --couleur-retrait, peint la croix cerclée d'une
|
||||||
|
commande qui retire (Commande, variante retrait), sur la carte et son
|
||||||
|
fond sourd, jamais sur le plan : c'est le seul jeton chromatique admis à
|
||||||
|
moins de 30° d'une teinte de conflit (JETON_RETRAIT, marques.js), le
|
||||||
|
canal chromatique que les conflits se réservent étant celui du plan.
|
||||||
|
|
||||||
La hachure d'une place à pourvoir peint des traits --couleur-hachure sur le
|
La hachure d'une place à pourvoir peint des traits --couleur-hachure sur le
|
||||||
fond de la chaise, --couleur-chaise-fond : ce sont ses deux couleurs.
|
fond de la chaise, --couleur-chaise-fond : ce sont ses deux couleurs.
|
||||||
|
|
@ -72,7 +79,8 @@
|
||||||
le neutre et les appartenances, chacun un fond teinté et une marque, que
|
le neutre et les appartenances, chacun un fond teinté et une marque, que
|
||||||
lisent les pastilles ; la bande de mode, un fond, un filet et une
|
lisent les pastilles ; la bande de mode, un fond, un filet et une
|
||||||
teinte par mode, celle de son icône ; les messages — avis, refus,
|
teinte par mode, celle de son icône ; les messages — avis, refus,
|
||||||
information —, chacun son encre, un fond, un filet et une marque.
|
information —, chacun son encre, un fond, un filet et une marque ; le
|
||||||
|
retrait, la teinte de sa croix.
|
||||||
|
|
||||||
Le texte d'une pastille ou de la bande de mode garde l'encre de la page
|
Le texte d'une pastille ou de la bande de mode garde l'encre de la page
|
||||||
(§ 12.7) : la teinte se porte par le fond et la marque. Les marques des
|
(§ 12.7) : la teinte se porte par le fond et la marque. Les marques des
|
||||||
|
|
@ -134,9 +142,11 @@ textarea {
|
||||||
--rayon-3: 0.75rem;
|
--rayon-3: 0.75rem;
|
||||||
--rayon-pilule: 62.5rem;
|
--rayon-pilule: 62.5rem;
|
||||||
|
|
||||||
/* La hauteur d'une cible principale (44 px), et la largeur au-delà de
|
/* La hauteur d'une cible principale (44 px), le côté minimal d'une cible
|
||||||
laquelle le contenu d'un écran se centre. */
|
(24 px, § 14.5), et la largeur au-delà de laquelle le contenu d'un
|
||||||
|
écran se centre. */
|
||||||
--hauteur-cible: 2.75rem;
|
--hauteur-cible: 2.75rem;
|
||||||
|
--cible-minimale: 1.5rem;
|
||||||
--largeur-page: 87rem;
|
--largeur-page: 87rem;
|
||||||
|
|
||||||
/* Ombres : une couleur opaque par thème, --couleur-ombre ; celle d'une
|
/* Ombres : une couleur opaque par thème, --couleur-ombre ; celle d'une
|
||||||
|
|
@ -229,6 +239,9 @@ textarea {
|
||||||
--couleur-information-filet: #b7cef0;
|
--couleur-information-filet: #b7cef0;
|
||||||
--couleur-information-marque: var(--couleur-information);
|
--couleur-information-marque: var(--couleur-information);
|
||||||
|
|
||||||
|
/* Retrait : la croix cerclée d'une commande qui retire. */
|
||||||
|
--couleur-retrait: #c62828;
|
||||||
|
|
||||||
/* Désignation. Le focus, la sélection, la mise en évidence et le fantôme
|
/* Désignation. Le focus, la sélection, la mise en évidence et le fantôme
|
||||||
passent sur une chaise vide comme sur une chaise pleine : leur clarté se
|
passent sur une chaise vide comme sur une chaise pleine : leur clarté se
|
||||||
tient entre le fond et le plein de la chaise, à 3:1 de chacun. */
|
tient entre le fond et le plein de la chaise, à 3:1 de chacun. */
|
||||||
|
|
@ -334,6 +347,8 @@ textarea {
|
||||||
--couleur-information-filet: #34507a;
|
--couleur-information-filet: #34507a;
|
||||||
--couleur-information-marque: var(--couleur-information);
|
--couleur-information-marque: var(--couleur-information);
|
||||||
|
|
||||||
|
--couleur-retrait: #ff6b6b;
|
||||||
|
|
||||||
--couleur-focus: #8764e1;
|
--couleur-focus: #8764e1;
|
||||||
--couleur-selection: #4070f0;
|
--couleur-selection: #4070f0;
|
||||||
--couleur-mise-en-evidence: #00897b;
|
--couleur-mise-en-evidence: #00897b;
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,14 @@ export const CONFLITS = Object.freeze(
|
||||||
/** Les jetons des teintes de conflit, dans l'ordre de CONFLITS. */
|
/** Les jetons des teintes de conflit, dans l'ordre de CONFLITS. */
|
||||||
export const JETONS_CONFLIT = Object.freeze(CONFLITS.map(({ jeton }) => jeton));
|
export const JETONS_CONFLIT = Object.freeze(CONFLITS.map(({ jeton }) => jeton));
|
||||||
|
|
||||||
|
/** Le jeton du rouge du retrait (§ 12.7) : la croix cerclée d'une commande
|
||||||
|
* qui retire (Commande, variante retrait), sur la carte et son fond sourd,
|
||||||
|
* jamais sur le plan. C'est le seul jeton chromatique admis à moins de 30°
|
||||||
|
* d'une teinte de conflit (verifierTeintesReservees, palette.js) : le
|
||||||
|
* canal chromatique que les conflits se réservent est celui du plan, où il
|
||||||
|
* ne se pose pas. */
|
||||||
|
export const JETON_RETRAIT = c('retrait');
|
||||||
|
|
||||||
/** Écarts perçus minimaux d'une teinte de conflit, en distance OKLab × 100
|
/** Écarts perçus minimaux d'une teinte de conflit, en distance OKLab × 100
|
||||||
* mesurée en couleurs d'origine et sous chaque dichromatie (ecartPercu,
|
* mesurée en couleurs d'origine et sous chaque dichromatie (ecartPercu,
|
||||||
* palette.js) : entreConflits, d'une teinte de conflit à une autre — la
|
* palette.js) : entreConflits, d'une teinte de conflit à une autre — la
|
||||||
|
|
@ -152,6 +160,9 @@ export const MARQUES = Object.freeze([
|
||||||
// contenu d'une feuille imprimée, mesuré sur le papier, clair dans les
|
// contenu d'une feuille imprimée, mesuré sur le papier, clair dans les
|
||||||
// deux thèmes.
|
// deux thèmes.
|
||||||
marque('filigrane', 'filigrane', ['papier']),
|
marque('filigrane', 'filigrane', ['papier']),
|
||||||
|
// La croix cerclée d'un retrait, seul contenu de son bouton : sur la
|
||||||
|
// carte, et sur son fond sourd, celui du survol.
|
||||||
|
marque('retrait', 'retrait', ['carte-fond', 'carte-fond-sourd']),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Les fonds teintés d'une pastille : ceux de ses teintes, puis ceux des
|
// Les fonds teintés d'une pastille : ceux de ses teintes, puis ceux des
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ const NOMS_MARQUES = [
|
||||||
'cadenasFerme', 'cadenasOuvert', 'anneau', 'hachure', 'remplissagePlein',
|
'cadenasFerme', 'cadenasOuvert', 'anneau', 'hachure', 'remplissagePlein',
|
||||||
'contourConflit', 'trameAppartenance', 'trameVoisins', 'trameTable', 'pastille', 'poignee', 'gel', 'miseEnEvidence',
|
'contourConflit', 'trameAppartenance', 'trameVoisins', 'trameTable', 'pastille', 'poignee', 'gel', 'miseEnEvidence',
|
||||||
'selection', 'focus',
|
'selection', 'focus',
|
||||||
'contourTable', 'contourChaise', 'echelle', 'fantome', 'chargement', 'filigrane',
|
'contourTable', 'contourChaise', 'echelle', 'fantome', 'chargement', 'filigrane', 'retrait',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Les remplissages d'une chaise : fond, pleine, et les deux couleurs de la
|
// Les remplissages d'une chaise : fond, pleine, et les deux couleurs de la
|
||||||
|
|
@ -39,7 +39,7 @@ const FONDS_REQUIS = {
|
||||||
const parNom = (registre, nom) => registre.find((e) => e.nom === nom);
|
const parNom = (registre, nom) => registre.find((e) => e.nom === nom);
|
||||||
|
|
||||||
describe('registre des marques', () => {
|
describe('registre des marques', () => {
|
||||||
test('les vingt et une marques du contrat, noms uniques, seuil 3, au moins un fond chacune', () => {
|
test('les vingt-deux marques du contrat, noms uniques, seuil 3, au moins un fond chacune', () => {
|
||||||
assert.equal(MARQUES.length, NOMS_MARQUES.length);
|
assert.equal(MARQUES.length, NOMS_MARQUES.length);
|
||||||
assert.deepEqual(MARQUES.map((m) => m.nom), NOMS_MARQUES);
|
assert.deepEqual(MARQUES.map((m) => m.nom), NOMS_MARQUES);
|
||||||
assert.equal(new Set(MARQUES.map((m) => m.nom)).size, MARQUES.length);
|
assert.equal(new Set(MARQUES.map((m) => m.nom)).size, MARQUES.length);
|
||||||
|
|
|
||||||
|
|
@ -273,14 +273,14 @@ export function teinte(couleur) {
|
||||||
|
|
||||||
/** § 12.7 : chaque teinte réservée — celles des conflits — est chromatique,
|
/** § 12.7 : chaque teinte réservée — celles des conflits — est chromatique,
|
||||||
* et la teinte de chaque autre jeton chromatique se tient à ECART_TEINTE
|
* et la teinte de chaque autre jeton chromatique se tient à ECART_TEINTE
|
||||||
* degrés au moins de chacune, dans chaque thème. Les teintes réservées ne
|
* degrés au moins de chacune, dans chaque thème, sauf celle d'un jeton
|
||||||
* se mesurent pas entre elles : verifierSeparation les distingue. Rend les
|
* d'admis, qui ne se mesure pas. Les teintes réservées ne se mesurent pas
|
||||||
* fautes [{theme, reservee, jeton, ecart}], ecart en degrés sur le cercle,
|
* entre elles : verifierSeparation les distingue. Rend les fautes
|
||||||
* dans l'ordre thème, teinte réservée, jeton ; une teinte réservée
|
* [{theme, reservee, jeton, ecart}], ecart en degrés sur le cercle, dans
|
||||||
* achromatique est elle-même une faute, jeton égal à reservee et ecart
|
* l'ordre thème, teinte réservée, jeton ; une teinte réservée achromatique
|
||||||
* null — aucune teinte n'est alors réservée. Un jeton réservé absent d'un
|
* est elle-même une faute, jeton égal à reservee et ecart null — aucune
|
||||||
* thème lève. */
|
* teinte n'est alors réservée. Un jeton réservé absent d'un thème lève. */
|
||||||
export function verifierTeintesReservees(texteCss, reservees) {
|
export function verifierTeintesReservees(texteCss, reservees, admis = []) {
|
||||||
const fautes = [];
|
const fautes = [];
|
||||||
for (const theme of THEMES) {
|
for (const theme of THEMES) {
|
||||||
const jetons = lireJetons(texteCss, theme);
|
const jetons = lireJetons(texteCss, theme);
|
||||||
|
|
@ -292,7 +292,7 @@ export function verifierTeintesReservees(texteCss, reservees) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const [nom, couleur] of jetons) {
|
for (const [nom, couleur] of jetons) {
|
||||||
if (reservees.includes(nom)) continue;
|
if (reservees.includes(nom) || admis.includes(nom)) continue;
|
||||||
const h = teinte(couleur);
|
const h = teinte(couleur);
|
||||||
if (h === null) continue;
|
if (h === null) continue;
|
||||||
const brut = Math.abs(h - reference);
|
const brut = Math.abs(h - reference);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { readdirSync, readFileSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { describe, test } from '../../test/lanceur.js';
|
import { describe, test } from '../../test/lanceur.js';
|
||||||
import { JETONS_CONFLIT } from './marques.js';
|
import { JETONS_CONFLIT, JETON_RETRAIT, MARQUES } from './marques.js';
|
||||||
import {
|
import {
|
||||||
SIMULATIONS,
|
SIMULATIONS,
|
||||||
contraste,
|
contraste,
|
||||||
|
|
@ -320,6 +320,9 @@ describe('teinte et verifierTeintesReservees (§ 12.7)', () => {
|
||||||
assert.deepEqual(fautes.map((f) => `${f.theme}:${f.jeton}`), [
|
assert.deepEqual(fautes.map((f) => `${f.theme}:${f.jeton}`), [
|
||||||
'clair:--couleur-b', 'clair:--couleur-d', 'sombre:--couleur-b', 'sombre:--couleur-d',
|
'clair:--couleur-b', 'clair:--couleur-d', 'sombre:--couleur-b', 'sombre:--couleur-d',
|
||||||
]);
|
]);
|
||||||
|
// Un jeton admis ne se mesure pas ; les autres, si.
|
||||||
|
const admis = verifierTeintesReservees(css, ['--couleur-conflit'], ['--couleur-b']);
|
||||||
|
assert.deepEqual(admis.map((f) => `${f.theme}:${f.jeton}`), ['clair:--couleur-d', 'sombre:--couleur-d']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -496,6 +499,21 @@ describe('jetons.css', () => {
|
||||||
// Aucun autre jeton ne porte le préfixe des teintes de conflit.
|
// Aucun autre jeton ne porte le préfixe des teintes de conflit.
|
||||||
assert.deepEqual([...jetons.keys()].filter((n) => n.startsWith('--couleur-conflit')), JETONS_CONFLIT, theme);
|
assert.deepEqual([...jetons.keys()].filter((n) => n.startsWith('--couleur-conflit')), JETONS_CONFLIT, theme);
|
||||||
}
|
}
|
||||||
assert.deepEqual(verifierTeintesReservees(JETONS, JETONS_CONFLIT), []);
|
assert.deepEqual(verifierTeintesReservees(JETONS, JETONS_CONFLIT, [JETON_RETRAIT]), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('le rouge du retrait est le seul jeton admis près d’une teinte de conflit : chromatique, sa valeur à lui, peint sur la carte seulement, jamais sur le plan', () => {
|
||||||
|
assert.equal(JETON_RETRAIT, '--couleur-retrait');
|
||||||
|
// Sans l'admission, le retrait est la seule faute.
|
||||||
|
assert.deepEqual([...new Set(verifierTeintesReservees(JETONS, JETONS_CONFLIT).map((f) => f.jeton))], [JETON_RETRAIT]);
|
||||||
|
for (const theme of ['clair', 'sombre']) {
|
||||||
|
const jetons = lireJetons(JETONS, theme);
|
||||||
|
const couleur = jetons.get(JETON_RETRAIT);
|
||||||
|
assert.notEqual(teinte(couleur), null, theme);
|
||||||
|
const memeValeur = [...jetons].filter(([n, c]) => n !== JETON_RETRAIT && String(c) === String(couleur));
|
||||||
|
assert.deepEqual(memeValeur.map(([n]) => n), [], theme);
|
||||||
|
}
|
||||||
|
const peintes = MARQUES.filter((m) => m.jeton === JETON_RETRAIT || m.fonds.includes(JETON_RETRAIT));
|
||||||
|
assert.deepEqual(peintes.map((m) => [m.nom, [...m.fonds]]), [['retrait', ['--couleur-carte-fond', '--couleur-carte-fond-sourd']]]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,11 @@
|
||||||
//
|
//
|
||||||
// Les étiquettes (etiquettes.js) : une pastille neutre à icône par
|
// Les étiquettes (etiquettes.js) : une pastille neutre à icône par
|
||||||
// étiquette, dans l'ordre de la charge — la teinte d'appartenance ne sert
|
// étiquette, dans l'ordre de la charge — la teinte d'appartenance ne sert
|
||||||
// qu'à l'appartenance (§ 12.7) —, chacune suivie de sa commande
|
// qu'à l'appartenance (§ 12.7) —, chacune suivie, en écriture seulement,
|
||||||
// « Retirer », nommée par elle ; puis un formulaire d'ajout, dont le
|
// de sa commande de retrait : une croix cerclée rouge (Commande, variante
|
||||||
// champ reçoit une ou plusieurs étiquettes séparées comme dans une
|
// retrait), nommée « Retirer « <étiquette> » » ; puis un formulaire
|
||||||
// cellule CSV. Entrée, ou « Ajouter », envoie la saisie entière à
|
// d'ajout, dont le champ reçoit une ou plusieurs étiquettes séparées
|
||||||
|
// comme dans une cellule CSV. Entrée, ou « Ajouter », envoie la saisie entière à
|
||||||
// changerEtiquettes, qui la découpe, la met en forme et la réconcilie : la
|
// changerEtiquettes, qui la découpe, la met en forme et la réconcilie : la
|
||||||
// fiche ne lit rien elle-même. Un refus de l'ajout — étiquette mal formée,
|
// fiche ne lit rien elle-même. Un refus de l'ajout — étiquette mal formée,
|
||||||
// trop d'étiquettes, séance en lecture, plan bloqué — se pose contre le
|
// trop d'étiquettes, séance en lecture, plan bloqué — se pose contre le
|
||||||
|
|
@ -31,14 +32,15 @@
|
||||||
// (ORTHOGRAPHE_RETENUE), tant que la révision qui l'a produit est la
|
// (ORTHOGRAPHE_RETENUE), tant que la révision qui l'a produit est la
|
||||||
// courante et la fiche celle de la personne. Le refus d'un retrait se
|
// courante et la fiche celle de la personne. Le refus d'un retrait se
|
||||||
// pose à côté de sa commande (Commande). En lecture comme sur un plan
|
// pose à côté de sa commande (Commande). En lecture comme sur un plan
|
||||||
// bloqué, pastilles et commandes restent : la séance refuse le geste, et
|
// bloqué (cleDeMode, mode.js), aucune commande de retrait ne se montre ;
|
||||||
|
// pastilles, champ et « Ajouter » restent : la séance refuse l'ajout, et
|
||||||
// le refus porte son remède. Une saisie non envoyée est un geste ouvert
|
// le refus porte son remède. Une saisie non envoyée est un geste ouvert
|
||||||
// (geste.js), qui suspend le compte d'inactivité ; l'envoi n'est pas
|
// (geste.js), qui suspend le compte d'inactivité ; l'envoi n'est pas
|
||||||
// ré-entrant. Passer à une autre personne vide le champ et son refus ; le
|
// ré-entrant. Passer à une autre personne vide le champ et son refus ; le
|
||||||
// refus d'un envoi qui revient après ce passage n'est posé nulle part.
|
// refus d'un envoi qui revient après ce passage n'est posé nulle part.
|
||||||
// Un retrait abouti rend le focus au « Retirer » qui prend la place du
|
// Un retrait abouti rend le focus à la commande de retrait qui prend la
|
||||||
// sien — le suivant, sinon le précédent (ligneVoisine) —, ou au champ
|
// place de la sienne — la suivante, sinon la précédente (ligneVoisine) —,
|
||||||
// quand il n'en reste aucun.
|
// ou au champ quand il n'en reste aucune.
|
||||||
//
|
//
|
||||||
// La fiche est une carte : fond et filet de la carte, rayon de 12 px,
|
// La fiche est une carte : fond et filet de la carte, rayon de 12 px,
|
||||||
// ombre légère ; ses étiquettes et légendes de tableau en petites
|
// ombre légère ; ses étiquettes et légendes de tableau en petites
|
||||||
|
|
@ -50,6 +52,7 @@
|
||||||
import { getContext, onDestroy, tick, untrack } from 'svelte';
|
import { getContext, onDestroy, tick, untrack } from 'svelte';
|
||||||
import { nomAffiche } from '../../application/personnes.js';
|
import { nomAffiche } from '../../application/personnes.js';
|
||||||
import { etatPlaces, statuts } from '../../application/places.js';
|
import { etatPlaces, statuts } from '../../application/places.js';
|
||||||
|
import { cleDeMode } from '../mode.js';
|
||||||
import Commande from '../primitives/Commande.svelte';
|
import Commande from '../primitives/Commande.svelte';
|
||||||
import Icone from '../primitives/Icone.svelte';
|
import Icone from '../primitives/Icone.svelte';
|
||||||
import Pastille from '../primitives/Pastille.svelte';
|
import Pastille from '../primitives/Pastille.svelte';
|
||||||
|
|
@ -79,7 +82,7 @@
|
||||||
const etapes = $derived(itineraire(participant.id, charge.evenement.tours, (tour) => etatPlaces(charge, { tour })));
|
const etapes = $derived(itineraire(participant.id, charge.evenement.tours, (tour) => etatPlaces(charge, { tour })));
|
||||||
|
|
||||||
// La fiche et son champ d'ajout, montés : le défilement vise la fiche,
|
// La fiche et son champ d'ajout, montés : le défilement vise la fiche,
|
||||||
// le focus d'après un retrait ses « Retirer » ou le champ.
|
// le focus d'après un retrait ses commandes de retrait ou le champ.
|
||||||
let element;
|
let element;
|
||||||
let champ;
|
let champ;
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|
@ -97,6 +100,9 @@
|
||||||
const idSignal = `${uid}-orthographe`;
|
const idSignal = `${uid}-orthographe`;
|
||||||
|
|
||||||
const pastilles = $derived(etiquettesDeLaFiche(participant));
|
const pastilles = $derived(etiquettesDeLaFiche(participant));
|
||||||
|
// Les commandes de retrait ne se montrent qu'en écriture, hors d'un plan
|
||||||
|
// bloqué.
|
||||||
|
const retraitOffert = $derived(cleDeMode($seance) === 'coquille.mode.ecriture');
|
||||||
const signaux = $derived(signauxOrthographe(dernier, { revision: $seance.evenement?.revision ?? null, participant: participant.id }));
|
const signaux = $derived(signauxOrthographe(dernier, { revision: $seance.evenement?.revision ?? null, participant: participant.id }));
|
||||||
const decrit = $derived(refusChamp !== null ? idRefus : signaux.length > 0 ? idSignal : undefined);
|
const decrit = $derived(refusChamp !== null ? idRefus : signaux.length > 0 ? idSignal : undefined);
|
||||||
|
|
||||||
|
|
@ -142,8 +148,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retire motCle ; aboutie, quand le bouton détruit a laissé le focus au
|
// Retire motCle ; aboutie, quand le bouton détruit a laissé le focus au
|
||||||
// document, le porte au « Retirer » voisin ou au champ. Un refus remonte
|
// document, le porte à la commande de retrait voisine ou au champ. Un
|
||||||
// à Commande, qui le pose à côté du bouton resté en place.
|
// refus remonte à Commande, qui le pose à côté du bouton resté en place.
|
||||||
async function retirer(motCle) {
|
async function retirer(motCle) {
|
||||||
const id = participant.id;
|
const id = participant.id;
|
||||||
const rang = pastilles.indexOf(motCle);
|
const rang = pastilles.indexOf(motCle);
|
||||||
|
|
@ -190,13 +196,15 @@
|
||||||
{#each pastilles as motCle (motCle)}
|
{#each pastilles as motCle (motCle)}
|
||||||
<li>
|
<li>
|
||||||
<Pastille sorte="statut" valeur="neutre" icone="etiquettes" libelle={motCle} />
|
<Pastille sorte="statut" valeur="neutre" icone="etiquettes" libelle={motCle} />
|
||||||
<Commande
|
{#if retraitOffert}
|
||||||
cle="participants.etiquettes.retirer"
|
<Commande
|
||||||
details={{ motCle }}
|
cle="participants.etiquettes.retirer"
|
||||||
nom="retirerEtiquette"
|
details={{ motCle }}
|
||||||
variante="discrete"
|
nom="retirerEtiquette"
|
||||||
executer={() => retirer(motCle)}
|
variante="retrait"
|
||||||
/>
|
executer={() => retirer(motCle)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -399,8 +407,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Les étiquettes : l'intitulé en petites capitales, les pastilles en
|
/* Les étiquettes : l'intitulé en petites capitales, les pastilles en
|
||||||
rangée qui se replie, chacune suivie de son « Retirer » ; puis le champ
|
rangée qui se replie, chacune suivie, en écriture, de sa croix de
|
||||||
d'ajout et son bouton sur une ligne, le refus ou le signal dessous. */
|
retrait ; puis le champ d'ajout et son bouton sur une ligne, le refus
|
||||||
|
ou le signal dessous. */
|
||||||
.etiquettes {
|
.etiquettes {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -935,10 +935,11 @@ describe('les étiquettes : la fiche et la légende (§ 4.5, § 10.2, § 5.9)',
|
||||||
const rubrique = (m) => fiche(m).querySelector('[data-etiquettes]');
|
const rubrique = (m) => fiche(m).querySelector('[data-etiquettes]');
|
||||||
const champEtiquettes = (m) => fiche(m).querySelector('[data-champ="etiquettes"]');
|
const champEtiquettes = (m) => fiche(m).querySelector('[data-champ="etiquettes"]');
|
||||||
const ajoutEtiquettes = (m) => fiche(m).querySelector('[data-commande="ajouterEtiquettes"]');
|
const ajoutEtiquettes = (m) => fiche(m).querySelector('[data-commande="ajouterEtiquettes"]');
|
||||||
|
// La commande de retrait d'une étiquette : une croix cerclée sans texte,
|
||||||
|
// nommée par son aria-label.
|
||||||
|
const retraits = (m) => [...fiche(m).querySelectorAll('[data-commande="retirerEtiquette"]')];
|
||||||
const retrait = (m, motCle) =>
|
const retrait = (m, motCle) =>
|
||||||
[...fiche(m).querySelectorAll('[data-commande="retirerEtiquette"]')].find(
|
retraits(m).find((bouton) => bouton.getAttribute('aria-label') === t('participants.etiquettes.retirer', { motCle })) ?? null;
|
||||||
(bouton) => bouton.textContent.trim() === t('participants.etiquettes.retirer', { motCle }),
|
|
||||||
) ?? null;
|
|
||||||
const legendeEtiquettes = (m) => section(m).querySelector('[data-legende-etiquettes]');
|
const legendeEtiquettes = (m) => section(m).querySelector('[data-legende-etiquettes]');
|
||||||
const etiquettesDe = (m, nom) => participants(m).find((p) => p.nom === nom).etiquettes;
|
const etiquettesDe = (m, nom) => participants(m).find((p) => p.nom === nom).etiquettes;
|
||||||
|
|
||||||
|
|
@ -964,7 +965,7 @@ describe('les étiquettes : la fiche et la légende (§ 4.5, § 10.2, § 5.9)',
|
||||||
await attendreSeance(m);
|
await attendreSeance(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
test('la fiche montre chaque étiquette en pastille neutre à icône, sa commande « Retirer » nommée par elle ; sans étiquette, elle le dit', async () => {
|
test('la fiche montre chaque étiquette en pastille neutre à icône, sa croix de retrait rouge nommée par elle ; sans étiquette, elle le dit', async () => {
|
||||||
const m = await evenementOuvert({ personnes: TROIS_ETIQUETEES });
|
const m = await evenementOuvert({ personnes: TROIS_ETIQUETEES });
|
||||||
await m.seance.executer('changerEtiquettes', { id: idDe(m, 'Ombrelle'), ajouter: 'finance, Montréal' });
|
await m.seance.executer('changerEtiquettes', { id: idDe(m, 'Ombrelle'), ajouter: 'finance, Montréal' });
|
||||||
await ouvrirFiche(m, 'Ombrelle');
|
await ouvrirFiche(m, 'Ombrelle');
|
||||||
|
|
@ -980,10 +981,33 @@ describe('les étiquettes : la fiche et la légende (§ 4.5, § 10.2, § 5.9)',
|
||||||
// La teinte d'appartenance ne sert qu'à l'appartenance (§ 12.7).
|
// La teinte d'appartenance ne sert qu'à l'appartenance (§ 12.7).
|
||||||
const teintes = new Set([1, 2, 3, 4, 5, 6].map((rang) => JSON.stringify(jeton(`--couleur-appartenance-${rang}-fond`))));
|
const teintes = new Set([1, 2, 3, 4, 5, 6].map((rang) => JSON.stringify(jeton(`--couleur-appartenance-${rang}-fond`))));
|
||||||
for (const li of elements) expect(teintes.has(JSON.stringify(fondDe(pastilleDans(li))))).toBe(false);
|
for (const li of elements) expect(teintes.has(JSON.stringify(fondDe(pastilleDans(li))))).toBe(false);
|
||||||
for (const motCle of ['Montréal', 'finance']) expect(retrait(m, motCle), motCle).not.toBeNull();
|
// La croix de retrait : une icône seule, sans texte, la croix cerclée
|
||||||
// En sombre, la pastille prend le neutre sombre.
|
// de la teinte du retrait, marquée « retrait », dans une cible de
|
||||||
|
// 24 px de côté au moins, et un anneau de focus au clavier.
|
||||||
|
for (const motCle of ['Montréal', 'finance']) {
|
||||||
|
const bouton = retrait(m, motCle);
|
||||||
|
expect(bouton, motCle).not.toBeNull();
|
||||||
|
expect(bouton.textContent.trim(), motCle).toBe('');
|
||||||
|
const traces = [...bouton.querySelectorAll('svg path')];
|
||||||
|
expect(traces.length, motCle).toBeGreaterThan(0);
|
||||||
|
for (const trace of traces) {
|
||||||
|
expect(trace.dataset.marque).toBe('retrait');
|
||||||
|
expect(composantes(getComputedStyle(trace).stroke)).toEqual(jeton('--couleur-retrait'));
|
||||||
|
}
|
||||||
|
const { width, height } = bouton.getBoundingClientRect();
|
||||||
|
expect(Math.min(width, height), motCle).toBeGreaterThanOrEqual(24);
|
||||||
|
}
|
||||||
|
// Tab mène d'une croix à la suivante, qui porte l'anneau de focus.
|
||||||
|
retrait(m, 'Montréal').focus();
|
||||||
|
await touche('Tab');
|
||||||
|
expect(document.activeElement).toBe(retrait(m, 'finance'));
|
||||||
|
expect(retrait(m, 'finance').matches(':focus-visible')).toBe(true);
|
||||||
|
expect(getComputedStyle(retrait(m, 'finance')).outlineStyle).toBe('solid');
|
||||||
|
expect(composantes(getComputedStyle(retrait(m, 'finance')).outlineColor)).toEqual(jeton('--couleur-focus'));
|
||||||
|
// En sombre, la pastille prend le neutre sombre, la croix le rouge sombre.
|
||||||
await sousTheme('sombre', async () => {
|
await sousTheme('sombre', async () => {
|
||||||
for (const li of elements) expect(fondDe(pastilleDans(li))).toEqual(jeton('--couleur-neutre-fond'));
|
for (const li of elements) expect(fondDe(pastilleDans(li))).toEqual(jeton('--couleur-neutre-fond'));
|
||||||
|
expect(composantes(getComputedStyle(retrait(m, 'finance').querySelector('path')).stroke)).toEqual(jeton('--couleur-retrait'));
|
||||||
});
|
});
|
||||||
|
|
||||||
await ouvrirFiche(m, 'Pervenche');
|
await ouvrirFiche(m, 'Pervenche');
|
||||||
|
|
@ -992,7 +1016,7 @@ describe('les étiquettes : la fiche et la légende (§ 4.5, § 10.2, § 5.9)',
|
||||||
expect(fiche(m).querySelectorAll('[data-commande="retirerEtiquette"]')).toHaveLength(0);
|
expect(fiche(m).querySelectorAll('[data-commande="retirerEtiquette"]')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('au clavier : une saisie de trois étiquettes, Entrée, une entrée au journal ; le champ se vide et garde le focus ; « Retirer » ôte la sienne et rend le focus à sa voisine', async () => {
|
test('au clavier : une saisie de trois étiquettes, Entrée, une entrée au journal ; le champ se vide et garde le focus ; la croix de retrait ôte la sienne et rend le focus à sa voisine', async () => {
|
||||||
const m = await evenementOuvert({ personnes: TROIS_ETIQUETEES });
|
const m = await evenementOuvert({ personnes: TROIS_ETIQUETEES });
|
||||||
await ouvrirFiche(m, 'Pervenche');
|
await ouvrirFiche(m, 'Pervenche');
|
||||||
const champ = champEtiquettes(m);
|
const champ = champEtiquettes(m);
|
||||||
|
|
@ -1017,8 +1041,8 @@ describe('les étiquettes : la fiche et la légende (§ 4.5, § 10.2, § 5.9)',
|
||||||
expect(document.activeElement).toBe(champEtiquettes(m));
|
expect(document.activeElement).toBe(champEtiquettes(m));
|
||||||
expect([...rubrique(m).querySelectorAll('li')].map((li) => texteDe(pastilleDans(li)))).toEqual(['bénévole', 'ressources humaines', 'voile']);
|
expect([...rubrique(m).querySelectorAll('li')].map((li) => texteDe(pastilleDans(li)))).toEqual(['bénévole', 'ressources humaines', 'voile']);
|
||||||
|
|
||||||
// « Retirer » activé par Entrée : le focus passe au « Retirer » suivant,
|
// La croix de retrait activée par Entrée : le focus passe à la croix
|
||||||
// sinon au précédent, sinon au champ ; jamais au document.
|
// suivante, sinon à la précédente, sinon au champ ; jamais au document.
|
||||||
const retirerAuClavier = async (motCle) => {
|
const retirerAuClavier = async (motCle) => {
|
||||||
retrait(m, motCle).focus();
|
retrait(m, motCle).focus();
|
||||||
await touche('Enter');
|
await touche('Enter');
|
||||||
|
|
@ -1197,29 +1221,31 @@ describe('les étiquettes : la fiche et la légende (§ 4.5, § 10.2, § 5.9)',
|
||||||
expect(legendeEtiquettes(m)).toBeNull();
|
expect(legendeEtiquettes(m)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('en lecture : les pastilles restent ; « Ajouter » et « Retirer » sont refusés, chacun avec son remède, et rien ne s’écrit', async () => {
|
test('en lecture : les pastilles restent, sans croix de retrait ; « Ajouter » est refusé avec son remède, rien ne s’écrit ; de retour en écriture, les croix reviennent', async () => {
|
||||||
const m = await evenementOuvert({ personnes: TROIS_ETIQUETEES });
|
const m = await evenementOuvert({ personnes: TROIS_ETIQUETEES });
|
||||||
await m.seance.executer('changerEtiquettes', { id: idDe(m, 'Ombrelle'), ajouter: 'finance' });
|
await m.seance.executer('changerEtiquettes', { id: idDe(m, 'Ombrelle'), ajouter: 'finance' });
|
||||||
await m.seance.passerEnLecture();
|
await m.seance.passerEnLecture();
|
||||||
await ouvrirFiche(m, 'Ombrelle');
|
await ouvrirFiche(m, 'Ombrelle');
|
||||||
const avant = texteJournal(m);
|
const avant = texteJournal(m);
|
||||||
expect(texteDe(pastilleDans(rubrique(m).querySelector('li')))).toBe('finance');
|
expect(texteDe(pastilleDans(rubrique(m).querySelector('li')))).toBe('finance');
|
||||||
|
expect(retraits(m)).toHaveLength(0);
|
||||||
|
|
||||||
await ajouterParClavier(m, 'bénévole');
|
await ajouterParClavier(m, 'bénévole');
|
||||||
const message = document.getElementById(champEtiquettes(m).getAttribute('aria-describedby'));
|
const message = document.getElementById(champEtiquettes(m).getAttribute('aria-describedby'));
|
||||||
expect(message.dataset.refus).toBe('MODE_LECTURE');
|
expect(message.dataset.refus).toBe('MODE_LECTURE');
|
||||||
expect(message.querySelector('[data-commande]')).not.toBeNull();
|
expect(message.querySelector('[data-commande]')).not.toBeNull();
|
||||||
|
|
||||||
await cliquer(retrait(m, 'finance'));
|
|
||||||
await attendreSeance(m);
|
|
||||||
const refusRetrait = retrait(m, 'finance').parentElement.querySelector('[data-refus]');
|
|
||||||
expect(refusRetrait.dataset.refus).toBe('MODE_LECTURE');
|
|
||||||
expect(refusRetrait.querySelector('[data-commande]')).not.toBeNull();
|
|
||||||
expect(etiquettesDe(m, 'Ombrelle')).toEqual(['finance']);
|
expect(etiquettesDe(m, 'Ombrelle')).toEqual(['finance']);
|
||||||
expect(texteJournal(m)).toBe(avant);
|
expect(texteJournal(m)).toBe(avant);
|
||||||
|
|
||||||
|
await m.seance.passerEnEcriture();
|
||||||
|
await rendu();
|
||||||
|
expect(retrait(m, 'finance')).not.toBeNull();
|
||||||
|
await m.seance.passerEnLecture();
|
||||||
|
await rendu();
|
||||||
|
expect(retraits(m)).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('plan bloqué, en écriture : « Ajouter » et « Retirer » sont refusés avec « Débloquer » pour remède', async () => {
|
test('plan bloqué : aucune croix de retrait ; « Ajouter » est refusé avec « Débloquer » pour remède', async () => {
|
||||||
const m = await evenementOuvert({ sieges: 4, tours: 2, tables: 1, personnes: TROIS_ETIQUETEES });
|
const m = await evenementOuvert({ sieges: 4, tours: 2, tables: 1, personnes: TROIS_ETIQUETEES });
|
||||||
await m.seance.executer('changerEtiquettes', { id: idDe(m, 'Ombrelle'), ajouter: 'finance' });
|
await m.seance.executer('changerEtiquettes', { id: idDe(m, 'Ombrelle'), ajouter: 'finance' });
|
||||||
await m.app.generation().generer({ graine: 1, nombre: 1, arret: 50, historique: 1000 });
|
await m.app.generation().generer({ graine: 1, nombre: 1, arret: 50, historique: 1000 });
|
||||||
|
|
@ -1229,13 +1255,12 @@ describe('les étiquettes : la fiche et la légende (§ 4.5, § 10.2, § 5.9)',
|
||||||
await ouvrirFiche(m, 'Ombrelle');
|
await ouvrirFiche(m, 'Ombrelle');
|
||||||
const avant = texteJournal(m);
|
const avant = texteJournal(m);
|
||||||
|
|
||||||
|
expect(texteDe(pastilleDans(rubrique(m).querySelector('li')))).toBe('finance');
|
||||||
|
expect(retraits(m)).toHaveLength(0);
|
||||||
|
|
||||||
await ajouterParClavier(m, 'bénévole');
|
await ajouterParClavier(m, 'bénévole');
|
||||||
const message = document.getElementById(champEtiquettes(m).getAttribute('aria-describedby'));
|
const message = document.getElementById(champEtiquettes(m).getAttribute('aria-describedby'));
|
||||||
expect(message.querySelector('[data-commande="debloquer"]')).not.toBeNull();
|
expect(message.querySelector('[data-commande="debloquer"]')).not.toBeNull();
|
||||||
await cliquer(retrait(m, 'finance'));
|
|
||||||
await attendreSeance(m);
|
|
||||||
const refusRetrait = retrait(m, 'finance').parentElement.querySelector('[data-refus]');
|
|
||||||
expect(refusRetrait.querySelector('[data-commande="debloquer"]')).not.toBeNull();
|
|
||||||
expect(texteJournal(m)).toBe(avant);
|
expect(texteJournal(m)).toBe(avant);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,32 @@
|
||||||
// son étiquette seule ne le distingue pas d'un homonyme ; null,
|
// son étiquette seule ne le distingue pas d'un homonyme ; null,
|
||||||
// l'étiquette le nomme. L'étiquette visible ne change pas. Le refus reçoit
|
// l'étiquette le nomme. L'étiquette visible ne change pas. Le refus reçoit
|
||||||
// nom pour nommer son remède.
|
// nom pour nommer son remède.
|
||||||
|
//
|
||||||
|
// type est celui de l'entrée : 'text', le défaut, ou 'number', un champ
|
||||||
|
// étroit de six caractères, qui porte min, max et pas (step) quand ils
|
||||||
|
// sont donnés. La lecture reste celle de lire, et le domaine celui de
|
||||||
|
// l'exécution : le navigateur rend vide un texte qu'il ne lit pas comme
|
||||||
|
// un nombre, que lire refuse comme tout texte vide.
|
||||||
import { getContext, onDestroy, untrack } from 'svelte';
|
import { getContext, onDestroy, untrack } from 'svelte';
|
||||||
import { effetDe } from '../raccourcis.js';
|
import { effetDe } from '../raccourcis.js';
|
||||||
import { creerMachineChamp, estRefusSeance, refusSeanceCourant } from './champ.js';
|
import { creerMachineChamp, estRefusSeance, refusSeanceCourant } from './champ.js';
|
||||||
import { creerGeste } from './geste.js';
|
import { creerGeste } from './geste.js';
|
||||||
import Refus from './Refus.svelte';
|
import Refus from './Refus.svelte';
|
||||||
|
|
||||||
let { cle, etiquette, valeur, lire, executer, details = {}, nom = null, gestes = {} } = $props();
|
let {
|
||||||
|
cle,
|
||||||
|
etiquette,
|
||||||
|
valeur,
|
||||||
|
lire,
|
||||||
|
executer,
|
||||||
|
details = {},
|
||||||
|
nom = null,
|
||||||
|
gestes = {},
|
||||||
|
type = 'text',
|
||||||
|
min = null,
|
||||||
|
max = null,
|
||||||
|
pas = null,
|
||||||
|
} = $props();
|
||||||
|
|
||||||
const t = getContext('t');
|
const t = getContext('t');
|
||||||
const seance = getContext('seance') ?? null;
|
const seance = getContext('seance') ?? null;
|
||||||
|
|
@ -192,7 +211,10 @@
|
||||||
<label for={idEntree}>{t(etiquette, details)}</label>
|
<label for={idEntree}>{t(etiquette, details)}</label>
|
||||||
<input
|
<input
|
||||||
id={idEntree}
|
id={idEntree}
|
||||||
type="text"
|
{type}
|
||||||
|
min={min ?? undefined}
|
||||||
|
max={max ?? undefined}
|
||||||
|
step={pas ?? undefined}
|
||||||
data-champ={cle}
|
data-champ={cle}
|
||||||
value={saisie}
|
value={saisie}
|
||||||
aria-label={nom ?? undefined}
|
aria-label={nom ?? undefined}
|
||||||
|
|
@ -225,6 +247,11 @@
|
||||||
font-size: var(--taille-texte);
|
font-size: var(--taille-texte);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Un nombre : six caractères de large, flèches comprises. */
|
||||||
|
input[type='number'] {
|
||||||
|
width: 6ch;
|
||||||
|
}
|
||||||
|
|
||||||
input:focus-visible {
|
input:focus-visible {
|
||||||
outline: 2px solid var(--couleur-focus);
|
outline: 2px solid var(--couleur-focus);
|
||||||
outline-offset: 1px;
|
outline-offset: 1px;
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,17 @@
|
||||||
// variante : 'secondaire', la défaut, à filet sur le fond de la carte ;
|
// variante : 'secondaire', la défaut, à filet sur le fond de la carte ;
|
||||||
// 'principale', l'action à faire sur l'écran, pleine de la teinte de
|
// 'principale', l'action à faire sur l'écran, pleine de la teinte de
|
||||||
// l'action, haute d'une cible principale ; 'discrete', sans fond ni filet
|
// l'action, haute d'une cible principale ; 'discrete', sans fond ni filet
|
||||||
// au repos. Elle ne change que l'apparence ; une variante hors de
|
// au repos ; 'retrait', l'icône croixCerclee seule, de --couleur-retrait,
|
||||||
// VARIANTES_COMMANDE lève RangeError au montage.
|
// sans fond ni filet au repos, dans une cible de --cible-minimale de côté
|
||||||
|
// au moins (§ 14.5) : son libellé, que rien ne montre, devient son nom
|
||||||
|
// accessible, à défaut de nomAccessible, et ses tracés portent la marque
|
||||||
|
// « retrait » du registre (marques.js). Elle ne change que l'apparence ;
|
||||||
|
// une variante hors de VARIANTES_COMMANDE, ou le retrait d'une commande à
|
||||||
|
// confirmer, lève RangeError au montage.
|
||||||
import { getContext, onDestroy, untrack } from 'svelte';
|
import { getContext, onDestroy, untrack } from 'svelte';
|
||||||
import { effetDe } from '../raccourcis.js';
|
import { effetDe } from '../raccourcis.js';
|
||||||
import { DELAI_CONFIRMATION_MS, creerMachineCommande, exigerVariante } from './commande.js';
|
import { DELAI_CONFIRMATION_MS, creerMachineCommande, exigerVariante } from './commande.js';
|
||||||
|
import Icone from './Icone.svelte';
|
||||||
import Refus from './Refus.svelte';
|
import Refus from './Refus.svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
|
@ -67,7 +73,11 @@
|
||||||
|
|
||||||
const libelle = $derived(armee ? t(confirmer) : t(cle, details));
|
const libelle = $derived(armee ? t(confirmer) : t(cle, details));
|
||||||
const designation = $derived(nom ?? cle.slice(cle.lastIndexOf('.') + 1));
|
const designation = $derived(nom ?? cle.slice(cle.lastIndexOf('.') + 1));
|
||||||
const forme = $derived(exigerVariante(variante));
|
const forme = $derived(exigerVariante(variante, confirmer));
|
||||||
|
// Le nom accessible : le libellé de confirmation, montré, quand elle est
|
||||||
|
// armée ; sinon nomAccessible, ou le libellé d'un retrait, qui ne le
|
||||||
|
// montre pas.
|
||||||
|
const nomDuBouton = $derived(armee ? undefined : (nomAccessible ?? (forme === 'retrait' ? libelle : undefined)));
|
||||||
|
|
||||||
function desarmer() {
|
function desarmer() {
|
||||||
if (minuterie === null) return;
|
if (minuterie === null) return;
|
||||||
|
|
@ -119,12 +129,14 @@
|
||||||
data-commande={designation}
|
data-commande={designation}
|
||||||
class:principale={forme === 'principale'}
|
class:principale={forme === 'principale'}
|
||||||
class:discrete={forme === 'discrete'}
|
class:discrete={forme === 'discrete'}
|
||||||
|
class:retrait={forme === 'retrait'}
|
||||||
aria-busy={enCours ? 'true' : undefined}
|
aria-busy={enCours ? 'true' : undefined}
|
||||||
aria-disabled={enCours ? 'true' : undefined}
|
aria-disabled={enCours ? 'true' : undefined}
|
||||||
aria-label={armee ? undefined : (nomAccessible ?? undefined)}
|
aria-label={nomDuBouton}
|
||||||
aria-describedby={refus === null ? undefined : idRefus}
|
aria-describedby={refus === null ? undefined : idRefus}
|
||||||
onkeydown={surTouche}
|
onkeydown={surTouche}
|
||||||
onclick={cliquer}>{libelle}</button
|
onclick={cliquer}
|
||||||
|
>{#if forme === 'retrait'}<Icone nom="croixCerclee" taille={18} marque="retrait" />{:else}{libelle}{/if}</button
|
||||||
>
|
>
|
||||||
{#if refus !== null}
|
{#if refus !== null}
|
||||||
<Refus id={idRefus} {refus} lever={() => (refus = null)} objet={nomAccessible} {gestes} />
|
<Refus id={idRefus} {refus} lever={() => (refus = null)} objet={nomAccessible} {gestes} />
|
||||||
|
|
@ -177,6 +189,27 @@
|
||||||
background-color: var(--couleur-carte-fond-sourd);
|
background-color: var(--couleur-carte-fond-sourd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Le retrait : l'icône seule, de la teinte du retrait, centrée dans une
|
||||||
|
cible ronde de --cible-minimale de côté au moins ; ni fond ni filet au
|
||||||
|
repos, le fond sourd de la carte au survol. */
|
||||||
|
button.retrait {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: var(--cible-minimale);
|
||||||
|
min-height: var(--cible-minimale);
|
||||||
|
padding: 0;
|
||||||
|
border-color: transparent;
|
||||||
|
border-radius: var(--rayon-pilule);
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--couleur-retrait);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.retrait:hover {
|
||||||
|
background-color: var(--couleur-carte-fond-sourd);
|
||||||
|
}
|
||||||
|
|
||||||
button[aria-disabled='true'] {
|
button[aria-disabled='true'] {
|
||||||
border-style: dashed;
|
border-style: dashed;
|
||||||
color: var(--couleur-page-texte-secondaire);
|
color: var(--couleur-page-texte-secondaire);
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,13 @@
|
||||||
// sans remplissage, du trait de la couleur courante — le parent la règle
|
// sans remplissage, du trait de la couleur courante — le parent la règle
|
||||||
// par color, d'un jeton. Elle est cachée aux technologies d'assistance et
|
// par color, d'un jeton. Elle est cachée aux technologies d'assistance et
|
||||||
// ne prend jamais le focus : un libellé l'accompagne toujours. taille :
|
// ne prend jamais le focus : un libellé l'accompagne toujours. taille :
|
||||||
// le côté du carré, en px. Un nom inconnu lève RangeError au montage.
|
// le côté du carré, en px. marque, le nom d'une marque du registre
|
||||||
|
// (MARQUES, marques.js), se pose en data-marque sur chacun de ses tracés,
|
||||||
|
// que les épreuves de rendu mesurent ; null, aucun. Un nom inconnu lève
|
||||||
|
// RangeError au montage.
|
||||||
import { traceIcone } from './icones.js';
|
import { traceIcone } from './icones.js';
|
||||||
|
|
||||||
let { nom, taille = 16 } = $props();
|
let { nom, taille = 16, marque = null } = $props();
|
||||||
|
|
||||||
const traces = $derived(traceIcone(nom));
|
const traces = $derived(traceIcone(nom));
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -28,7 +31,7 @@
|
||||||
focusable="false"
|
focusable="false"
|
||||||
>
|
>
|
||||||
{#each traces as trace, rang (rang)}
|
{#each traces as trace, rang (rang)}
|
||||||
<path d={trace} />
|
<path d={trace} data-marque={marque ?? undefined} />
|
||||||
{/each}
|
{/each}
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,19 +39,26 @@ import { COMMANDES } from '../../application/commandes.js';
|
||||||
/** Les variantes d'une commande, la secondaire en tête — celle d'une
|
/** Les variantes d'une commande, la secondaire en tête — celle d'une
|
||||||
* commande qui n'en donne pas : principale, l'action à faire sur l'écran,
|
* commande qui n'en donne pas : principale, l'action à faire sur l'écran,
|
||||||
* pleine de la teinte de l'action ; secondaire, à filet ; discrète, sans
|
* pleine de la teinte de l'action ; secondaire, à filet ; discrète, sans
|
||||||
* fond ni filet au repos. La variante ne change que l'apparence : le rôle,
|
* fond ni filet au repos ; retrait, une croix cerclée de la teinte du
|
||||||
* les attributs et la machine restent ceux de toute commande. */
|
* retrait, sans fond ni filet au repos, dont le libellé devient le nom
|
||||||
export const VARIANTES_COMMANDE = Object.freeze(['secondaire', 'principale', 'discrete']);
|
* accessible. La variante ne change que l'apparence : le rôle, la
|
||||||
|
* désignation et la machine restent ceux de toute commande. */
|
||||||
|
export const VARIANTES_COMMANDE = Object.freeze(['secondaire', 'principale', 'discrete', 'retrait']);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Une variante de commande, rendue telle quelle.
|
* Une variante de commande, rendue telle quelle. Le retrait ne montre
|
||||||
|
* aucun texte : son libellé armé n'aurait où s'afficher, et une commande à
|
||||||
|
* confirmer ne le prend pas.
|
||||||
*
|
*
|
||||||
* @param {string} variante
|
* @param {string} variante
|
||||||
* @returns {'secondaire'|'principale'|'discrete'}
|
* @param {string|null} [confirmer] la clé de confirmation de la commande
|
||||||
* @throws {RangeError} hors de VARIANTES_COMMANDE, en la nommant
|
* @returns {'secondaire'|'principale'|'discrete'|'retrait'}
|
||||||
|
* @throws {RangeError} hors de VARIANTES_COMMANDE, en la nommant ; le
|
||||||
|
* retrait d'une commande à confirmer
|
||||||
*/
|
*/
|
||||||
export function exigerVariante(variante) {
|
export function exigerVariante(variante, confirmer = null) {
|
||||||
if (!VARIANTES_COMMANDE.includes(variante)) throw new RangeError(`commande : variante inconnue ${JSON.stringify(variante) ?? 'undefined'}`);
|
if (!VARIANTES_COMMANDE.includes(variante)) throw new RangeError(`commande : variante inconnue ${JSON.stringify(variante) ?? 'undefined'}`);
|
||||||
|
if (variante === 'retrait' && confirmer !== null) throw new RangeError('commande : le retrait ne se confirme pas');
|
||||||
return variante;
|
return variante;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -352,8 +352,8 @@ describe('commande : l’action d’un remède', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('commande : la variante', () => {
|
describe('commande : la variante', () => {
|
||||||
test('trois variantes, la secondaire en tête : celle d’une commande qui n’en donne pas', () => {
|
test('quatre variantes, la secondaire en tête : celle d’une commande qui n’en donne pas', () => {
|
||||||
assert.deepEqual(VARIANTES_COMMANDE, ['secondaire', 'principale', 'discrete']);
|
assert.deepEqual(VARIANTES_COMMANDE, ['secondaire', 'principale', 'discrete', 'retrait']);
|
||||||
assert.ok(Object.isFrozen(VARIANTES_COMMANDE));
|
assert.ok(Object.isFrozen(VARIANTES_COMMANDE));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -363,4 +363,10 @@ describe('commande : la variante', () => {
|
||||||
assert.throws(() => exigerVariante(autre), (e) => e instanceof RangeError && e.message.includes(JSON.stringify(autre) ?? 'undefined'), String(autre));
|
assert.throws(() => exigerVariante(autre), (e) => e instanceof RangeError && e.message.includes(JSON.stringify(autre) ?? 'undefined'), String(autre));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('le retrait ne se confirme pas : avec une clé de confirmation, exigerVariante lève RangeError ; les autres variantes la prennent', () => {
|
||||||
|
assert.throws(() => exigerVariante('retrait', 'communs.commande.confirmer'), RangeError);
|
||||||
|
assert.equal(exigerVariante('retrait', null), 'retrait');
|
||||||
|
for (const variante of ['secondaire', 'principale', 'discrete']) assert.equal(exigerVariante(variante, 'communs.commande.confirmer'), variante);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -415,6 +415,30 @@ describe('commande : la variante', () => {
|
||||||
expect(style(secondaire).fontSize).toBe('16px');
|
expect(style(secondaire).fontSize).toBe('16px');
|
||||||
expect(() => commande({ variante: 'primaire' })).toThrow(RangeError);
|
expect(() => commande({ variante: 'primaire' })).toThrow(RangeError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('retrait : la croix cerclée seule, de la teinte du retrait, sans texte ni fond, nommée par son libellé, dans une cible de 24 px au moins ; à confirmer, elle lève au montage', async () => {
|
||||||
|
const libelle = t('participants.etiquettes.retirer', { motCle: 'voile' });
|
||||||
|
const retrait = commande({ variante: 'retrait', cle: 'participants.etiquettes.retirer', details: { motCle: 'voile' }, nom: 'retirerEtiquette' });
|
||||||
|
expect(retrait.textContent.trim()).toBe('');
|
||||||
|
expect(retrait.getAttribute('aria-label')).toBe(libelle);
|
||||||
|
expect(retrait.getAttribute('data-commande')).toBe('retirerEtiquette');
|
||||||
|
expect([...retrait.querySelectorAll('svg path')].map((p) => p.getAttribute('d'))).toEqual([...ICONES.croixCerclee]);
|
||||||
|
const { width, height } = retrait.getBoundingClientRect();
|
||||||
|
expect(Math.min(width, height)).toBeGreaterThanOrEqual(24);
|
||||||
|
for (const theme of THEMES) {
|
||||||
|
await sousTheme(theme, () => {
|
||||||
|
expect(fond(retrait), theme).toEqual(TRANSPARENT);
|
||||||
|
expect(composantes(style(retrait).borderTopColor)[3], theme).toBe(0);
|
||||||
|
for (const trace of retrait.querySelectorAll('path')) {
|
||||||
|
expect(trace.dataset.marque, theme).toBe('retrait');
|
||||||
|
expect(composantes(getComputedStyle(trace).stroke), theme).toEqual(jeton('--couleur-retrait'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Un nom accessible donné passe devant le libellé.
|
||||||
|
expect(commande({ variante: 'retrait', nomAccessible: 'Ôter' }).getAttribute('aria-label')).toBe('Ôter');
|
||||||
|
expect(() => commande({ variante: 'retrait', confirmer: 'communs.commande.confirmerDeblocage' })).toThrow(RangeError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('refus, infobulle, champ et fenêtre à confirmer : habillés des jetons', () => {
|
describe('refus, infobulle, champ et fenêtre à confirmer : habillés des jetons', () => {
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,8 @@ const DESSINS = [
|
||||||
['demarrer', ['M8 5l11 7-11 7z']],
|
['demarrer', ['M8 5l11 7-11 7z']],
|
||||||
// Une étiquette (§ 4.5) : l'étiquette à œillet, pointe en bas à droite.
|
// Une étiquette (§ 4.5) : l'étiquette à œillet, pointe en bas à droite.
|
||||||
['etiquettes', ['M3 3h8l10 10-8 8L3 11z', cercle(7.5, 7.5, 1.5)]],
|
['etiquettes', ['M3 3h8l10 10-8 8L3 11z', cercle(7.5, 7.5, 1.5)]],
|
||||||
|
// Un retrait (Commande, variante retrait) : une croix dans un cercle.
|
||||||
|
['croixCerclee', [cercle(12, 12, 9), 'M9 9l6 6M15 9l-6 6']],
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Les noms des icônes, dans l'ordre du module. */
|
/** Les noms des icônes, dans l'ordre du module. */
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ describe('icônes des primitives', () => {
|
||||||
assert.deepEqual(NOMS_ICONES, [
|
assert.deepEqual(NOMS_ICONES, [
|
||||||
'crayon', 'listePoints', 'coche', 'cadenas', 'liste', 'importer', 'exporter', 'ajouter', 'chercher',
|
'crayon', 'listePoints', 'coche', 'cadenas', 'liste', 'importer', 'exporter', 'ajouter', 'chercher',
|
||||||
'calendrier', 'information', 'triangle', 'interdit', 'dossier', 'oeil', 'stylo', 'defaire', 'imprimer',
|
'calendrier', 'information', 'triangle', 'interdit', 'dossier', 'oeil', 'stylo', 'defaire', 'imprimer',
|
||||||
'etoile', 'croix', 'chevron', 'points', 'demarrer', 'etiquettes',
|
'etoile', 'croix', 'chevron', 'points', 'demarrer', 'etiquettes', 'croixCerclee',
|
||||||
]);
|
]);
|
||||||
assert.ok(Object.isFrozen(ICONES) && Object.isFrozen(NOMS_ICONES));
|
assert.ok(Object.isFrozen(ICONES) && Object.isFrozen(NOMS_ICONES));
|
||||||
assert.deepEqual(Object.keys(ICONES).sort(), [...NOMS_ICONES].sort());
|
assert.deepEqual(Object.keys(ICONES).sort(), [...NOMS_ICONES].sort());
|
||||||
|
|
|
||||||
|
|
@ -251,6 +251,46 @@ describe('champ : la fin du geste (§ 2.1, § 8.2)', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('champ : le type de l’entrée', () => {
|
||||||
|
test('texte par défaut, sans bornes ; nombre : min, max et pas posés, une lettre ne s’y inscrit pas, et lire refuse le vide qu’elle laisse ; des chiffres s’écrivent', async () => {
|
||||||
|
const montage = await preparer();
|
||||||
|
const { entree } = monterChamp(montage);
|
||||||
|
expect(entree.type).toBe('text');
|
||||||
|
for (const attribut of ['min', 'max', 'step']) expect(entree.hasAttribute(attribut), attribut).toBe(false);
|
||||||
|
|
||||||
|
const recus = [];
|
||||||
|
const cible = monter(
|
||||||
|
Champ,
|
||||||
|
{
|
||||||
|
cle: 'nombre',
|
||||||
|
etiquette: 'communs.commande.modifier',
|
||||||
|
valeur: 3,
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
pas: 1,
|
||||||
|
lire: (texte) => (/^\d+$/.test(texte) ? { valeur: Number(texte) } : { refus: { code: 'NON_ENTIER', details: { saisie: texte, remede: null } } }),
|
||||||
|
executer: (valeur) => recus.push(valeur),
|
||||||
|
},
|
||||||
|
montage.contexte,
|
||||||
|
);
|
||||||
|
const nombre = cible.querySelector('input[data-champ="nombre"]');
|
||||||
|
expect([nombre.type, nombre.min, nombre.max, nombre.step, nombre.value]).toEqual(['number', '1', '100', '1', '3']);
|
||||||
|
await cliquer(nombre);
|
||||||
|
nombre.select();
|
||||||
|
await taper('x');
|
||||||
|
await cliquerAilleurs();
|
||||||
|
expect(nombre.value).toBe('');
|
||||||
|
expect(cible.querySelector('[data-refus="NON_ENTIER"]')).not.toBeNull();
|
||||||
|
expect(recus).toEqual([]);
|
||||||
|
await cliquer(nombre);
|
||||||
|
await taper('42');
|
||||||
|
await cliquerAilleurs();
|
||||||
|
expect(recus).toEqual([42]);
|
||||||
|
expect(cible.querySelector('[data-refus]')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('champ : le refus de l’exécution (§ 2.1, § 5.9)', () => {
|
describe('champ : le refus de l’exécution (§ 2.1, § 5.9)', () => {
|
||||||
test('executer refusé avec un code : le refus se pose contre le champ, la saisie gardée, rien d’écrit ; le refus levé par son remède, le champ rend la valeur enregistrée, et une saisie nouvelle s’écrit', async () => {
|
test('executer refusé avec un code : le refus se pose contre le champ, la saisie gardée, rien d’écrit ; le refus levé par son remède, le champ rend la valeur enregistrée, et une saisie nouvelle s’écrit', async () => {
|
||||||
const montage = await preparer();
|
const montage = await preparer();
|
||||||
|
|
|
||||||
|
|
@ -659,7 +659,7 @@ export const ECRANS = Object.freeze([
|
||||||
async atteindre(contexte) {
|
async atteindre(contexte) {
|
||||||
// Deux personnes étiquetées, puis la fiche d'une troisième, où
|
// Deux personnes étiquetées, puis la fiche d'une troisième, où
|
||||||
// « FINANCE » tapé au clavier s'enregistre sous l'orthographe que
|
// « FINANCE » tapé au clavier s'enregistre sous l'orthographe que
|
||||||
// l'événement porte : les pastilles neutres et leur « Retirer », le
|
// l'événement porte : les pastilles neutres et leur croix de retrait, le
|
||||||
// signal d'orthographe contre le champ, et la légende des étiquettes
|
// signal d'orthographe contre le champ, et la légende des étiquettes
|
||||||
// sous celle des appartenances.
|
// sous celle des appartenances.
|
||||||
let charge = chargePrincipale();
|
let charge = chargePrincipale();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue