[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;
}
/* 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
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

View file

@ -133,6 +133,17 @@ async function enManque(m) {
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 -----------------------------------------------------------------
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, 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
// 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]) {
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 () => {
await fixerCadre(largeur, 700);
const m = await neuf();
await enManque(m);
const message = await attendre(() => {
const element = dans(m, '[data-annonces] [data-avertissement="PLACES_MANQUANTES"]');
expect(element).not.toBeNull();
return element;
});
const texte = message.querySelector('p');
const action = message.querySelector('[data-commande]');
expect(texte.textContent).toBe(t('PLACES_MANQUANTES', { placesManquantes: 1 }));
expect(action).not.toBeNull();
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], ['action', 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);
for (const manque of [1, 4, 13]) {
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 () => {
await fixerCadre(largeur, 700);
const m = await neuf();
await enManqueDe(m, manque);
const message = await attendre(() => {
const element = dans(m, '[data-annonces] [data-avertissement="PLACES_MANQUANTES"]');
expect(element).not.toBeNull();
return element;
});
const texte = message.querySelector('p');
const action = message.querySelector('button[data-commande]');
expect(texte.textContent).toBe(t('PLACES_MANQUANTES', { placesManquantes: manque }));
expect(action).not.toBeNull();
expect(action.textContent.trim()).toBe(t('communs.remede.ouvrirSection.tables'));
// La zone des annonces ne défile pas, et le bouton tient dans sa
// boîte visible.
const zone = message.closest('[data-annonces]');
expect(zone.scrollHeight, 'zone : hauteur du contenu').toBeLessThanOrEqual(zone.clientHeight + 1);
expect(zone.scrollTop).toBe(0);
const z = boite(zone);
const a = boite(action);
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(document.documentElement.scrollHeight).toBeLessThanOrEqual(innerHeight);
expect(document.documentElement.scrollWidth).toBeLessThanOrEqual(innerWidth);
});
expect(visible(action)).toBe(true);
expect(document.documentElement.scrollHeight).toBeLessThanOrEqual(innerHeight);
expect(document.documentElement.scrollWidth).toBeLessThanOrEqual(innerWidth);
});
}
}
});