UNPKG

12.4 kBJavaScriptView Raw
1import _extends from "@babel/runtime/helpers/esm/extends";
2import { ListActionTypes } from './listActions.types';
3/**
4 * Looks up the next valid item to highlight within the list.
5 *
6 * @param currentIndex The index of the start of the search.
7 * @param lookupDirection Whether to look for the next or previous item.
8 * @param items The array of items to search.
9 * @param includeDisabledItems Whether to include disabled items in the search.
10 * @param isItemDisabled A function that determines whether an item is disabled.
11 * @param wrapAround Whether to wrap around the list when searching.
12 * @returns The index of the next valid item to highlight or -1 if no valid item is found.
13 */
14function findValidItemToHighlight(currentIndex, lookupDirection, items, includeDisabledItems, isItemDisabled, wrapAround) {
15 if (items.length === 0 || !includeDisabledItems && items.every((item, itemIndex) => isItemDisabled(item, itemIndex))) {
16 return -1;
17 }
18 let nextFocus = currentIndex;
19 for (;;) {
20 // No valid items found
21 if (!wrapAround && lookupDirection === 'next' && nextFocus === items.length || !wrapAround && lookupDirection === 'previous' && nextFocus === -1) {
22 return -1;
23 }
24 const nextFocusDisabled = includeDisabledItems ? false : isItemDisabled(items[nextFocus], nextFocus);
25 if (nextFocusDisabled) {
26 nextFocus += lookupDirection === 'next' ? 1 : -1;
27 if (wrapAround) {
28 nextFocus = (nextFocus + items.length) % items.length;
29 }
30 } else {
31 return nextFocus;
32 }
33 }
34}
35
36/**
37 * Gets the next item to highlight based on the current highlighted item and the search direction.
38 *
39 * @param previouslyHighlightedValue The item from which to start the search for the next candidate.
40 * @param offset The offset from the previously highlighted item to search for the next candidate or a special named value ('reset', 'start', 'end').
41 * @param context The list action context.
42 *
43 * @returns The next item to highlight or null if no item is valid.
44 */
45export function moveHighlight(previouslyHighlightedValue, offset, context) {
46 var _items$nextIndex;
47 const {
48 items,
49 isItemDisabled,
50 disableListWrap,
51 disabledItemsFocusable,
52 itemComparer,
53 focusManagement
54 } = context;
55
56 // TODO: make this configurable
57 // The always should be an item highlighted when focus is managed by the DOM
58 // so that it's accessible by the `tab` key.
59 const defaultHighlightedIndex = focusManagement === 'DOM' ? 0 : -1;
60 const maxIndex = items.length - 1;
61 const previouslyHighlightedIndex = previouslyHighlightedValue == null ? -1 : items.findIndex(item => itemComparer(item, previouslyHighlightedValue));
62 let nextIndexCandidate;
63 let lookupDirection;
64 let wrapAround = !disableListWrap;
65 switch (offset) {
66 case 'reset':
67 if (defaultHighlightedIndex === -1) {
68 return null;
69 }
70 nextIndexCandidate = 0;
71 lookupDirection = 'next';
72 wrapAround = false;
73 break;
74 case 'start':
75 nextIndexCandidate = 0;
76 lookupDirection = 'next';
77 wrapAround = false;
78 break;
79 case 'end':
80 nextIndexCandidate = maxIndex;
81 lookupDirection = 'previous';
82 wrapAround = false;
83 break;
84 default:
85 {
86 const newIndex = previouslyHighlightedIndex + offset;
87 if (newIndex < 0) {
88 if (!wrapAround && previouslyHighlightedIndex !== -1 || Math.abs(offset) > 1) {
89 nextIndexCandidate = 0;
90 lookupDirection = 'next';
91 } else {
92 nextIndexCandidate = maxIndex;
93 lookupDirection = 'previous';
94 }
95 } else if (newIndex > maxIndex) {
96 if (!wrapAround || Math.abs(offset) > 1) {
97 nextIndexCandidate = maxIndex;
98 lookupDirection = 'previous';
99 } else {
100 nextIndexCandidate = 0;
101 lookupDirection = 'next';
102 }
103 } else {
104 nextIndexCandidate = newIndex;
105 lookupDirection = offset >= 0 ? 'next' : 'previous';
106 }
107 }
108 }
109 const nextIndex = findValidItemToHighlight(nextIndexCandidate, lookupDirection, items, disabledItemsFocusable, isItemDisabled, wrapAround);
110
111 // If there are no valid items to highlight, return the previously highlighted item (if it's still valid).
112 if (nextIndex === -1 && previouslyHighlightedValue !== null && !isItemDisabled(previouslyHighlightedValue, previouslyHighlightedIndex)) {
113 return previouslyHighlightedValue;
114 }
115 return (_items$nextIndex = items[nextIndex]) != null ? _items$nextIndex : null;
116}
117
118/**
119 * Toggles the selection of an item.
120 *
121 * @param item Item to toggle.
122 * @param selectedValues Already selected items.
123 * @param selectionMode The number of items that can be simultanously selected.
124 * @param itemComparer A custom item comparer function.
125 *
126 * @returns The new array of selected items.
127 */
128export function toggleSelection(item, selectedValues, selectionMode, itemComparer) {
129 if (selectionMode === 'none') {
130 return [];
131 }
132 if (selectionMode === 'single') {
133 // if the item to select has already been selected, return the original array
134 if (itemComparer(selectedValues[0], item)) {
135 return selectedValues;
136 }
137 return [item];
138 }
139
140 // The toggled item is selected; remove it from the selection.
141 if (selectedValues.some(sv => itemComparer(sv, item))) {
142 return selectedValues.filter(sv => !itemComparer(sv, item));
143 }
144
145 // The toggled item is not selected - add it to the selection.
146 return [...selectedValues, item];
147}
148
149/**
150 * Handles item selection in a list.
151 *
152 * @param item - The item to be selected.
153 * @param state - The current state of the list.
154 * @param context - The context of the list action.
155 * @returns The new state of the list after the item has been selected, or the original state if the item is disabled.
156 */
157export function handleItemSelection(item, state, context) {
158 const {
159 itemComparer,
160 isItemDisabled,
161 selectionMode,
162 items
163 } = context;
164 const {
165 selectedValues
166 } = state;
167 const itemIndex = items.findIndex(i => itemComparer(item, i));
168 if (isItemDisabled(item, itemIndex)) {
169 return state;
170 }
171
172 // if the item is already selected, remove it from the selection, otherwise add it
173 const newSelectedValues = toggleSelection(item, selectedValues, selectionMode, itemComparer);
174 return _extends({}, state, {
175 selectedValues: newSelectedValues,
176 highlightedValue: item
177 });
178}
179function handleKeyDown(key, state, context) {
180 const previouslySelectedValue = state.highlightedValue;
181 const {
182 orientation,
183 pageSize
184 } = context;
185 switch (key) {
186 case 'Home':
187 return _extends({}, state, {
188 highlightedValue: moveHighlight(previouslySelectedValue, 'start', context)
189 });
190 case 'End':
191 return _extends({}, state, {
192 highlightedValue: moveHighlight(previouslySelectedValue, 'end', context)
193 });
194 case 'PageUp':
195 return _extends({}, state, {
196 highlightedValue: moveHighlight(previouslySelectedValue, -pageSize, context)
197 });
198 case 'PageDown':
199 return _extends({}, state, {
200 highlightedValue: moveHighlight(previouslySelectedValue, pageSize, context)
201 });
202 case 'ArrowUp':
203 if (orientation !== 'vertical') {
204 break;
205 }
206 return _extends({}, state, {
207 highlightedValue: moveHighlight(previouslySelectedValue, -1, context)
208 });
209 case 'ArrowDown':
210 if (orientation !== 'vertical') {
211 break;
212 }
213 return _extends({}, state, {
214 highlightedValue: moveHighlight(previouslySelectedValue, 1, context)
215 });
216 case 'ArrowLeft':
217 {
218 if (orientation === 'vertical') {
219 break;
220 }
221 const offset = orientation === 'horizontal-ltr' ? -1 : 1;
222 return _extends({}, state, {
223 highlightedValue: moveHighlight(previouslySelectedValue, offset, context)
224 });
225 }
226 case 'ArrowRight':
227 {
228 if (orientation === 'vertical') {
229 break;
230 }
231 const offset = orientation === 'horizontal-ltr' ? 1 : -1;
232 return _extends({}, state, {
233 highlightedValue: moveHighlight(previouslySelectedValue, offset, context)
234 });
235 }
236 case 'Enter':
237 case ' ':
238 if (state.highlightedValue === null) {
239 return state;
240 }
241 return handleItemSelection(state.highlightedValue, state, context);
242 default:
243 break;
244 }
245 return state;
246}
247function handleBlur(state, context) {
248 if (context.focusManagement === 'DOM') {
249 return state;
250 }
251 return _extends({}, state, {
252 highlightedValue: null
253 });
254}
255function textCriteriaMatches(nextFocus, searchString, stringifyItem) {
256 var _stringifyItem;
257 const text = (_stringifyItem = stringifyItem(nextFocus)) == null ? void 0 : _stringifyItem.trim().toLowerCase();
258 if (!text || text.length === 0) {
259 // Make item not navigable if stringification fails or results in empty string.
260 return false;
261 }
262 return text.indexOf(searchString) === 0;
263}
264function handleTextNavigation(state, searchString, context) {
265 const {
266 items,
267 isItemDisabled,
268 disabledItemsFocusable,
269 getItemAsString
270 } = context;
271 const startWithCurrentItem = searchString.length > 1;
272 let nextItem = startWithCurrentItem ? state.highlightedValue : moveHighlight(state.highlightedValue, 1, context);
273 for (let index = 0; index < items.length; index += 1) {
274 // Return un-mutated state if looped back to the currently highlighted value
275 if (!nextItem || !startWithCurrentItem && state.highlightedValue === nextItem) {
276 return state;
277 }
278 if (textCriteriaMatches(nextItem, searchString, getItemAsString) && (!isItemDisabled(nextItem, items.indexOf(nextItem)) || disabledItemsFocusable)) {
279 // The nextItem is the element to be highlighted
280 return _extends({}, state, {
281 highlightedValue: nextItem
282 });
283 }
284 // Move to the next element.
285 nextItem = moveHighlight(nextItem, 1, context);
286 }
287
288 // No item matches the text search criteria
289 return state;
290}
291function handleItemsChange(items, previousItems, state, context) {
292 var _state$selectedValues;
293 const {
294 itemComparer,
295 focusManagement
296 } = context;
297 let newHighlightedValue = null;
298 if (state.highlightedValue != null) {
299 var _items$find;
300 newHighlightedValue = (_items$find = items.find(item => itemComparer(item, state.highlightedValue))) != null ? _items$find : null;
301 } else if (focusManagement === 'DOM' && previousItems.length === 0) {
302 newHighlightedValue = moveHighlight(null, 'reset', context);
303 }
304
305 // exclude selected values that are no longer in the items list
306 const selectedValues = (_state$selectedValues = state.selectedValues) != null ? _state$selectedValues : [];
307 const newSelectedValues = selectedValues.filter(selectedValue => items.some(item => itemComparer(item, selectedValue)));
308 return _extends({}, state, {
309 highlightedValue: newHighlightedValue,
310 selectedValues: newSelectedValues
311 });
312}
313function handleResetHighlight(state, context) {
314 return _extends({}, state, {
315 highlightedValue: moveHighlight(null, 'reset', context)
316 });
317}
318function handleHighlightLast(state, context) {
319 return _extends({}, state, {
320 highlightedValue: moveHighlight(null, 'end', context)
321 });
322}
323function handleClearSelection(state, context) {
324 return _extends({}, state, {
325 selectedValues: [],
326 highlightedValue: moveHighlight(null, 'reset', context)
327 });
328}
329export function listReducer(state, action) {
330 const {
331 type,
332 context
333 } = action;
334 switch (type) {
335 case ListActionTypes.keyDown:
336 return handleKeyDown(action.key, state, context);
337 case ListActionTypes.itemClick:
338 return handleItemSelection(action.item, state, context);
339 case ListActionTypes.blur:
340 return handleBlur(state, context);
341 case ListActionTypes.textNavigation:
342 return handleTextNavigation(state, action.searchString, context);
343 case ListActionTypes.itemsChange:
344 return handleItemsChange(action.items, action.previousItems, state, context);
345 case ListActionTypes.resetHighlight:
346 return handleResetHighlight(state, context);
347 case ListActionTypes.highlightLast:
348 return handleHighlightLast(state, context);
349 case ListActionTypes.clearSelection:
350 return handleClearSelection(state, context);
351 default:
352 return state;
353 }
354}
\No newline at end of file