fix(tableaux): scroll contenu dans dashContainer, plus de scrollbar page #82

Remplace le scroll de page (window.scrollTo) par dashContainer.scrollLeft :
- overflow-x:hidden sur .dash-spreadsheet-container → la table ne déborde
  plus la page → scrollbar navigateur en bas éliminée
- overflow-y:clip évite la conversion CSS visible→auto (pas de scrollbar
  vertical parasite sur le conteneur)
- metrics(), syncThumb, drag et clic utilisent tous dashContainer
- wheel handler capture les gestures trackpad horizontaux et les redirige
  vers dashContainer.scrollLeft

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Colin Maudry
2026-06-23 19:20:41 +02:00
parent 3c0cf6bc07
commit 2e22fcabe9
2 changed files with 45 additions and 39 deletions
+8 -2
View File
@@ -361,8 +361,14 @@ table.cell-table th {
/* ===== Tableaux : en-têtes collants + scroll horizontal (#82) ===== */ /* ===== Tableaux : en-têtes collants + scroll horizontal (#82) ===== */
/* Neutraliser l'overflow interne de Dash pour que le sticky se cale sur la page */ /* Contenir le scroll horizontal dans le conteneur Dash (élimine la scrollbar native en bas de page) */
.marches_table .dash-spreadsheet-container, /* overflow-y:clip évite la conversion CSS visible→auto qui ajouterait une scrollbar verticale */
.marches_table .dash-spreadsheet-container {
overflow-x: hidden !important;
overflow-y: clip !important;
}
/* L'inner reste visible pour que le tableau se déploie librement en largeur */
.marches_table .dash-spreadsheet-inner { .marches_table .dash-spreadsheet-inner {
overflow: visible !important; overflow: visible !important;
} }
+37 -37
View File
@@ -19,30 +19,24 @@
bar.appendChild(thumb); bar.appendChild(thumb);
wrapper.insertBefore(bar, wrapper.firstChild); wrapper.insertBefore(bar, wrapper.firstChild);
// Étend la barre à toute la largeur du viewport, hors du container Bootstrap. // Métriques basées sur le conteneur scrollable (pas la page).
// wrapper.getBoundingClientRect().left donne le décalage horizontal du conteneur. // dashContainer a overflow-x:hidden → scrollLeft est contrôlable par JS.
const adjustBarWidth = () => {
const left = wrapper.getBoundingClientRect().left;
bar.style.marginLeft = -left + "px";
bar.style.width = window.innerWidth + "px";
};
// Métriques basées sur window.innerWidth (la barre occupe exactement la largeur du viewport).
const metrics = () => { const metrics = () => {
const total = document.documentElement.scrollWidth; const total = dashContainer.scrollWidth;
const visible = window.innerWidth; const visible = dashContainer.clientWidth;
const thumbW = Math.max(40, (visible / total) * visible); const thumbW = Math.max(40, (visible / total) * visible);
const scrollRange = total - visible; const scrollRange = total - visible;
const thumbRange = visible - thumbW; const thumbRange = visible - thumbW;
return { total, visible, thumbW, scrollRange, thumbRange }; return { total, visible, thumbW, scrollRange, thumbRange };
}; };
// --- Mise à jour de la position du thumb --- // Mise à jour de la position du thumb selon dashContainer.scrollLeft.
const syncThumb = () => { const syncThumb = () => {
const { total, visible, thumbW, scrollRange, thumbRange } = metrics(); const { total, visible, thumbW, scrollRange, thumbRange } = metrics();
if (total <= visible + 1 || thumbRange <= 0) return; if (total <= visible + 1 || thumbRange <= 0) return;
thumb.style.width = thumbW + "px"; thumb.style.width = thumbW + "px";
const fraction = scrollRange > 0 ? window.scrollX / scrollRange : 0; const fraction =
scrollRange > 0 ? dashContainer.scrollLeft / scrollRange : 0;
thumb.style.left = Math.round(fraction * thumbRange) + "px"; thumb.style.left = Math.round(fraction * thumbRange) + "px";
}; };
@@ -52,16 +46,16 @@
const startDrag = (clientX) => { const startDrag = (clientX) => {
dragStartX = clientX; dragStartX = clientX;
dragScrollStart = window.scrollX; dragScrollStart = dashContainer.scrollLeft;
}; };
const moveDrag = (clientX) => { const moveDrag = (clientX) => {
if (dragStartX === null) return; if (dragStartX === null) return;
const dx = clientX - dragStartX; const dx = clientX - dragStartX;
const { scrollRange, thumbRange } = metrics(); const { scrollRange, thumbRange } = metrics();
if (thumbRange <= 0) return; if (thumbRange <= 0) return;
window.scrollTo( dashContainer.scrollLeft = Math.max(
dragScrollStart + (dx / thumbRange) * scrollRange, 0,
window.scrollY Math.min(scrollRange, dragScrollStart + (dx / thumbRange) * scrollRange)
); );
}; };
const endDrag = () => { const endDrag = () => {
@@ -98,31 +92,40 @@
// Clic sur le track (hors thumb) : saute à la position cliquée. // Clic sur le track (hors thumb) : saute à la position cliquée.
bar.addEventListener("click", (e) => { bar.addEventListener("click", (e) => {
if (e.target === thumb) return; if (e.target === thumb) return;
const rect = bar.getBoundingClientRect();
const { scrollRange, thumbW, thumbRange } = metrics(); const { scrollRange, thumbW, thumbRange } = metrics();
const fraction = Math.max( const fraction = Math.max(
0, 0,
Math.min( Math.min(1, (e.clientX - rect.left - thumbW / 2) / thumbRange)
1,
(e.clientX - bar.getBoundingClientRect().left - thumbW / 2) /
thumbRange
)
); );
window.scrollTo(fraction * scrollRange, window.scrollY); dashContainer.scrollLeft = fraction * scrollRange;
}); });
// Synchronise le thumb quand la page défile (clavier, molette, barre native, etc.). // Scroll molette/trackpad horizontal → redirigé vers le conteneur.
// SPA : ce listener est intentionnellement conservé pour toute la durée de vie de la page. // SPA : ce listener est intentionnellement conservé pour toute la durée de vie de la page.
window.addEventListener("scroll", syncThumb); wrapper.addEventListener(
"wheel",
(e) => {
if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;
e.preventDefault();
dashContainer.scrollLeft = Math.max(
0,
Math.min(
dashContainer.scrollWidth - dashContainer.clientWidth,
dashContainer.scrollLeft + e.deltaX
)
);
},
{ passive: false }
);
// Synchronise le thumb quand le conteneur défile (drag, wheel, ou autre).
// SPA : ce listener est intentionnellement conservé pour toute la durée de vie de la page.
dashContainer.addEventListener("scroll", syncThumb);
const refresh = () => { const refresh = () => {
adjustBarWidth(); const hasOverflow =
// Mesure la largeur réelle de CETTE table (pas de la page entière) pour dashContainer.scrollWidth > dashContainer.clientWidth + 1;
// ne pas afficher la barre sur les petits tableaux d'une page qui a aussi
// un grand tableau (ex. top10 sur /acheteur, /titulaire, /observatoire).
const tableEl =
dashContainer.querySelector(".cell-table") || dashContainer;
const tableWidth = tableEl.getBoundingClientRect().width;
const hasOverflow = tableWidth > window.innerWidth + 1;
bar.classList.toggle("is-hidden", !hasOverflow); bar.classList.toggle("is-hidden", !hasOverflow);
if (hasOverflow) syncThumb(); if (hasOverflow) syncThumb();
}; };
@@ -135,10 +138,7 @@
attributes: true, attributes: true,
}); });
// SPA : ce listener est intentionnellement conservé pour toute la durée de vie de la page. // SPA : ce listener est intentionnellement conservé pour toute la durée de vie de la page.
window.addEventListener("resize", () => { window.addEventListener("resize", refresh);
adjustBarWidth();
refresh();
});
refresh(); refresh();
} }