UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

396 lines (387 loc) • 15.2 kB
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;