@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
396 lines (387 loc) • 15.2 kB
JavaScript
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
import { useCallback, useEffect, useReducer, useRef } from 'react';
import { fg } from '@atlaskit/platform-feature-flags';
/**
* a custom hook that handles keyboard navigation for Arrow keys based on a
* given listSize, and a step (for up and down arrows).
*
* @param {number} listSize
* @param {number} upDownStep
*
* Example usage:
* const list = ['Confluence','Jira','Atlaskit'];
* const {
* selectedItemIndex,
* focusedItemIndex,
* focusOnSearch,
* focusOnViewMore,
* setFocusedItemIndex,
* onKeyDown
* } = useSelectAndFocusOnArrowNavigation(list.length - 1, 1);
*
* return (
* <div onKeyDown={onKeyDown}>
* <SearchBar onClick={() => setFocusedItemIndex(undefined)} focus={focusOnSearch} />
* {list.map((item, index) => (
* <ListItem
* title={item}
* style={{ ..., color: selected ? 'selectedStateColor' : defaultColor }}
* onClick={() => {
* setFocusedItemIndex(index);
* }
* />
* )}
* </div>
* );
*
* const SearchBar = ({ focus }) => {
* const ref = useRefToFocusOrScroll(focus);
* return <input ref={ref} />
* }
*
*/
export var ACTIONS = /*#__PURE__*/function (ACTIONS) {
ACTIONS["FOCUS_SEARCH"] = "focusOnSearch";
ACTIONS["UPDATE_STATE"] = "updateState";
ACTIONS["MOVE"] = "move";
return ACTIONS;
}({});
var reducer = function reducer(state, action) {
switch (action.type) {
case ACTIONS.UPDATE_STATE:
return _objectSpread(_objectSpread({}, state), action.payload);
case ACTIONS.FOCUS_SEARCH:
return _objectSpread(_objectSpread({}, state), {}, {
focusedItemIndex: undefined,
focusedCategoryIndex: undefined,
focusOnSearch: true,
focusOnViewMore: false
});
case ACTIONS.MOVE:
return moveReducer(state, action);
}
};
var moveReducer = function moveReducer(state, action) {
var listSize = state.listSize,
canFocusViewMore = state.canFocusViewMore;
if (state.focusOnSearch) {
// up arrow
if (action.payload.positions && action.payload.positions <= -1) {
return _objectSpread(_objectSpread({}, state), {}, {
focusOnSearch: false,
focusOnViewMore: !!canFocusViewMore,
focusedItemIndex: canFocusViewMore ? undefined : listSize,
selectedItemIndex: canFocusViewMore ? undefined : listSize
});
} else {
if (fg('platform_editor_is_disabled_state_element_browser')) {
var _action$payload$step;
var _newIndex = action.payload.positions ? action.payload.positions - ((_action$payload$step = action.payload.step) !== null && _action$payload$step !== void 0 ? _action$payload$step : 1) : 0;
var _safeIndex = ensureSafeIndex(_newIndex, state.listSize);
var isLastItemFocused = _newIndex > listSize;
var focusOnSearch = isLastItemFocused && !canFocusViewMore;
var focusOnViewMore = isLastItemFocused && !!canFocusViewMore;
return _objectSpread(_objectSpread({}, state), {}, {
focusOnSearch: focusOnSearch,
focusOnViewMore: focusOnViewMore,
focusedItemIndex: _safeIndex,
selectedItemIndex: _safeIndex
});
} else {
return _objectSpread(_objectSpread({}, state), {}, {
focusOnSearch: false,
focusOnViewMore: false,
focusedItemIndex: 0,
selectedItemIndex: 0
});
}
}
}
if (state.focusOnViewMore) {
// down arrow
if (action.payload.positions === 1) {
return _objectSpread(_objectSpread({}, state), {}, {
focusOnSearch: true,
focusOnViewMore: false,
focusedItemIndex: undefined,
// if search is focused then select first item.
selectedItemIndex: 0
});
} else {
return _objectSpread(_objectSpread({}, state), {}, {
focusOnSearch: false,
focusOnViewMore: false,
focusedItemIndex: listSize,
selectedItemIndex: listSize
});
}
}
var newIndex = state.selectedItemIndex ?
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
state.selectedItemIndex + action.payload.positions :
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
action.payload.positions;
var safeIndex = ensureSafeIndex(newIndex, state.listSize);
// down arrow key is pressed or right arrow key is pressed.
if (state.focusedItemIndex !== undefined && action.payload.positions && action.payload.positions >= 1) {
// when multi column element browser is open and we are in last
// row then newIndex will be greater than listSize when
// down arrow key is pressed.
// Or when last item is focused and down or right arrow key is pressed.
var _isLastItemFocused = newIndex > listSize;
var _focusOnSearch = _isLastItemFocused && !canFocusViewMore;
var _focusOnViewMore = _isLastItemFocused && !!canFocusViewMore;
// if search is focused, then select first item.
// otherwise if view more is focused then select item should be undefined.
var selectedItemIndex = _focusOnSearch ? 0 : _focusOnViewMore ? undefined : safeIndex;
return _objectSpread(_objectSpread({}, state), {}, {
focusOnSearch: _focusOnSearch,
focusOnViewMore: _focusOnViewMore,
selectedItemIndex: selectedItemIndex,
focusedItemIndex: _isLastItemFocused ? undefined : safeIndex
});
}
// up arrow key is pressed or left arrow key is pressed.
if (state.focusedItemIndex !== undefined && action.payload.positions && action.payload.positions <= -1) {
// if arrow up key is pressed when focus is in first row,
// or, arrow left key is pressed when first item is focused,
// then newIndex will become less than zero.
// In this case, focus search, and, kept previously selected item.
var isFirstRowFocused = newIndex < 0;
// if focus goes to search then kept last selected item in first row.
var _selectedItemIndex = isFirstRowFocused ? state.selectedItemIndex : safeIndex;
return _objectSpread(_objectSpread({}, state), {}, {
// focus search if first item is focused on up or left arrow key
focusOnSearch: isFirstRowFocused,
focusOnViewMore: false,
focusedItemIndex: isFirstRowFocused ? undefined : safeIndex,
selectedItemIndex: _selectedItemIndex
});
}
return _objectSpread(_objectSpread({}, state), {}, {
focusOnSearch: false,
focusOnViewMore: false,
selectedItemIndex: safeIndex,
focusedItemIndex: safeIndex
});
};
var initialState = {
focusOnSearch: true,
focusOnViewMore: false,
selectedItemIndex: 0,
focusedItemIndex: undefined,
listSize: 0
};
var getInitialState = function getInitialState(listSize, canFocusViewMore) {
return function (initialState) {
return _objectSpread(_objectSpread({}, initialState), {}, {
listSize: listSize,
canFocusViewMore: canFocusViewMore
});
};
};
// Get the offset forwards - skip items that are disabled
var skipForwardOffsetToSafeItem = function skipForwardOffsetToSafeItem(currentIndex, listSize, stepSize, itemIsDisabled
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/max-params
) {
if (currentIndex === undefined) {
return undefined;
}
// Iterate through the list starting from the next item
for (var offset = 1; currentIndex + offset * stepSize <= listSize; offset++) {
if (!itemIsDisabled(currentIndex + offset * stepSize)) {
return offset * stepSize;
}
}
// Move to the last place if possible
return listSize - currentIndex + 1;
};
// Get the offset backwards - skip items that are disabled
var skipBackwardOffsetToSafeItem = function skipBackwardOffsetToSafeItem(currentIndex, stepSize, itemIsDisabled) {
if (currentIndex === undefined) {
return undefined;
}
// Iterate backwards starting from the previous item
for (var offset = 1; currentIndex - offset * stepSize >= -1; offset++) {
if (!itemIsDisabled(currentIndex - offset * stepSize) || currentIndex - offset * stepSize === -1) {
return offset * stepSize;
}
}
// Move to search if no available index
return currentIndex + 1;
};
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/max-params
function useSelectAndFocusOnArrowNavigation(listSize, step, canFocusViewMore, itemIsDisabled, isFocusSearch) {
var _useReducer = useReducer(reducer, initialState, getInitialState(listSize, canFocusViewMore)),
_useReducer2 = _slicedToArray(_useReducer, 2),
state = _useReducer2[0],
dispatch = _useReducer2[1];
useEffect(function () {
dispatch({
type: ACTIONS.UPDATE_STATE,
payload: {
canFocusViewMore: canFocusViewMore
}
});
}, [canFocusViewMore]);
var selectedItemIndex = state.selectedItemIndex,
focusedItemIndex = state.focusedItemIndex,
focusOnSearch = state.focusOnSearch,
focusOnViewMore = state.focusOnViewMore,
focusedCategoryIndex = state.focusedCategoryIndex;
// calls if items size changed
var reset = useCallback(function (listSize) {
var payload = _objectSpread(_objectSpread({}, initialState), {}, {
listSize: listSize
});
// A11Y: if categories exist ,on the initial render search element should receive focus.
// After user pick some category the category should stay focused.
payload = Object.assign(payload, {
focusOnSearch: isFocusSearch !== null && isFocusSearch !== void 0 ? isFocusSearch : initialState.focusOnSearch
});
dispatch({
type: ACTIONS.UPDATE_STATE,
payload: payload
});
}, [isFocusSearch]);
var removeFocusFromSearchAndSetOnItem = useCallback(function (index) {
var payload = {
focusedItemIndex: index,
selectedItemIndex: index,
focusOnSearch: false,
focusOnViewMore: false
};
payload = Object.assign(payload, {
focusedCategoryIndex: undefined
});
dispatch({
type: ACTIONS.UPDATE_STATE,
payload: payload
});
}, [dispatch]);
var setFocusedCategoryIndex = useCallback(function (index) {
var payload = {
focusOnSearch: false,
focusOnViewMore: false,
focusedCategoryIndex: index,
focusedItemIndex: undefined
};
dispatch({
type: ACTIONS.UPDATE_STATE,
payload: payload
});
}, [dispatch]);
var setFocusOnSearch = useCallback(function () {
dispatch({
type: ACTIONS.FOCUS_SEARCH,
payload: {}
});
}, [dispatch]);
var isMoving = useRef(false);
var move = useCallback(function (e, positions, actualStep) {
e.preventDefault();
e.stopPropagation();
// avoid firing 2 moves at the same time when holding an arrow down as this can freeze the screen
if (!isMoving.current) {
isMoving.current = true;
requestAnimationFrame(function () {
isMoving.current = false;
dispatch({
type: ACTIONS.MOVE,
payload: {
positions: positions,
step: actualStep
}
});
});
}
}, []);
var onKeyDown = useCallback(function (e) {
var avoidKeysWhileSearching = ['/',
// While already focused on search bar, let users type in.
'ArrowRight', 'ArrowLeft'];
if (focusOnSearch && avoidKeysWhileSearching.includes(e.key)) {
return;
}
switch (e.key) {
case '/':
e.preventDefault();
e.stopPropagation();
return setFocusOnSearch();
case 'ArrowRight':
{
if (fg('platform_editor_is_disabled_state_element_browser')) {
var _skipForwardOffsetToS;
var itemIndex = focusOnSearch ? -1 : selectedItemIndex;
var nextItem = (_skipForwardOffsetToS = skipForwardOffsetToSafeItem(itemIndex, listSize, 1, itemIsDisabled)) !== null && _skipForwardOffsetToS !== void 0 ? _skipForwardOffsetToS : 1;
return move(e, nextItem);
} else {
return move(e, +1);
}
}
case 'ArrowLeft':
{
if (fg('platform_editor_is_disabled_state_element_browser')) {
var _skipBackwardOffsetTo;
var _nextItem = (_skipBackwardOffsetTo = skipBackwardOffsetToSafeItem(selectedItemIndex, 1, itemIsDisabled)) !== null && _skipBackwardOffsetTo !== void 0 ? _skipBackwardOffsetTo : 1;
return move(e, -_nextItem);
} else {
return move(e, -1);
}
}
case 'ArrowDown':
{
if (fg('platform_editor_is_disabled_state_element_browser')) {
var _skipForwardOffsetToS2;
var _itemIndex = focusOnSearch ? -step : selectedItemIndex;
var _nextItem2 = (_skipForwardOffsetToS2 = skipForwardOffsetToSafeItem(_itemIndex, listSize, step, itemIsDisabled)) !== null && _skipForwardOffsetToS2 !== void 0 ? _skipForwardOffsetToS2 : step;
return move(e, +_nextItem2, step);
} else {
return move(e, +step);
}
}
case 'ArrowUp':
{
if (fg('platform_editor_is_disabled_state_element_browser')) {
var _skipBackwardOffsetTo2;
var _nextItem3 = (_skipBackwardOffsetTo2 = skipBackwardOffsetToSafeItem(selectedItemIndex, step, itemIsDisabled)) !== null && _skipBackwardOffsetTo2 !== void 0 ? _skipBackwardOffsetTo2 : step;
return move(e, Math.min(-_nextItem3, -step), step);
} else {
return move(e, -step, step);
}
}
}
}, [focusOnSearch, setFocusOnSearch, move, selectedItemIndex, itemIsDisabled, listSize, step]);
useEffect(function () {
// To reset selection when user filters
reset(listSize);
}, [listSize, reset]);
return {
selectedItemIndex: selectedItemIndex,
onKeyDown: onKeyDown,
focusOnSearch: focusOnSearch,
focusOnViewMore: focusOnViewMore,
setFocusOnSearch: setFocusOnSearch,
focusedItemIndex: focusedItemIndex,
setFocusedItemIndex: removeFocusFromSearchAndSetOnItem,
focusedCategoryIndex: focusedCategoryIndex,
setFocusedCategoryIndex: setFocusedCategoryIndex
};
}
export var ensureSafeIndex = function ensureSafeIndex(index, listSize) {
if (index < 0) {
return 0;
}
if (index > listSize) {
return listSize;
}
return index;
};
export default useSelectAndFocusOnArrowNavigation;