improvement(scroll): fade every capped list's clipped edges
The sweep's own three (the thread, the Plex collections filter, the tag manager) plus the seven the spec missed — the playlist picker, the row tag menu, the Plex playlist/collection dialogs, the player's track menu, the video description and the table's multi-select filter. Same idiom throughout: a height-capped list that silently clipped its content now says so, and hides its scrollbar like the rail and the panels already do. The table's option list is its own module-level component because it owns a hook: FilterPopover is redeclared per DataTable render, so React remounts it.
This commit is contained in:
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ArrowDown, ArrowUp, ChevronLeft, ChevronRight, ListFilter, X } from "lucide-react";
|
||||
import { useDismiss } from "../lib/useDismiss";
|
||||
import { useScrollFade } from "../lib/useScrollFade";
|
||||
|
||||
// A reusable, client-side data table: per-column sort + filter (in the header) + pagination,
|
||||
// with its sort/filter/page state optionally persisted to localStorage so a reload (F5) keeps
|
||||
@@ -9,6 +10,22 @@ import { useDismiss } from "../lib/useDismiss";
|
||||
// other modules (e.g. the playlist manager) can reuse it. Below `md` it falls back to a card
|
||||
// list built from the same column definitions.
|
||||
|
||||
// The multi-select filter's option list, capped and scrollable. Its own component (module level,
|
||||
// not nested in DataTable) because it owns a hook: FilterPopover is redeclared on every DataTable
|
||||
// render, so React remounts it — state parked in there would churn.
|
||||
function FilterOptionList({ children }: { children: ReactNode }) {
|
||||
const fade = useScrollFade();
|
||||
return (
|
||||
<div
|
||||
ref={fade.ref}
|
||||
style={fade.style}
|
||||
className="flex flex-col gap-0.5 max-h-60 overflow-y-auto no-scrollbar"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ColumnFilter<T> =
|
||||
| { kind: "text"; get: (row: T) => string }
|
||||
| { kind: "select"; options: { value: string; label: string }[]; test: (row: T, value: string) => boolean }
|
||||
@@ -218,7 +235,7 @@ export default function DataTable<T>({
|
||||
</div>
|
||||
)}
|
||||
{f.kind === "multi" && (
|
||||
<div className="flex flex-col gap-0.5 max-h-60 overflow-y-auto">
|
||||
<FilterOptionList>
|
||||
{f.options.length === 0 && (
|
||||
<span className="text-xs text-muted px-2 py-1">{t("datatable.noOptions")}</span>
|
||||
)}
|
||||
@@ -249,7 +266,7 @@ export default function DataTable<T>({
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FilterOptionList>
|
||||
)}
|
||||
{isActive(val) && (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user