1 import type { ChangeEvent, DependencyList } from 'react';
2 import { useEffect, useMemo, useState } from 'react';
4 import useHandler from '@proton/components/hooks/useHandler';
5 import unique from '@proton/utils/unique';
10 resetDependencies?: DependencyList;
11 onCheck?: (checked: boolean) => void;
16 * Implement the selection logic shared between mail and contacts
17 * You have an active id which represents the selection if there is no checked items
18 * As soon as you have checked items, it replaces the active item
19 * Items can be any object, we only deal with IDs
20 * @param activeID The current active item
21 * @param allIDs The complete list of ids in the list
22 * @param resetDependencies React dependencies to reset selection if there is a change
23 * @param onCheck Optional action to be triggered when interacting with element checkboxes
24 * @param rowMode Used only for mail since we keep checkedMap state in row mode
25 * @returns all helpers useful to check one, a range or all items
27 const useItemsSelection = ({ activeID, allIDs, rowMode, resetDependencies, onCheck }: Props) => {
28 // We are managing checked IDs through a Map and not an array for performance issues.
29 const [checkedMap, setCheckedMap] = useState<{ [ID: string]: boolean }>({});
31 // Last item check to deal with range selection
32 const [lastChecked, setLastChecked] = useState<string>('');
34 const isChecked = (ID: string) => !!checkedMap[ID];
36 useEffect(() => setCheckedMap({}), resetDependencies || []);
38 const checkedIDs = useMemo(() => {
39 return Object.keys(checkedMap).filter((ID) => checkedMap[ID]);
42 const selectedIDs = useMemo(() => {
43 // In row mode, activeID has priority over checkedIDs
44 if (activeID && rowMode) {
47 if (checkedIDs.length) {
54 }, [checkedIDs, activeID]);
57 * Put *IDs* to *checked* state
58 * Uncheck others if *replace* is true
60 const handleCheck = useHandler((IDs: string[], checked: boolean, replace: boolean) => {
61 // Run onCheck function when interacting with a checkbox
63 // Items can be checked and included in a new selection (select all/select range).
64 // In that case they will be duplicated in the array, which could break the length we expect in the 2nd condition
65 const uniqueIDs = unique(IDs);
66 if (uniqueIDs.length === 0) {
68 } else if (uniqueIDs.length === allIDs.length) {
70 allIDs.reduce<{ [ID: string]: boolean }>((acc, ID) => {
77 allIDs.reduce<{ [ID: string]: boolean }>((acc, ID) => {
78 const wasChecked = isChecked(ID);
79 const toCheck = IDs.includes(ID);
98 * Check or uncheck all items
100 const handleCheckAll = useHandler((check: boolean) =>
101 check ? handleCheck(allIDs, true, true) : handleCheck([], true, true)
105 * Just check the given id, nothing more
107 const handleCheckOnlyOne = useHandler((id: string) => {
108 handleCheck([id], !isChecked(id), false);
113 * Check all items from the last checked to the given id
115 const handleCheckRange = useHandler((id: string) => {
119 const start = allIDs.findIndex((ID) => ID === id);
120 const end = allIDs.findIndex((ID) => ID === lastChecked);
122 ids.push(...allIDs.slice(Math.min(start, end), Math.max(start, end) + 1));
125 handleCheck(ids, !isChecked(id), false);
130 * Check only one or check range depending on the shift key value in the event
132 const handleCheckOne = useHandler((event: ChangeEvent, id: string) => {
133 const { shiftKey } = event.nativeEvent as any;
136 handleCheckRange(id);
138 handleCheckOnlyOne(id);
142 // Automatically uncheck an id which is not anymore in the list (Happens frequently when using search)
144 const filteredCheckedIDs = checkedIDs.filter((id) => allIDs.includes(id));
146 if (filteredCheckedIDs.length !== checkedIDs.length) {
147 handleCheck(filteredCheckedIDs, true, true);
162 export default useItemsSelection;