From 0ed807f98ce0eda1a4ab76eebe7e31b6ce36efa2 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Tue, 23 Jun 2026 15:08:47 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20design=20scroll=20horizontal=20+=20en-t?= =?UTF-8?q?=C3=AAtes=20sticky=20des=20tableaux=20#82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- ...ableaux-scroll-horizontal-sticky-design.md | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-23-tableaux-scroll-horizontal-sticky-design.md diff --git a/docs/superpowers/specs/2026-06-23-tableaux-scroll-horizontal-sticky-design.md b/docs/superpowers/specs/2026-06-23-tableaux-scroll-horizontal-sticky-design.md new file mode 100644 index 0000000..071e7b7 --- /dev/null +++ b/docs/superpowers/specs/2026-06-23-tableaux-scroll-horizontal-sticky-design.md @@ -0,0 +1,117 @@ +# Défilement horizontal ergonomique des tableaux (#82) + +## Problème + +Les tableaux de données (`DataTable` Dash) sont souvent plus larges que l'écran et +**débordent vers la droite**. Aujourd'hui aucun `overflowX` n'est défini sur leur +conteneur : le tableau étire la page entière, et le seul moyen de faire défiler +horizontalement est la **barre de défilement de la fenêtre du navigateur**, tout en +bas du viewport. Cette barre : + +- est discrète et fait défiler **toute la page** (pas seulement le tableau) ; +- n'est pas comprise comme « le moyen de voir le reste du tableau ». + +De plus, les tableaux dépassent souvent du bas de l'écran : une barre placée en bas +du tableau serait invisible sans scroller. + +## Objectif + +Rendre le défilement horizontal **évident et toujours accessible**, et garder les +**en-têtes de colonnes visibles** pendant le défilement vertical, sans introduire de +zone scrollable imbriquée gênante. + +## Approche retenue (option B — sticky au niveau page) + +Le tableau **reste dans le flux de la page** (pas de conteneur à hauteur fixe, pas de +scroll imbriqué). On combine deux mécanismes : + +1. **En-têtes collants** — `position: sticky; top: 0` sur la ligne d'en-tête du + tableau. Quand l'utilisateur descend dans la page, les en-têtes se figent en haut + de la fenêtre au lieu d'être « avalés ». + +2. **Barre de défilement horizontale miroir en haut** — un petit élément placé + juste au-dessus du tableau, lui aussi `sticky` en haut, dont le défilement + horizontal est **synchronisé** avec celui du tableau. Elle est donc toujours + visible dès qu'on voit le haut du tableau, et pilote le défilement horizontal sans + devoir descendre en bas du tableau. + +La barre miroir et les en-têtes collants se figent ensemble en haut de la fenêtre : +l'utilisateur garde en permanence le repère des colonnes **et** le contrôle du +défilement horizontal. + +### Pourquoi pas l'option A (tableau « fenêtré » à hauteur fixe) + +Écartée volontairement : un conteneur à hauteur fixe avec scroll interne crée un +**scroll imbriqué** (la molette agit d'abord sur le tableau, pas sur la page), source +de confusion. L'option B garde un comportement de défilement vertical unique (celui +de la page) ; seul le défilement horizontal est « custom ». + +## Contrainte technique CSS à gérer + +Un conteneur en `overflow-x: auto` devient automatiquement un conteneur de +défilement **vertical** (règle CSS : `overflow-y: visible` recalculé en `auto` dès +que l'autre axe n'est pas `visible`), ce qui **casse** le `position: sticky; top: 0` +des en-têtes par rapport à la page. + +Conséquences pour l'implémentation : + +- Le **défilement horizontal réel** doit se faire dans un conteneur dédié en + `overflow-x: auto` ; mais ce conteneur ne peut pas, en même temps, héberger des + en-têtes sticky « page ». La barre miroir du haut résout ce conflit : c'est **elle** + qui porte le `overflow-x: auto`, séparée du tableau, et synchronisée par JS. +- Il faudra **vérifier et neutraliser au besoin l'`overflow` interne** que Dash + DataTable applique à ses propres conteneurs (`.dash-spreadsheet-container`, + `.dash-spreadsheet-inner`) pour que le sticky des en-têtes fonctionne. +- Le rendu réel de Dash DataTable doit être inspecté avant de figer le CSS : la + structure DOM exacte (où poser `sticky`, quel élément porte la largeur totale) + conditionne la solution. **À valider en testant dans le navigateur.** + +## Composants + +| Élément | Rôle | Emplacement probable | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| CSS en-têtes sticky | `position: sticky; top: 0` sur la ligne d'en-tête, z-index, fond opaque | `src/assets/css/style.css` (cible `.marches_table`) | +| Barre miroir (markup) | `
` scrollable au-dessus du tableau, avec un enfant à la largeur du tableau | composant partagé `DataTable` / wrapper dans `src/figures.py` | +| Synchronisation JS | lier scrollLeft barre ↔ tableau ; recopier la largeur du tableau dans la barre ; recalcul au resize / changement de données | nouveau fichier `src/assets/js/*.js` (assets Dash, chargé automatiquement) | +| CSS barre miroir | hauteur, sticky `top: 0`, masquage si pas de débordement | `src/assets/css/style.css` | + +## Portée + +Les **quatre pages** utilisant `className="marches_table"` : +`/tableau`, `/acheteur`, `/titulaire`, `/observatoire`. La solution passe par le +composant `DataTable` partagé et la classe CSS `marches_table`, donc l'effort est +quasi identique pour une ou quatre pages. + +## Flux de données / interactions + +1. Au rendu (et à chaque changement de données / largeur de fenêtre), le JS mesure la + largeur totale du tableau et la reporte dans l'élément interne de la barre miroir → + la barre miroir affiche une glissière proportionnelle. +2. Événement `scroll` sur la barre miroir → on applique `scrollLeft` au conteneur du + tableau ; et inversement (scroll du tableau → barre miroir), avec garde anti-boucle. +3. Si le tableau ne déborde pas, la barre miroir est masquée. + +## Cas limites + +- **Pas de débordement** : barre miroir masquée, en-têtes sticky inoffensifs. +- **Pagination / re-render** (pages en `page_action="custom"`) : la largeur peut + changer → la synchro doit se recalculer après mise à jour des données. +- **Resize de la fenêtre** : recalcul de la largeur miroir. +- **Plusieurs tableaux sur une page** (`/observatoire`, `/titulaire`, `/acheteur` ont + plusieurs `marches_table`) : le JS doit gérer chaque tableau indépendamment. +- **Persistance / tri / filtre** : ne doit pas casser la synchro (réattacher les + écouteurs si le DOM est recréé). + +## Tests / validation + +- Vérification **manuelle dans le navigateur** (point critique vu l'incertitude sur le + DOM de DataTable) : débordement horizontal sur `/tableau`, sticky des en-têtes en + scrollant, synchro des deux barres, comportement sur les pages à tableaux multiples. +- S'assurer que les tests Selenium existants ne régressent pas + (`pytest tests/test_main.py`). + +## Hors périmètre (YAGNI) + +- Colonnes figées (1re colonne sticky horizontalement). +- Réduction du nombre de colonnes par défaut / refonte du sélecteur de colonnes. +- `overscroll-behavior` et zones scrollables imbriquées (option A écartée).