import type { ReactElement, ReactNode } from "react";
import { useMemo, useRef } from "react";
import { useDir } from "../Dir";
import {
DEFAULT_KEYBOARD_MOVEMENT,
DEFAULT_LTR_KEYBOARD_MOVEMENT,
DEFAULT_RTL_KEYBOARD_MOVEMENT,
KeyboardMovementContextProvider,
} from "./movementContext";
import type {
KeyboardFocusContext,
KeyboardFocusElementData,
KeyboardMovementBehavior,
KeyboardMovementConfig,
KeyboardMovementConfiguration,
} from "./types";
import { getSearchText } from "./utils";
/**
* @remarks \@since 5.0.0
*/
export interface KeyboardMovementProviderProps
extends KeyboardMovementBehavior,
KeyboardMovementConfiguration {
children: ReactNode;
}
/**
* @example
* Main Usage
* ```tsx
* function Example() {
* return (
*
*
*
* );
* }
*
* function CustomKeyboardFocusWidget() {
* const { focusIndex: _focusIndex, ...eventHandlers } = useKeyboardFocus();
* return (
*
*
*
*
*
*
* );
* }
*
* function FocusableChild() {
* const refCallback = useKeyboardFocusableElement()
*
* return Content
;
* }
* ```
*
* @remarks \@since 5.0.0
*/
export function KeyboardMovementProvider({
children,
loopable = false,
searchable = false,
horizontal = false,
includeDisabled = false,
incrementKeys: propIncrementKeys,
decrementKeys: propDecrementKeys,
jumpToFirstKeys: propJumpToFirstKeys,
jumpToLastKeys: propJumpToLastKeys,
}: KeyboardMovementProviderProps): ReactElement {
const isRTL = useDir().dir === "rtl";
let defaults: Readonly>;
if (horizontal) {
defaults = isRTL
? DEFAULT_RTL_KEYBOARD_MOVEMENT
: DEFAULT_LTR_KEYBOARD_MOVEMENT;
} else {
defaults = DEFAULT_KEYBOARD_MOVEMENT;
}
const incrementKeys = propIncrementKeys || defaults.incrementKeys;
const decrementKeys = propDecrementKeys || defaults.decrementKeys;
const jumpToFirstKeys = propJumpToFirstKeys || defaults.jumpToFirstKeys;
const jumpToLastKeys = propJumpToLastKeys || defaults.jumpToLastKeys;
const watching = useRef([]);
const configuration: KeyboardMovementConfig = {
incrementKeys,
decrementKeys,
jumpToFirstKeys,
jumpToLastKeys,
};
const config = useRef(configuration);
config.current = configuration;
const value = useMemo(
() => ({
attach(element) {
watching.current.push({
element,
content: getSearchText(element, searchable),
});
},
detach(element) {
watching.current = watching.current.filter(
(cache) => cache.element !== element
);
},
watching,
config,
loopable,
searchable,
horizontal,
includeDisabled: includeDisabled,
}),
[horizontal, includeDisabled, loopable, searchable]
);
return (
{children}
);
}