UNPKG

11.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}
148function handleItemSelection(item, state, context) {
149 const {
150 itemComparer,
151 isItemDisabled,
152 selectionMode,
153 items
154 } = context;
155 const {
156 selectedValues
157 } = state;
158 const itemIndex = items.findIndex(i => itemComparer(item, i));
159 if (isItemDisabled(item, itemIndex)) {
160 return state;
161 }
162
163 // if the item is already selected, remove it from the selection, otherwise add it
164 const newSelectedValues = toggleSelection(item, selectedValues, selectionMode, itemComparer);
165 return _extends({}, state, {
166 selectedValues: newSelectedValues,
167 highlightedValue: item
168 });
169}
170function handleKeyDown(key, state, context) {
171 const previouslySelectedValue = state.highlightedValue;
172 const {
173 orientation,
174 pageSize
175 } = context;
176 switch (key) {
177 case 'Home':
178 return _extends({}, state, {
179 highlightedValue: moveHighlight(previouslySelectedValue, 'start', context)
180 });
181 case 'End':
182 return _extends({}, state, {
183 highlightedValue: moveHighlight(previouslySelectedValue, 'end', context)
184 });
185 case 'PageUp':
186 return _extends({}, state, {
187 highlightedValue: moveHighlight(previouslySelectedValue, -pageSize, context)
188 });
189 case 'PageDown':
190 return _extends({}, state, {
191 highlightedValue: moveHighlight(previouslySelectedValue, pageSize, context)
192 });
193 case 'ArrowUp':
194 if (orientation !== 'vertical') {
195 break;
196 }
197 return _extends({}, state, {
198 highlightedValue: moveHighlight(previouslySelectedValue, -1, context)
199 });
200 case 'ArrowDown':
201 if (orientation !== 'vertical') {
202 break;
203 }
204 return _extends({}, state, {
205 highlightedValue: moveHighlight(previouslySelectedValue, 1, context)
206 });
207 case 'ArrowLeft':
208 {
209 if (orientation === 'vertical') {
210 break;
211 }
212 const offset = orientation === 'horizontal-ltr' ? -1 : 1;
213 return _extends({}, state, {
214 highlightedValue: moveHighlight(previouslySelectedValue, offset, context)
215 });
216 }
217 case 'ArrowRight':
218 {
219 if (orientation === 'vertical') {
220 break;
221 }
222 const offset = orientation === 'horizontal-ltr' ? 1 : -1;
223 return _extends({}, state, {
224 highlightedValue: moveHighlight(previouslySelectedValue, offset, context)
225 });
226 }
227 case 'Enter':
228 case ' ':
229 if (state.highlightedValue === null) {
230 return state;
231 }
232 return handleItemSelection(state.highlightedValue, state, context);
233 default:
234 break;
235 }
236 return state;
237}
238function handleBlur(state, context) {
239 if (context.focusManagement === 'DOM') {
240 return state;
241 }
242 return _extends({}, state, {
243 highlightedValue: null
244 });
245}
246function textCriteriaMatches(nextFocus, searchString, stringifyItem) {
247 var _stringifyItem;
248 const text = (_stringifyItem = stringifyItem(nextFocus)) == null ? void 0 : _stringifyItem.trim().toLowerCase();
249 if (!text || text.length === 0) {
250 // Make item not navigable if stringification fails or results in empty string.
251 return false;
252 }
253 return text.indexOf(searchString) === 0;
254}
255function handleTextNavigation(state, searchString, context) {
256 const {
257 items,
258 isItemDisabled,
259 disabledItemsFocusable,
260 getItemAsString
261 } = context;
262 const startWithCurrentItem = searchString.length > 1;
263 let nextItem = startWithCurrentItem ? state.highlightedValue : moveHighlight(state.highlightedValue, 1, context);
264 for (let index = 0; index < items.length; index += 1) {
265 // Return un-mutated state if looped back to the currently highlighted value
266 if (!nextItem || !startWithCurrentItem && state.highlightedValue === nextItem) {
267 return state;
268 }
269 if (textCriteriaMatches(nextItem, searchString, getItemAsString) && (!isItemDisabled(nextItem, items.indexOf(nextItem)) || disabledItemsFocusable)) {
270 // The nextItem is the element to be highlighted
271 return _extends({}, state, {
272 highlightedValue: nextItem
273 });
274 }
275 // Move to the next element.
276 nextItem = moveHighlight(nextItem, 1, context);
277 }
278
279 // No item matches the text search criteria
280 return state;
281}
282function handleItemsChange(items, previousItems, state, context) {
283 var _state$selectedValues;
284 const {
285 itemComparer,
286 focusManagement
287 } = context;
288 let newHighlightedValue = null;
289 if (state.highlightedValue != null) {
290 var _items$find;
291 newHighlightedValue = (_items$find = items.find(item => itemComparer(item, state.highlightedValue))) != null ? _items$find : null;
292 } else if (focusManagement === 'DOM' && previousItems.length === 0) {
293 newHighlightedValue = moveHighlight(null, 'reset', context);
294 }
295
296 // exclude selected values that are no longer in the items list
297 const selectedValues = (_state$selectedValues = state.selectedValues) != null ? _state$selectedValues : [];
298 const newSelectedValues = selectedValues.filter(selectedValue => items.some(item => itemComparer(item, selectedValue)));
299 return _extends({}, state, {
300 highlightedValue: newHighlightedValue,
301 selectedValues: newSelectedValues
302 });
303}
304export default function listReducer(state, action) {
305 const {
306 type,
307 context
308 } = action;
309 switch (type) {
310 case ListActionTypes.keyDown:
311 return handleKeyDown(action.key, state, context);
312 case ListActionTypes.itemClick:
313 return handleItemSelection(action.item, state, context);
314 case ListActionTypes.blur:
315 return handleBlur(state, context);
316 case ListActionTypes.textNavigation:
317 return handleTextNavigation(state, action.searchString, context);
318 case ListActionTypes.itemsChange:
319 return handleItemsChange(action.items, action.previousItems, state, context);
320 default:
321 return state;
322 }
323}
\No newline at end of file