UNPKG

7.78 kBJavaScriptView Raw
1import { useMemo, useReducer, useCallback, useEffect } from 'react';
2
3import withLogger from '../util/withLogger';
4
5/**
6 * A [React Hook]{@link https://reactjs.org/docs/hooks-intro.html} that contains
7 * logic for handling a list of items.
8 *
9 * It returns the state of the list and an API object for managing the items in the list.
10 *
11 * @typedef {function} useListState
12 *
13 * @param {Object} config - an object containing:
14 * @param {Func} getItemKey - A function to get an Item's key.
15 * @param {Array} initialSelection - An array of keys that should be selected.
16 * @param {string} selectionModel - The list's selection type (radio or checkbox).
17 * @param {function(Set):void} onSelectionChange - function to be called when the List selection changes.
18 *
19 * @return {Object[]} An array with two entries containing the following content: [ {@link ListState}, {@link API}]
20 */
21export const useListState = ({
22 getItemKey,
23 initialSelection,
24 onSelectionChange,
25 selectionModel
26}) => {
27 const initialState = useMemo(
28 () => getInitialState({ getItemKey, initialSelection, selectionModel }),
29 [getItemKey, initialSelection, selectionModel]
30 );
31
32 const [state, dispatch] = useReducer(wrappedReducer, initialState);
33 const { selectedKeys } = state;
34
35 // Whenever the selectedKeys changes, notify.
36 useEffect(() => {
37 if (onSelectionChange) {
38 onSelectionChange(selectedKeys);
39 }
40 }, [onSelectionChange, selectedKeys]);
41
42 /*
43 * Functions of the API.
44 */
45
46 /**
47 * Function to remove focus on any item if it has focus.
48 *
49 * @typedef {function} removeFocus
50 *
51 * @param {void}
52 * @returns {void}
53 */
54 const removeFocus = useCallback(
55 () => dispatch({ type: 'REMOVE_FOCUS' }),
56 []
57 );
58
59 /**
60 * Function to set focus on a given item in the list.
61 *
62 * @typedef {function} setFocus
63 *
64 * @param {Key} key - Key of the item to set focus on.
65 * @returns {void}
66 */
67 const setFocus = useCallback(
68 key => dispatch({ type: 'SET_FOCUS', payload: { key } }),
69 []
70 );
71
72 /**
73 * Function to update the selected keys.
74 *
75 * @typedef {function} updateSelectedKeys
76 *
77 * @param {Key} key - The key of the item in the list to select (or deselect).
78 * @returns {void}
79 */
80 const updateSelectedKeys = useCallback(
81 key =>
82 dispatch({
83 type: 'UPDATE_SELECTED_KEYS',
84 payload: { key, selectionModel }
85 }),
86 [selectionModel]
87 );
88
89 /**
90 * The API for managing the Items inside the List.
91 *
92 * This object should never change.
93 * @typedef {Object} API
94 *
95 * @property {setFocus} setFocus
96 * @property {removeFocus} removeFocus
97 * @property {updateSelectedKeys} updateSelectedKeys
98 */
99
100 const api = useMemo(
101 () => ({
102 setFocus,
103 removeFocus,
104 updateSelectedKeys
105 }),
106 [setFocus, removeFocus, updateSelectedKeys]
107 );
108
109 return [state, api];
110};
111
112/**
113 * Reducer function that is used by the useReducer hook inside the useListState hook.
114 *
115 * @function reducer
116 *
117 * @param {ListState} state
118 * @param {Object} action - object that contains:
119 * @param {string} action.type
120 * @param {Object} action.payload - object that contains:
121 * @param {Key} action.payload.key
122 * @param {string} action.payload.selectionModel
123 */
124const reducer = (state, { type, payload }) => {
125 const { selectedKeys } = state;
126
127 switch (type) {
128 case 'REMOVE_FOCUS':
129 return {
130 ...state,
131 hasFocus: false
132 };
133 case 'SET_FOCUS':
134 return {
135 ...state,
136 hasFocus: true,
137 cursor: payload.key
138 };
139 case 'UPDATE_SELECTED_KEYS': {
140 const { key, selectionModel } = payload;
141 const newSelectedKeys = updateSelectedKeysInternal(
142 key,
143 selectedKeys,
144 selectionModel
145 );
146
147 return {
148 ...state,
149 selectedKeys: newSelectedKeys
150 };
151 }
152 default:
153 return state;
154 }
155};
156
157const wrappedReducer = withLogger(reducer);
158
159/**
160 * Helper function to update the List's Set of selected keys.
161 *
162 * @function getInitialState
163 *
164 * @param {Object} options - an object containing:
165 * @param {Func} getItemKey - Get an item's key.
166 * @param {Array} initialSelection - An array of keys that should be selected initially.
167 * @param {string} selectionModel
168 *
169 * @returns {Object} - {@link ListState}
170 */
171const getInitialState = ({ getItemKey, initialSelection, selectionModel }) => {
172 const initiallySelectedKeys = getInitiallySelectedKeys({
173 getItemKey,
174 initialSelection,
175 selectionModel
176 });
177
178 return {
179 cursor: null,
180 hasFocus: false,
181 selectedKeys: new Set(initiallySelectedKeys)
182 };
183};
184
185/**
186 * Helper function to validate and set the initial list of selected keys.
187 *
188 * @param {Object} options - an object containing:
189 * @param {Func} getItemKey - Get an item's key.
190 * @param {Array} initialSelection - An array of keys that should be selected initially.
191 * @param {string} selectionModel
192 * @returns {Array} an array containing initial item keys
193 */
194const getInitiallySelectedKeys = ({
195 getItemKey,
196 initialSelection,
197 selectionModel
198}) => {
199 if (!initialSelection) {
200 return null;
201 }
202
203 // We store the keys of each item that is initially selected,
204 // but we must also respect the selection model.
205 if (selectionModel === 'radio') {
206 // Only one thing can be selected at a time.
207 const target = Array.isArray(initialSelection)
208 ? initialSelection[0]
209 : initialSelection;
210
211 const itemKey = getItemKey(target);
212
213 if (itemKey) {
214 return [itemKey];
215 }
216
217 return [];
218 }
219
220 if (selectionModel === 'checkbox') {
221 // Multiple things can be selected at a time.
222
223 // Do we have multiple things?
224 if (Array.isArray(initialSelection)) {
225 return initialSelection.map(getItemKey);
226 }
227
228 const itemKey = getItemKey(initialSelection);
229
230 if (itemKey) {
231 return [itemKey];
232 }
233
234 return [];
235 }
236};
237
238/**
239 * Helper function to update the List's Set of selected keys.
240 *
241 * @function updateSelectedKeysInternal
242 *
243 * @param {Key} key - The key to update (add to or remove from) the Set.
244 * @param {Set} selectedKeys - The keys that are currently in the Set.
245 * @param {Set} selectionModel - One of "radio" or "checkbox".
246 * Informs whether multiple keys can be selected at the same time.
247 *
248 * @returns {Set} - The new Set of selectedKeys.
249 */
250const updateSelectedKeysInternal = (key, selectedKeys, selectionModel) => {
251 let newSelectedKeys;
252
253 if (selectionModel === 'radio') {
254 // For radio, only one item can be selected at a time.
255 newSelectedKeys = new Set().add(key);
256 }
257
258 if (selectionModel === 'checkbox') {
259 newSelectedKeys = new Set(selectedKeys);
260
261 if (!newSelectedKeys.has(key)) {
262 // The item is being selected.
263 newSelectedKeys.add(key);
264 } else {
265 // The item is being deselected.
266 newSelectedKeys.delete(key);
267 }
268 }
269
270 return newSelectedKeys;
271};
272
273// Custom Type Definitions
274
275/**
276 * Item's key type.
277 *
278 * @typedef {(string|number)} Key
279 */
280
281/**
282 * The current state of the List.
283 *
284 * @typedef {Object} ListState
285 *
286 * @property {Key} cursor
287 * @property {boolean} hasFocus
288 * @property {Set} selectedKeys
289 */