Files
colibre/docs/superpowers/specs/2026-06-23-tableaux-scroll-horizontal-sticky-design.md
Colin Maudry ec2b8389c3 docs: verdict spike DOM tableaux #82
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 15:44:11 +02:00

146 lines
8.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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) | `<div>` 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).
## Verdict du spike
Le spike (Steps 13 exécutés par l'utilisateur en DevTools) doit valider les points suivants. En cas de déviation, le reste du plan reste applicable ; seule l'implémentation du sticky (Task 2) ajustera sa stratégie.
### Éléments attendus du DOM
- **Conteneur scrollable** : `.dash-spreadsheet-container` (enfant direct de `.marches_table`)
- **Conteneur interne** : `.dash-spreadsheet-inner` (enfant de `.dash-spreadsheet-container`) — peut aussi porter un `overflow` interne
- **En-têtes** : sélecteur exact `th.dash-header` (dans un `tr` au sein du tableau)
- **Table complète** : `.cell-table` avec ses dimensions (`scrollWidth` >> `clientWidth` du parent → débordement confirmé)
### Hypothèse sticky
L'astuce CSS consiste à :
1. Neutraliser l'`overflow` sur `.dash-spreadsheet-container` et `.dash-spreadsheet-inner` en les ramenant à `overflow: visible` (ou en supprimant le style si possible)
2. Appliquer `position: sticky; top: 0; z-index: 10; background: #fff` aux en-têtes `th.dash-header`
**Verdict attendu :** ✅ Oui — les en-têtes restent collés au haut de la fenêtre quand on scroll verticalement la page, sans recours à du JS supplémentaire (hormis la synchro scrollLeft pour le miroir).
**Si verdict = ❌ Non :** les en-têtes seront pilotés entièrement en JS (repositionnement au scroll), avec synchronisation du scroll vertical. Le reste du plan (barre miroir, synchro horizontale) reste valable.
### Références (à noter lors du spike)
- `scrollWidth` et `clientWidth` de la table vs. ses parents
- Styles `overflow` en _Computed_ sur `.dash-spreadsheet-container`, `.dash-spreadsheet-inner` et `.cell-table`
- Résultat du test d'hypothèse JS (sticky page fonctionne-t-il ?)