UNPKG

11.9 kBJavaScriptView Raw
1"use strict";
2
3var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4Object.defineProperty(exports, "__esModule", {
5 value: true
6});
7exports.default = listReducer;
8exports.moveHighlight = moveHighlight;
9exports.toggleSelection = toggleSelection;
10var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
11var _listActions = require("./listActions.types");
12/**
13 * Looks up the next valid item to highlight within the list.
14 *
15 * @param currentIndex The index of the start of the search.
16 * @param lookupDirection Whether to look for the next or previous item.
17 * @param items The array of items to search.
18 * @param includeDisabledItems Whether to include disabled items in the search.
19 * @param isItemDisabled A function that determines whether an item is disabled.
20 * @param wrapAround Whether to wrap around the list when searching.
21 * @returns The index of the next valid item to highlight or -1 if no valid item is found.
22 */
23function findValidItemToHighlight(currentIndex, lookupDirection, items, includeDisabledItems, isItemDisabled, wrapAround) {
24 if (items.length === 0 || !includeDisabledItems && items.every((item, itemIndex) => isItemDisabled(item, itemIndex))) {
25 return -1;
26 }
27 let nextFocus = currentIndex;
28 for (;;) {
29 // No valid items found
30 if (!wrapAround && lookupDirection === 'next' && nextFocus === items.length || !wrapAround && lookupDirection === 'previous' && nextFocus === -1) {
31 return -1;
32 }
33 const nextFocusDisabled = includeDisabledItems ? false : isItemDisabled(items[nextFocus], nextFocus);
34 if (nextFocusDisabled) {
35 nextFocus += lookupDirection === 'next' ? 1 : -1;
36 if (wrapAround) {
37 nextFocus = (nextFocus + items.length) % items.length;
38 }
39 } else {
40 return nextFocus;
41 }
42 }
43}
44
45/**
46 * Gets the next item to highlight based on the current highlighted item and the search direction.
47 *
48 * @param previouslyHighlightedValue The item from which to start the search for the next candidate.
49 * @param offset The offset from the previously highlighted item to search for the next candidate or a special named value ('reset', 'start', 'end').
50 * @param context The list action context.
51 *
52 * @returns The next item to highlight or null if no item is valid.
53 */
54function moveHighlight(previouslyHighlightedValue, offset, context) {
55 var _items$nextIndex;
56 const {
57 items,
58 isItemDisabled,
59 disableListWrap,
60 disabledItemsFocusable,
61 itemComparer,
62 focusManagement
63 } = context;
64
65 // TODO: make this configurable
66 // The always should be an item highlighted when focus is managed by the DOM
67 // so that it's accessible by the `tab` key.
68 const defaultHighlightedIndex = focusManagement === 'DOM' ? 0 : -1;
69 const maxIndex = items.length - 1;
70 const previouslyHighlightedIndex = previouslyHighlightedValue == null ? -1 : items.findIndex(item => itemComparer(item, previouslyHighlightedValue));
71 let nextIndexCandidate;
72 let lookupDirection;
73 let wrapAround = !disableListWrap;
74 switch (offset) {
75 case 'reset':
76 if (defaultHighlightedIndex === -1) {
77 return null;
78 }
79 nextIndexCandidate = 0;
80 lookupDirection = 'next';
81 wrapAround = false;
82 break;
83 case 'start':
84 nextIndexCandidate = 0;
85 lookupDirection = 'next';
86 wrapAround = false;
87 break;
88 case 'end':
89 nextIndexCandidate = maxIndex;
90 lookupDirection = 'previous';
91 wrapAround = false;
92 break;
93 default:
94 {
95 const newIndex = previouslyHighlightedIndex + offset;
96 if (newIndex < 0) {
97 if (!wrapAround && previouslyHighlightedIndex !== -1 || Math.abs(offset) > 1) {
98 nextIndexCandidate = 0;
99 lookupDirection = 'next';
100 } else {
101 nextIndexCandidate = maxIndex;
102 lookupDirection = 'previous';
103 }
104 } else if (newIndex > maxIndex) {
105 if (!wrapAround || Math.abs(offset) > 1) {
106 nextIndexCandidate = maxIndex;
107 lookupDirection = 'previous';
108 } else {
109 nextIndexCandidate = 0;
110 lookupDirection = 'next';
111 }
112 } else {
113 nextIndexCandidate = newIndex;
114 lookupDirection = offset >= 0 ? 'next' : 'previous';
115 }
116 }
117 }
118 const nextIndex = findValidItemToHighlight(nextIndexCandidate, lookupDirection, items, disabledItemsFocusable, isItemDisabled, wrapAround);
119
120 // If there are no valid items to highlight, return the previously highlighted item (if it's still valid).
121 if (nextIndex === -1 && previouslyHighlightedValue !== null && !isItemDisabled(previouslyHighlightedValue, previouslyHighlightedIndex)) {
122 return previouslyHighlightedValue;
123 }
124 return (_items$nextIndex = items[nextIndex]) != null ? _items$nextIndex : null;
125}
126
127/**
128 * Toggles the selection of an item.
129 *
130 * @param item Item to toggle.
131 * @param selectedValues Already selected items.
132 * @param selectionMode The number of items that can be simultanously selected.
133 * @param itemComparer A custom item comparer function.
134 *
135 * @returns The new array of selected items.
136 */
137function toggleSelection(item, selectedValues, selectionMode, itemComparer) {
138 if (selectionMode === 'none') {
139 return [];
140 }
141 if (selectionMode === 'single') {
142 // if the item to select has already been selected, return the original array
143 if (itemComparer(selectedValues[0], item)) {
144 return selectedValues;
145 }
146 return [item];
147 }
148
149 // The toggled item is selected; remove it from the selection.
150 if (selectedValues.some(sv => itemComparer(sv, item))) {
151 return selectedValues.filter(sv => !itemComparer(sv, item));
152 }
153
154 // The toggled item is not selected - add it to the selection.
155 return [...selectedValues, item];
156}
157function 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 (0, _extends2.default)({}, 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 (0, _extends2.default)({}, state, {
188 highlightedValue: moveHighlight(previouslySelectedValue, 'start', context)
189 });
190 case 'End':
191 return (0, _extends2.default)({}, state, {
192 highlightedValue: moveHighlight(previouslySelectedValue, 'end', context)
193 });
194 case 'PageUp':
195 return (0, _extends2.default)({}, state, {
196 highlightedValue: moveHighlight(previouslySelectedValue, -pageSize, context)
197 });
198 case 'PageDown':
199 return (0, _extends2.default)({}, state, {
200 highlightedValue: moveHighlight(previouslySelectedValue, pageSize, context)
201 });
202 case 'ArrowUp':
203 if (orientation !== 'vertical') {
204 break;
205 }
206 return (0, _extends2.default)({}, state, {
207 highlightedValue: moveHighlight(previouslySelectedValue, -1, context)
208 });
209 case 'ArrowDown':
210 if (orientation !== 'vertical') {
211 break;
212 }
213 return (0, _extends2.default)({}, 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 (0, _extends2.default)({}, 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 (0, _extends2.default)({}, 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 (0, _extends2.default)({}, 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 (0, _extends2.default)({}, 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 (0, _extends2.default)({}, state, {
309 highlightedValue: newHighlightedValue,
310 selectedValues: newSelectedValues
311 });
312}
313function listReducer(state, action) {
314 const {
315 type,
316 context
317 } = action;
318 switch (type) {
319 case _listActions.ListActionTypes.keyDown:
320 return handleKeyDown(action.key, state, context);
321 case _listActions.ListActionTypes.itemClick:
322 return handleItemSelection(action.item, state, context);
323 case _listActions.ListActionTypes.blur:
324 return handleBlur(state, context);
325 case _listActions.ListActionTypes.textNavigation:
326 return handleTextNavigation(state, action.searchString, context);
327 case _listActions.ListActionTypes.itemsChange:
328 return handleItemsChange(action.items, action.previousItems, state, context);
329 default:
330 return state;
331 }
332}
\No newline at end of file