[FIX] event header: a lone warning shows its action without scrolling

A four-line warning with its action needed 132 px in the header's side
zone, capped at 7.25rem, so « Ouvrir les tables » hid below the fold. A
zone holding a single warning now grows to 10rem, five lines plus the
action; several warnings keep the lower cap, so the plan loses no more.
Checked: 1, 4 and 13 missing places at 1366 and 1024 px, the action
inside the visible zone; node 4012, browser 872.

--- FR ---

[FIX] en-tête : un avis seul montre son action sans défilement

Un avis de quatre lignes et son action demandaient 132 px dans la zone
latérale de l'en-tête, bornée à 7,25rem : « Ouvrir les tables » restait
caché. Une zone qui ne porte qu'un avis monte à 10rem, cinq lignes et
l'action ; plusieurs avis gardent la borne basse, le plan ne perd rien
de plus. Vérifié : 1, 4 et 13 places manquantes à 1366 et 1024 px,
l'action dans la zone visible ; node 4012, navigateur 872.

Assisted-by: Claude Opus 5.5
This commit is contained in:
Mathieu Benoit 2026-10-11 01:53:47 -04:00
parent 6f7cd3511b
commit 16a7abbedc
2 changed files with 65 additions and 29 deletions

View file

@ -404,6 +404,17 @@
padding-block: 0.125rem; padding-block: 0.125rem;
} }
/* Une zone qui ne porte qu'un message — un seul enfant, qui n'en a qu'un
lui-même — n'a rien à faire défiler pour en montrer d'autres : son
plafond, 10rem, tient entier un avertissement de cinq lignes de texte
et son action dessous. Le plafond de plusieurs messages reste celui
ci-dessus, et la surface garde sa hauteur devant des avertissements
nombreux. Ces enfants viennent d'un autre composant : :global les
soustrait à la portée des styles de celui-ci. */
.place > .annonces:has(> :global(:only-child > :only-child)) {
max-height: 10rem;
}
/* Seules — sans tête, en plein écran, ou devant un support rompu —, les /* Seules — sans tête, en plein écran, ou devant un support rompu —, les
annonces ne prennent jamais plus du quart du cadre, et cèdent d'abord annonces ne prennent jamais plus du quart du cadre, et cèdent d'abord
leur hauteur à la surface : leur facteur de rétrécissement, mille fois leur hauteur à la surface : leur facteur de rétrécissement, mille fois

View file

@ -133,6 +133,17 @@ async function enManque(m) {
flushSync(); flushSync();
} }
// L'événement passé en écriture, une table de trois places créée, puis
// trois personnes de plus que de places : le manque donné, à chaque tour.
async function enManqueDe(m, manque) {
await m.seance.passerEnEcriture();
await m.seance.executer('creerTable', { forme: 'ronde' });
for (let n = 0; n < EVENEMENT.siegesParDefaut + manque; n += 1) {
await m.seance.executer('ajouterParticipant', { champs: { nom: `${NOMS[n % NOMS.length]} ${n + 1}` } });
}
flushSync();
}
// --- Les épreuves ----------------------------------------------------------------- // --- Les épreuves -----------------------------------------------------------------
describe('la bande de mode (§ 8.4)', () => { describe('la bande de mode (§ 8.4)', () => {
@ -436,37 +447,51 @@ describe('les avertissements, en messages d’avis', () => {
// Le texte de l'avertissement et son action ne sont jamais rognés : ni // Le texte de l'avertissement et son action ne sont jamais rognés : ni
// le texte, ni le message, ni aucune boîte qui les porte jusqu'au cadre // le texte, ni le message, ni aucune boîte qui les porte jusqu'au cadre
// ne déborde de sa hauteur, et chacun tient dans la boîte visible de // ne déborde de sa hauteur, et chacun tient dans la boîte visible de
// chaque ascendant qui coupe ce qui le dépasse. // chaque ascendant qui coupe ce qui le dépasse, la zone des annonces la
// première. Le manque d'une place, de quatre et de treize : le nombre et
// l'accord allongent la phrase.
for (const largeur of [1366, 1024]) { for (const largeur of [1366, 1024]) {
test(`à ${largeur} × 700, le texte de PLACES_MANQUANTES et « Ouvrir les tables » se lisent en entier : aucune boîte ne les rogne jusqu’au cadre`, async () => { for (const manque of [1, 4, 13]) {
await fixerCadre(largeur, 700); test(`à ${largeur} × 700, il manque ${manque} : le texte de PLACES_MANQUANTES et le bouton « Ouvrir les tables » se lisent en entier, sans défiler`, async () => {
const m = await neuf(); await fixerCadre(largeur, 700);
await enManque(m); const m = await neuf();
const message = await attendre(() => { await enManqueDe(m, manque);
const element = dans(m, '[data-annonces] [data-avertissement="PLACES_MANQUANTES"]'); const message = await attendre(() => {
expect(element).not.toBeNull(); const element = dans(m, '[data-annonces] [data-avertissement="PLACES_MANQUANTES"]');
return element; expect(element).not.toBeNull();
}); return element;
const texte = message.querySelector('p'); });
const action = message.querySelector('[data-commande]'); const texte = message.querySelector('p');
expect(texte.textContent).toBe(t('PLACES_MANQUANTES', { placesManquantes: 1 })); const action = message.querySelector('button[data-commande]');
expect(action).not.toBeNull(); expect(texte.textContent).toBe(t('PLACES_MANQUANTES', { placesManquantes: manque }));
const borne = cadre(m); expect(action).not.toBeNull();
for (let element = texte; element !== borne.parentElement; element = element.parentElement) { expect(action.textContent.trim()).toBe(t('communs.remede.ouvrirSection.tables'));
const nom = `${element.tagName.toLowerCase()}${element.hasAttribute('data-annonces') ? '[data-annonces]' : ''}`; // La zone des annonces ne défile pas, et le bouton tient dans sa
expect(element.scrollHeight, `${nom} : hauteur du contenu`).toBeLessThanOrEqual(element.clientHeight + 1); // boîte visible.
if (element === texte || style(element).overflowY === 'visible') continue; const zone = message.closest('[data-annonces]');
const b = boite(element); expect(zone.scrollHeight, 'zone : hauteur du contenu').toBeLessThanOrEqual(zone.clientHeight + 1);
const bas = b.top + element.clientTop + element.clientHeight; expect(zone.scrollTop).toBe(0);
for (const [lu, porte] of [['texte', texte], ['action', action]]) { const z = boite(zone);
expect(boite(porte).top, `${lu} dans ${nom}`).toBeGreaterThanOrEqual(b.top + element.clientTop - 0.5); const a = boite(action);
expect(boite(porte).bottom, `${lu} dans ${nom}`).toBeLessThanOrEqual(bas + 0.5); expect(a.top, 'bouton sous le haut de la zone').toBeGreaterThanOrEqual(z.top + zone.clientTop - 0.5);
expect(a.bottom, 'bouton au-dessus du bas visible de la zone').toBeLessThanOrEqual(z.top + zone.clientTop + zone.clientHeight + 0.5);
const borne = cadre(m);
for (let element = texte; element !== borne.parentElement; element = element.parentElement) {
const nom = `${element.tagName.toLowerCase()}${element.hasAttribute('data-annonces') ? '[data-annonces]' : ''}`;
expect(element.scrollHeight, `${nom} : hauteur du contenu`).toBeLessThanOrEqual(element.clientHeight + 1);
if (element === texte || style(element).overflowY === 'visible') continue;
const b = boite(element);
const bas = b.top + element.clientTop + element.clientHeight;
for (const [lu, porte] of [['texte', texte], ['bouton', action]]) {
expect(boite(porte).top, `${lu} dans ${nom}`).toBeGreaterThanOrEqual(b.top + element.clientTop - 0.5);
expect(boite(porte).bottom, `${lu} dans ${nom}`).toBeLessThanOrEqual(bas + 0.5);
}
} }
} expect(visible(action)).toBe(true);
expect(visible(action)).toBe(true); expect(document.documentElement.scrollHeight).toBeLessThanOrEqual(innerHeight);
expect(document.documentElement.scrollHeight).toBeLessThanOrEqual(innerHeight); expect(document.documentElement.scrollWidth).toBeLessThanOrEqual(innerWidth);
expect(document.documentElement.scrollWidth).toBeLessThanOrEqual(innerWidth); });
}); }
} }
}); });