364 lines
13 KiB
JavaScript
364 lines
13 KiB
JavaScript
"use strict";
|
|
|
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
exports.handleItemSelection = handleItemSelection;
|
|
exports.listReducer = listReducer;
|
|
exports.moveHighlight = moveHighlight;
|
|
exports.toggleSelection = toggleSelection;
|
|
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
|
|
var _listActions = require("./listActions.types");
|
|
/**
|
|
* Looks up the next valid item to highlight within the list.
|
|
*
|
|
* @param currentIndex The index of the start of the search.
|
|
* @param lookupDirection Whether to look for the next or previous item.
|
|
* @param items The array of items to search.
|
|
* @param includeDisabledItems Whether to include disabled items in the search.
|
|
* @param isItemDisabled A function that determines whether an item is disabled.
|
|
* @param wrapAround Whether to wrap around the list when searching.
|
|
* @returns The index of the next valid item to highlight or -1 if no valid item is found.
|
|
*/
|
|
function findValidItemToHighlight(currentIndex, lookupDirection, items, includeDisabledItems, isItemDisabled, wrapAround) {
|
|
if (items.length === 0 || !includeDisabledItems && items.every((item, itemIndex) => isItemDisabled(item, itemIndex))) {
|
|
return -1;
|
|
}
|
|
let nextFocus = currentIndex;
|
|
for (;;) {
|
|
// No valid items found
|
|
if (!wrapAround && lookupDirection === 'next' && nextFocus === items.length || !wrapAround && lookupDirection === 'previous' && nextFocus === -1) {
|
|
return -1;
|
|
}
|
|
const nextFocusDisabled = includeDisabledItems ? false : isItemDisabled(items[nextFocus], nextFocus);
|
|
if (nextFocusDisabled) {
|
|
nextFocus += lookupDirection === 'next' ? 1 : -1;
|
|
if (wrapAround) {
|
|
nextFocus = (nextFocus + items.length) % items.length;
|
|
}
|
|
} else {
|
|
return nextFocus;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the next item to highlight based on the current highlighted item and the search direction.
|
|
*
|
|
* @param previouslyHighlightedValue The item from which to start the search for the next candidate.
|
|
* @param offset The offset from the previously highlighted item to search for the next candidate or a special named value ('reset', 'start', 'end').
|
|
* @param context The list action context.
|
|
*
|
|
* @returns The next item to highlight or null if no item is valid.
|
|
*/
|
|
function moveHighlight(previouslyHighlightedValue, offset, context) {
|
|
var _items$nextIndex;
|
|
const {
|
|
items,
|
|
isItemDisabled,
|
|
disableListWrap,
|
|
disabledItemsFocusable,
|
|
itemComparer,
|
|
focusManagement
|
|
} = context;
|
|
|
|
// TODO: make this configurable
|
|
// The always should be an item highlighted when focus is managed by the DOM
|
|
// so that it's accessible by the `tab` key.
|
|
const defaultHighlightedIndex = focusManagement === 'DOM' ? 0 : -1;
|
|
const maxIndex = items.length - 1;
|
|
const previouslyHighlightedIndex = previouslyHighlightedValue == null ? -1 : items.findIndex(item => itemComparer(item, previouslyHighlightedValue));
|
|
let nextIndexCandidate;
|
|
let lookupDirection;
|
|
let wrapAround = !disableListWrap;
|
|
switch (offset) {
|
|
case 'reset':
|
|
if (defaultHighlightedIndex === -1) {
|
|
return null;
|
|
}
|
|
nextIndexCandidate = 0;
|
|
lookupDirection = 'next';
|
|
wrapAround = false;
|
|
break;
|
|
case 'start':
|
|
nextIndexCandidate = 0;
|
|
lookupDirection = 'next';
|
|
wrapAround = false;
|
|
break;
|
|
case 'end':
|
|
nextIndexCandidate = maxIndex;
|
|
lookupDirection = 'previous';
|
|
wrapAround = false;
|
|
break;
|
|
default:
|
|
{
|
|
const newIndex = previouslyHighlightedIndex + offset;
|
|
if (newIndex < 0) {
|
|
if (!wrapAround && previouslyHighlightedIndex !== -1 || Math.abs(offset) > 1) {
|
|
nextIndexCandidate = 0;
|
|
lookupDirection = 'next';
|
|
} else {
|
|
nextIndexCandidate = maxIndex;
|
|
lookupDirection = 'previous';
|
|
}
|
|
} else if (newIndex > maxIndex) {
|
|
if (!wrapAround || Math.abs(offset) > 1) {
|
|
nextIndexCandidate = maxIndex;
|
|
lookupDirection = 'previous';
|
|
} else {
|
|
nextIndexCandidate = 0;
|
|
lookupDirection = 'next';
|
|
}
|
|
} else {
|
|
nextIndexCandidate = newIndex;
|
|
lookupDirection = offset >= 0 ? 'next' : 'previous';
|
|
}
|
|
}
|
|
}
|
|
const nextIndex = findValidItemToHighlight(nextIndexCandidate, lookupDirection, items, disabledItemsFocusable, isItemDisabled, wrapAround);
|
|
|
|
// If there are no valid items to highlight, return the previously highlighted item (if it's still valid).
|
|
if (nextIndex === -1 && previouslyHighlightedValue !== null && !isItemDisabled(previouslyHighlightedValue, previouslyHighlightedIndex)) {
|
|
return previouslyHighlightedValue;
|
|
}
|
|
return (_items$nextIndex = items[nextIndex]) != null ? _items$nextIndex : null;
|
|
}
|
|
|
|
/**
|
|
* Toggles the selection of an item.
|
|
*
|
|
* @param item Item to toggle.
|
|
* @param selectedValues Already selected items.
|
|
* @param selectionMode The number of items that can be simultanously selected.
|
|
* @param itemComparer A custom item comparer function.
|
|
*
|
|
* @returns The new array of selected items.
|
|
*/
|
|
function toggleSelection(item, selectedValues, selectionMode, itemComparer) {
|
|
if (selectionMode === 'none') {
|
|
return [];
|
|
}
|
|
if (selectionMode === 'single') {
|
|
// if the item to select has already been selected, return the original array
|
|
if (itemComparer(selectedValues[0], item)) {
|
|
return selectedValues;
|
|
}
|
|
return [item];
|
|
}
|
|
|
|
// The toggled item is selected; remove it from the selection.
|
|
if (selectedValues.some(sv => itemComparer(sv, item))) {
|
|
return selectedValues.filter(sv => !itemComparer(sv, item));
|
|
}
|
|
|
|
// The toggled item is not selected - add it to the selection.
|
|
return [...selectedValues, item];
|
|
}
|
|
|
|
/**
|
|
* Handles item selection in a list.
|
|
*
|
|
* @param item - The item to be selected.
|
|
* @param state - The current state of the list.
|
|
* @param context - The context of the list action.
|
|
* @returns The new state of the list after the item has been selected, or the original state if the item is disabled.
|
|
*/
|
|
function handleItemSelection(item, state, context) {
|
|
const {
|
|
itemComparer,
|
|
isItemDisabled,
|
|
selectionMode,
|
|
items
|
|
} = context;
|
|
const {
|
|
selectedValues
|
|
} = state;
|
|
const itemIndex = items.findIndex(i => itemComparer(item, i));
|
|
if (isItemDisabled(item, itemIndex)) {
|
|
return state;
|
|
}
|
|
|
|
// if the item is already selected, remove it from the selection, otherwise add it
|
|
const newSelectedValues = toggleSelection(item, selectedValues, selectionMode, itemComparer);
|
|
return (0, _extends2.default)({}, state, {
|
|
selectedValues: newSelectedValues,
|
|
highlightedValue: item
|
|
});
|
|
}
|
|
function handleKeyDown(key, state, context) {
|
|
const previouslySelectedValue = state.highlightedValue;
|
|
const {
|
|
orientation,
|
|
pageSize
|
|
} = context;
|
|
switch (key) {
|
|
case 'Home':
|
|
return (0, _extends2.default)({}, state, {
|
|
highlightedValue: moveHighlight(previouslySelectedValue, 'start', context)
|
|
});
|
|
case 'End':
|
|
return (0, _extends2.default)({}, state, {
|
|
highlightedValue: moveHighlight(previouslySelectedValue, 'end', context)
|
|
});
|
|
case 'PageUp':
|
|
return (0, _extends2.default)({}, state, {
|
|
highlightedValue: moveHighlight(previouslySelectedValue, -pageSize, context)
|
|
});
|
|
case 'PageDown':
|
|
return (0, _extends2.default)({}, state, {
|
|
highlightedValue: moveHighlight(previouslySelectedValue, pageSize, context)
|
|
});
|
|
case 'ArrowUp':
|
|
if (orientation !== 'vertical') {
|
|
break;
|
|
}
|
|
return (0, _extends2.default)({}, state, {
|
|
highlightedValue: moveHighlight(previouslySelectedValue, -1, context)
|
|
});
|
|
case 'ArrowDown':
|
|
if (orientation !== 'vertical') {
|
|
break;
|
|
}
|
|
return (0, _extends2.default)({}, state, {
|
|
highlightedValue: moveHighlight(previouslySelectedValue, 1, context)
|
|
});
|
|
case 'ArrowLeft':
|
|
{
|
|
if (orientation === 'vertical') {
|
|
break;
|
|
}
|
|
const offset = orientation === 'horizontal-ltr' ? -1 : 1;
|
|
return (0, _extends2.default)({}, state, {
|
|
highlightedValue: moveHighlight(previouslySelectedValue, offset, context)
|
|
});
|
|
}
|
|
case 'ArrowRight':
|
|
{
|
|
if (orientation === 'vertical') {
|
|
break;
|
|
}
|
|
const offset = orientation === 'horizontal-ltr' ? 1 : -1;
|
|
return (0, _extends2.default)({}, state, {
|
|
highlightedValue: moveHighlight(previouslySelectedValue, offset, context)
|
|
});
|
|
}
|
|
case 'Enter':
|
|
case ' ':
|
|
if (state.highlightedValue === null) {
|
|
return state;
|
|
}
|
|
return handleItemSelection(state.highlightedValue, state, context);
|
|
default:
|
|
break;
|
|
}
|
|
return state;
|
|
}
|
|
function handleBlur(state, context) {
|
|
if (context.focusManagement === 'DOM') {
|
|
return state;
|
|
}
|
|
return (0, _extends2.default)({}, state, {
|
|
highlightedValue: null
|
|
});
|
|
}
|
|
function textCriteriaMatches(nextFocus, searchString, stringifyItem) {
|
|
var _stringifyItem;
|
|
const text = (_stringifyItem = stringifyItem(nextFocus)) == null ? void 0 : _stringifyItem.trim().toLowerCase();
|
|
if (!text || text.length === 0) {
|
|
// Make item not navigable if stringification fails or results in empty string.
|
|
return false;
|
|
}
|
|
return text.indexOf(searchString) === 0;
|
|
}
|
|
function handleTextNavigation(state, searchString, context) {
|
|
const {
|
|
items,
|
|
isItemDisabled,
|
|
disabledItemsFocusable,
|
|
getItemAsString
|
|
} = context;
|
|
const startWithCurrentItem = searchString.length > 1;
|
|
let nextItem = startWithCurrentItem ? state.highlightedValue : moveHighlight(state.highlightedValue, 1, context);
|
|
for (let index = 0; index < items.length; index += 1) {
|
|
// Return un-mutated state if looped back to the currently highlighted value
|
|
if (!nextItem || !startWithCurrentItem && state.highlightedValue === nextItem) {
|
|
return state;
|
|
}
|
|
if (textCriteriaMatches(nextItem, searchString, getItemAsString) && (!isItemDisabled(nextItem, items.indexOf(nextItem)) || disabledItemsFocusable)) {
|
|
// The nextItem is the element to be highlighted
|
|
return (0, _extends2.default)({}, state, {
|
|
highlightedValue: nextItem
|
|
});
|
|
}
|
|
// Move to the next element.
|
|
nextItem = moveHighlight(nextItem, 1, context);
|
|
}
|
|
|
|
// No item matches the text search criteria
|
|
return state;
|
|
}
|
|
function handleItemsChange(items, previousItems, state, context) {
|
|
var _state$selectedValues;
|
|
const {
|
|
itemComparer,
|
|
focusManagement
|
|
} = context;
|
|
let newHighlightedValue = null;
|
|
if (state.highlightedValue != null) {
|
|
var _items$find;
|
|
newHighlightedValue = (_items$find = items.find(item => itemComparer(item, state.highlightedValue))) != null ? _items$find : null;
|
|
} else if (focusManagement === 'DOM' && previousItems.length === 0) {
|
|
newHighlightedValue = moveHighlight(null, 'reset', context);
|
|
}
|
|
|
|
// exclude selected values that are no longer in the items list
|
|
const selectedValues = (_state$selectedValues = state.selectedValues) != null ? _state$selectedValues : [];
|
|
const newSelectedValues = selectedValues.filter(selectedValue => items.some(item => itemComparer(item, selectedValue)));
|
|
return (0, _extends2.default)({}, state, {
|
|
highlightedValue: newHighlightedValue,
|
|
selectedValues: newSelectedValues
|
|
});
|
|
}
|
|
function handleResetHighlight(state, context) {
|
|
return (0, _extends2.default)({}, state, {
|
|
highlightedValue: moveHighlight(null, 'reset', context)
|
|
});
|
|
}
|
|
function handleHighlightLast(state, context) {
|
|
return (0, _extends2.default)({}, state, {
|
|
highlightedValue: moveHighlight(null, 'end', context)
|
|
});
|
|
}
|
|
function handleClearSelection(state, context) {
|
|
return (0, _extends2.default)({}, state, {
|
|
selectedValues: [],
|
|
highlightedValue: moveHighlight(null, 'reset', context)
|
|
});
|
|
}
|
|
function listReducer(state, action) {
|
|
const {
|
|
type,
|
|
context
|
|
} = action;
|
|
switch (type) {
|
|
case _listActions.ListActionTypes.keyDown:
|
|
return handleKeyDown(action.key, state, context);
|
|
case _listActions.ListActionTypes.itemClick:
|
|
return handleItemSelection(action.item, state, context);
|
|
case _listActions.ListActionTypes.blur:
|
|
return handleBlur(state, context);
|
|
case _listActions.ListActionTypes.textNavigation:
|
|
return handleTextNavigation(state, action.searchString, context);
|
|
case _listActions.ListActionTypes.itemsChange:
|
|
return handleItemsChange(action.items, action.previousItems, state, context);
|
|
case _listActions.ListActionTypes.resetHighlight:
|
|
return handleResetHighlight(state, context);
|
|
case _listActions.ListActionTypes.highlightLast:
|
|
return handleHighlightLast(state, context);
|
|
case _listActions.ListActionTypes.clearSelection:
|
|
return handleClearSelection(state, context);
|
|
default:
|
|
return state;
|
|
}
|
|
} |