import { useCallback, useMemo, useState } from 'react'; /** * Which rows a person has ticked, and the header checkbox that follows. * * Shared by the three screens that grew a bulk action — importing from the * catalogue, requesting stock, and deciding requests — because the fiddly parts * are the same every time and getting them subtly different between screens is * how a merchant learns to distrust the tick boxes. * * ── The part that is easy to get wrong ────────────────────────────────────── * * A selection is kept against the ROWS CURRENTLY VISIBLE. Filter a list down, * tick everything, clear the filter, and press the button: a naive * implementation acts on rows the person could not see when they chose. So * "select all" only ever covers what is on screen, and `chosen` is intersected * with the visible ids before it is handed back. * * Ids that scroll out of view are NOT dropped from the set, because narrowing a * search and widening it again should not silently untick the work. They are * simply not acted on while they are hidden. */ export interface Selection { /** Visible ids that are ticked — what a bulk action should act on. */ chosen: number[]; count: number; has: (id: number) => boolean; toggle: (id: number) => void; /** Tick or untick everything currently visible. */ toggleAll: () => void; clear: () => void; /** Every visible row is ticked. Drives the header checkbox. */ allChosen: boolean; /** Some but not all — the indeterminate state. */ someChosen: boolean; } export function useSelection(visibleIds: readonly number[]): Selection { const [picked, setPicked] = useState>(() => new Set()); const chosen = useMemo( () => visibleIds.filter((id) => picked.has(id)), [visibleIds, picked], ); const toggle = useCallback((id: number) => { setPicked((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }, []); const allChosen = visibleIds.length > 0 && chosen.length === visibleIds.length; const toggleAll = useCallback(() => { setPicked((prev) => { const next = new Set(prev); const everyVisibleChosen = visibleIds.length > 0 && visibleIds.every((id) => next.has(id)); for (const id of visibleIds) { if (everyVisibleChosen) { next.delete(id); } else { next.add(id); } } return next; }); }, [visibleIds]); const clear = useCallback(() => setPicked(new Set()), []); return { chosen, count: chosen.length, has: (id: number) => picked.has(id), toggle, toggleAll, clear, allChosen, someChosen: chosen.length > 0 && !allChosen, }; }