Skip to content

Commit

Permalink
Merge pull request #153 from mblajek/horizontal-scroll
Browse files Browse the repository at this point in the history
Allow scrolling the table horizontally by using the mouse wheel on the table header.
  • Loading branch information
TPReal authored Nov 2, 2023
2 parents 38c6881 + 6f81d3f commit 4a2d907
Showing 1 changed file with 77 additions and 4 deletions.
81 changes: 77 additions & 4 deletions resources/js/components/ui/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -123,12 +143,54 @@ export const Table = <T,>(allProps: VoidProps<Props<T>>): 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<HTMLDivElement>();
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<number>();
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 (
<TableContextProvider table={props.table}>
<Show when={!props.isLoading} fallback={<BigSpinner />}>
<div class={cx(s.tableContainer, s[props.mode])}>
<Show when={props.aboveTable?.()}>{(aboveTable) => <div class={s.aboveTable}>{aboveTable()}</div>}</Show>
<div class={s.scrollingWrapper}>
<div
ref={setScrollingWrapper}
class={s.scrollingWrapper}
onScroll={() => setLastScrollTimestamp(Date.now())}
onScrollEnd={() => setLastScrollTimestamp(0)}
>
<div ref={scrollToTopPoint} class={s.tableBg}>
<div
class={cx(s.table, {[s.dimmed!]: props.isDimmed})}
Expand All @@ -139,7 +201,18 @@ export const Table = <T,>(allProps: VoidProps<Props<T>>): JSX.Element => {
{({header, column}) => (
<Show when={header()}>
{(header) => (
<div class={s.cell}>
<div
class={s.cell}
onWheel={(e) => {
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();
}
}}
>
<Show when={!header().isPlaceholder}>
<CellRenderer component={column.columnDef.header} props={header().getContext()} />
</Show>
Expand Down

0 comments on commit 4a2d907

Please sign in to comment.