UNPKG

@momentum-ui/react-collaboration

Version:

Cisco Momentum UI Framework for React Collaboration Applications

142 lines 6.77 kB
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react'; import { useKeyboard } from '@react-aria/interactions'; import { setNextFocus, onCurrentFocusNotFound } from '../components/List/List.utils'; import { useFocusWithinState } from './useFocusState'; import { isNumber } from 'lodash'; import { usePrevious } from './usePrevious'; import { useSpatialNavigationContext } from '../components/SpatialNavigationProvider/SpatialNavigationProvider.utils'; var useOrientationBasedKeyboardNavigation = function (props) { var spatialNav = useSpatialNavigationContext(); var allItemIndexes = props.allItemIndexes, listSize = props.listSize, orientation = props.orientation, noLoop = props.noLoop, contextProps = props.contextProps, _a = props.initialFocus, initialFocus = _a === void 0 ? 0 : _a; var _b = useState(-1), currentFocus = _b[0], setCurrentFocusInternal = _b[1]; var _c = useState(true), updateFocusBlocked = _c[0], setUpdateFocusBlockedInternal = _c[1]; // When a new list item registers itself with this hook, we need to tell it // whether it is focused and whether the focus setting is blocked // We don't want the addFocusCallback to have a dependency on either of these // as it would then cause all the list items to re-render when the focus changes // so we use refs to keep track of the current values var currentFocusRef = useRef(currentFocus); var focusCallbacks = useRef({}); var updateFocusBlockedRef = useRef(updateFocusBlocked); var setUpdateFocusBlocked = useCallback(function (value) { updateFocusBlockedRef.current = value; setUpdateFocusBlockedInternal(value); }, []); var setCurrentFocus = useCallback(function (index) { currentFocusRef.current = index; setCurrentFocusInternal(index); }, []); var addFocusCallback = useCallback(function (index, callback) { if (callback) { focusCallbacks.current[index] = callback; } else { delete focusCallbacks.current[index]; } callback === null || callback === void 0 ? void 0 : callback(index === currentFocusRef.current, updateFocusBlockedRef.current); }, []); var _d = useFocusWithinState({}), isFocusedWithin = _d.isFocusedWithin, focusWithinProps = _d.focusWithinProps; var lastCurrentFocus = usePrevious(currentFocus); // If current focus changes, we need to let both the old and the new focused list item know useLayoutEffect(function () { var _a, _b, _c, _d; if (lastCurrentFocus !== currentFocus) { (_b = (_a = focusCallbacks.current)[lastCurrentFocus]) === null || _b === void 0 ? void 0 : _b.call(_a, false, updateFocusBlocked); (_d = (_c = focusCallbacks.current)[currentFocus]) === null || _d === void 0 ? void 0 : _d.call(_c, true, updateFocusBlocked); } }, [currentFocus, isFocusedWithin, lastCurrentFocus, updateFocusBlocked]); // When the initial focus changes, we temporarily disable the automatic focus of the currentFocus // Once that new focused item has rendered, it will re-enable the automatic focus useLayoutEffect(function () { if (!isFocusedWithin) { setUpdateFocusBlocked(true); setCurrentFocus(initialFocus); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialFocus]); var getCurrentFocus = useCallback(function () { return currentFocusRef.current; }, []); var _e = contextProps || {}, shouldFocusOnPress = _e.shouldFocusOnPress, shouldItemFocusBeInset = _e.shouldItemFocusBeInset; // It is important to keep frequently changing values out of the context // The context is used by all the list items and we want to keep list item // re-renders to a minimum var context = useMemo(function () { return ({ noLoop: spatialNav ? true : noLoop, setCurrentFocus: setCurrentFocus, setUpdateFocusBlocked: setUpdateFocusBlocked, isFocusedWithin: isFocusedWithin, addFocusCallback: addFocusCallback, getCurrentFocus: getCurrentFocus, shouldFocusOnPress: shouldFocusOnPress, shouldItemFocusBeInset: shouldItemFocusBeInset, }); }, [ noLoop, setCurrentFocus, setUpdateFocusBlocked, isFocusedWithin, addFocusCallback, getCurrentFocus, shouldFocusOnPress, shouldItemFocusBeInset, spatialNav, ]); var getContext = useCallback(function () { return context; }, [context]); var previousAllItemIndexes = usePrevious(allItemIndexes); useEffect(function () { if (!allItemIndexes) { if (!isNumber(currentFocus)) { console.warn('Unable to handle non-numeric index without allItemIndexes', currentFocus); return; } if (currentFocus >= listSize) { var newFocus = listSize - 1; if (newFocus >= 0) { setCurrentFocus(newFocus); } else { setCurrentFocus(initialFocus); } } return; } if (currentFocus !== -1 && !allItemIndexes.includes(currentFocus)) { setCurrentFocus(onCurrentFocusNotFound(currentFocus, allItemIndexes, previousAllItemIndexes)); } }, [ allItemIndexes, currentFocus, initialFocus, listSize, previousAllItemIndexes, setCurrentFocus, ]); var keyboardProps = useKeyboard({ onKeyDown: function (evt) { var forwardKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown'; var backwardKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp'; switch (evt.key) { case 'Escape': evt.continuePropagation(); break; case backwardKey: case forwardKey: { var next = setNextFocus(currentFocus, listSize, evt.key === backwardKey, context.noLoop, allItemIndexes); if (next !== undefined) { evt.nativeEvent.stopImmediatePropagation(); evt.preventDefault(); setCurrentFocus(next); } break; } default: break; } }, }).keyboardProps; return { keyboardProps: keyboardProps, focusWithinProps: focusWithinProps, getContext: getContext, }; }; export default useOrientationBasedKeyboardNavigation; //# sourceMappingURL=useOrientationBasedKeyboardNavigation.js.map