@momentum-ui/react-collaboration
Version:
Cisco Momentum UI Framework for React Collaboration Applications
142 lines • 6.77 kB
JavaScript
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