@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
283 lines (280 loc) • 10.4 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ensureSafeIndex = exports.default = exports.ACTIONS = void 0;
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _react = require("react");
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) { (0, _defineProperty2.default)(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; }
/**
* 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} />
* }
*
*/
var ACTIONS = exports.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,
focusOnSearch: true,
focusOnViewMore: false
});
case ACTIONS.MOVE:
return moveReducer(state, action);
}
return state;
};
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 {
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 ? state.selectedItemIndex + action.payload.positions : 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
});
};
};
function useSelectAndFocusOnArrowNavigation(listSize, step, canFocusViewMore) {
var _useReducer = (0, _react.useReducer)(reducer, initialState, getInitialState(listSize, canFocusViewMore)),
_useReducer2 = (0, _slicedToArray2.default)(_useReducer, 2),
state = _useReducer2[0],
dispatch = _useReducer2[1];
(0, _react.useEffect)(function () {
dispatch({
type: ACTIONS.UPDATE_STATE,
payload: {
canFocusViewMore: canFocusViewMore
}
});
}, [canFocusViewMore]);
var selectedItemIndex = state.selectedItemIndex,
focusedItemIndex = state.focusedItemIndex,
focusOnSearch = state.focusOnSearch,
focusOnViewMore = state.focusOnViewMore;
var reset = (0, _react.useCallback)(function (listSize) {
var payload = _objectSpread(_objectSpread({}, initialState), {}, {
listSize: listSize
});
dispatch({
type: ACTIONS.UPDATE_STATE,
payload: payload
});
}, []);
var removeFocusFromSearchAndSetOnItem = (0, _react.useCallback)(function (index) {
var payload = {
focusedItemIndex: index,
selectedItemIndex: index,
focusOnSearch: false,
focusOnViewMore: false
};
dispatch({
type: ACTIONS.UPDATE_STATE,
payload: payload
});
}, [dispatch]);
var setFocusOnSearch = (0, _react.useCallback)(function () {
dispatch({
type: ACTIONS.FOCUS_SEARCH,
payload: {}
});
}, [dispatch]);
var isMoving = (0, _react.useRef)(false);
var move = (0, _react.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 = (0, _react.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':
return move(e, +1);
case 'ArrowLeft':
return move(e, -1);
case 'ArrowDown':
return move(e, +step);
case 'ArrowUp':
return move(e, -step, step);
}
}, [focusOnSearch, setFocusOnSearch, move, step]);
(0, _react.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
};
}
var ensureSafeIndex = exports.ensureSafeIndex = function ensureSafeIndex(index, listSize) {
if (index < 0) {
return 0;
}
if (index > listSize) {
return listSize;
}
return index;
};
var _default = exports.default = useSelectAndFocusOnArrowNavigation;