Files
daily_console_web/src/components/useSelection.ts
2026-09-02 16:49:47 +05:30

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,
};
}