From 6f81d3f3c6fab315a3c399bf971f0f0caaa7d038 Mon Sep 17 00:00:00 2001 From: TPReal Date: Mon, 30 Oct 2023 15:44:41 +0100 Subject: [PATCH] Allow scrolling the table horizontally by using the mouse wheel on the table header. --- resources/js/components/ui/Table/Table.tsx | 81 ++++++++++++++++++++-- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/resources/js/components/ui/Table/Table.tsx b/resources/js/components/ui/Table/Table.tsx index b2566b58a..e8ee4c4ff 100644 --- a/resources/js/components/ui/Table/Table.tsx +++ b/resources/js/components/ui/Table/Table.tsx @@ -13,8 +13,28 @@ import { getPaginationRowModel, getSortedRowModel, } from "@tanstack/solid-table"; -import {LangEntryFunc, LangPrefixFunc, createTranslationsFromPrefix, cx} from "components/utils"; -import {Accessor, For, Index, JSX, Show, Signal, VoidProps, createEffect, createSignal, mergeProps, on} from "solid-js"; +import { + LangEntryFunc, + LangPrefixFunc, + createTranslationsFromPrefix, + currentTime, + cx, + debouncedAccessor, +} from "components/utils"; +import { + Accessor, + For, + Index, + JSX, + Show, + Signal, + VoidProps, + createEffect, + createMemo, + createSignal, + mergeProps, + on, +} from "solid-js"; import {Dynamic} from "solid-js/web"; import {TableContextProvider, getHeaders, useTableCells} from "."; import {BigSpinner} from "../Spinner"; @@ -123,12 +143,54 @@ export const Table = (allProps: VoidProps>): JSX.Element => { .getVisibleLeafColumns() .map((c) => (!c.getCanResize() && c.getSize() === AUTO_SIZE_COLUMN_DEFS.size ? "auto" : `${c.getSize()}px`)) .join(" "); + // Implement horizontal scrolling on mouse wheel on the table header. + // The simplest implementation of calling scrollBy in the onWheel handler does not work well if + // smooth scrolling is used. That's why this code tracks the desired position in a signal and scrolls + // to it when no scrolling is taking place at the moment. + const [scrollingWrapper, setScrollingWrapper] = createSignal(); + const [lastScrollTimestamp, setLastScrollTimestamp] = createSignal(0); + // Whether the table is currently scrolling. This is true after setting lastScrollTimestamp to 0 + // in onScrollEnd, but also after enough time is elapsed since the last onScroll event, because in some + // situations the onScrollEnd event is not reliable, and we don't want to get stuck thinking the table + // is still scrolling when it's not. + const isScrolling = createMemo(() => currentTime().toMillis() - lastScrollTimestamp() < 100); + const [desiredScrollX, setDesiredScrollX] = createSignal(); + createEffect( + on( + [ + scrollingWrapper, + isScrolling, + // Allow multiple steps to accummulate before this is triggered. This improves smoothness. + // eslint-disable-next-line solid/reactivity + debouncedAccessor(desiredScrollX, { + timeMs: 100, + outputImmediately: (x) => x === undefined, + }), + ], + ([scrWrapper, isScrolling]) => { + if (scrWrapper && !isScrolling) { + // Use the most up-to-date value of desiredScrollX, not the debounced one. + const desiredX = desiredScrollX(); + if (desiredX !== undefined && desiredX !== scrWrapper.scrollLeft) { + scrWrapper.scrollTo({left: desiredX, behavior: "smooth"}); + } else { + setDesiredScrollX(undefined); + } + } + }, + ), + ); return ( }>
{(aboveTable) =>
{aboveTable()}
}
-
+
setLastScrollTimestamp(Date.now())} + onScrollEnd={() => setLastScrollTimestamp(0)} + >
(allProps: VoidProps>): JSX.Element => { {({header, column}) => ( {(header) => ( -
+
{ + const scrWrapper = scrollingWrapper(); + if (scrWrapper && e.deltaY) { + setDesiredScrollX((l = scrWrapper.scrollLeft) => + Math.min(Math.max(l + e.deltaY, 0), scrWrapper.scrollWidth - scrWrapper.clientWidth), + ); + e.preventDefault(); + } + }} + >