89 lines
2.8 KiB
TypeScript
89 lines
2.8 KiB
TypeScript
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<ReadonlySet<number>>(() => 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,
|
|
};
|
|
}
|