import * as React from "react"; import { ReactElement } from "react"; import * as R from "ramda"; import { Touchable } from "./Touchable"; import { useIsRTL } from "@applicaster/zapp-react-native-utils/localizationUtils"; import { noop } from "@applicaster/zapp-react-native-utils/functionUtils"; import { useFocusManager } from "@applicaster/zapp-react-native-utils/focusManager"; import { usePrevious } from "@applicaster/zapp-react-native-utils/reactHooks/utils"; type ChildrenFunction = ( focused: boolean, nextFocus: ParentFocus, ref: React.Ref ) => React.ReactNode; type Props = { children: ReactElement | ChildrenFunction; id: string; disableFocus?: boolean; blockFocus?: boolean; onPress?: (ref: FocusManager.FocusableRef) => void; onFocus?: ( ref: FocusManager.FocusableRef, options: FocusManager.Android.CallbackOptions, context?: FocusManager.FocusContext ) => void; onBlur?: ( ref: FocusManager.FocusableRef, options: FocusManager.Android.CallbackOptions ) => void; onPressIn?: (ref: FocusManager.FocusableRef) => void; onPressOut?: (ref: FocusManager.FocusableRef) => void; onLongPress?: (ref: FocusManager.FocusableRef) => void; onRegister?: () => void; onUnregister?: () => void; isFocusableCell?: boolean; /** only for FocusableScrollView */ onSetIsFocusable?: (isFocusable: boolean) => void; } & ParentFocus; export const FocusableContext = React.createContext< { focused: boolean; // eslint-disable-next-line setIsFocusable: (enableFocus: boolean) => void; ref: FocusManager.FocusableRef; parentFocusableId: Option; } & ParentFocus >({ focused: false, setIsFocusable: () => {}, ref: { current: null }, parentFocusableId: undefined, }); export const useFocusable = () => React.useContext(FocusableContext); function FocusableComponent(props: Props, forwardedRef) { const { children, id, nextFocusDown, nextFocusUp, nextFocusLeft, nextFocusRight, onPress, onFocus, onBlur, blockFocus, onPressIn, onPressOut, onLongPress, onRegister = noop, onUnregister = noop, isFocusableCell = true, onSetIsFocusable, } = props; const isRTL = useIsRTL(); const focusManager = useFocusManager(); const { ref: parentFocusableRef, parentFocusableId } = useFocusable(); const touchableRef = React.useRef(null); const [focused, setFocused] = React.useState(() => focusManager.isFocused(id) ); const [disableFocus, setDisableFocus] = React.useState(props.disableFocus); const prevDisableFocus = usePrevious(props.disableFocus); React.useImperativeHandle(forwardedRef, () => touchableRef.current); React.useEffect(() => { if (props.disableFocus !== prevDisableFocus) { setDisableFocus(props.disableFocus ?? false); } }, [props.disableFocus, prevDisableFocus]); React.useEffect(() => { if (disableFocus) { focusManager.onDisableFocusChange(id); } }, [disableFocus]); React.useLayoutEffect(() => { if (id) { const unregister = focusManager.registerFocusable({ touchableRef, parentFocusableRef, isFocusableCell, parentFocusableId, }); onRegister(); return () => { unregister(); onUnregister(); }; } }, [id, onRegister, onUnregister, isFocusableCell, parentFocusableId]); if (R.isNil(id)) { // eslint-disable-next-line no-console console.error( "Focusable: Id parameter or Setting ref on focusable component is required" ); } const _onFocus = React.useCallback( (ref, options, context) => { setFocused(true); onFocus?.(ref, options, context); }, [onFocus] ); const _onBlur = React.useCallback( (ref, options = {}) => { if (ref.props.id !== focusManager.focusedId) { setFocused(false); } onBlur?.(ref, options); }, [onBlur] ); const setIsFocusable = React.useCallback( (blockFocus) => { onSetIsFocusable?.(blockFocus); setDisableFocus(!blockFocus); }, [onSetIsFocusable] ); const parentFocus = React.useMemo( () => ({ nextFocusRight: isRTL ? nextFocusLeft : nextFocusRight, nextFocusLeft: isRTL ? nextFocusRight : nextFocusLeft, nextFocusUp, nextFocusDown, }), [isRTL, nextFocusDown, nextFocusUp, nextFocusLeft, nextFocusRight] ); const contextState = React.useMemo(() => { return { focused, ...parentFocus, ref: touchableRef, setIsFocusable, parentFocusableId: id, }; }, [parentFocus, focused, id]); return ( {React.useMemo( () => typeof children === "function" ? children(focused, parentFocus, touchableRef) : children, [children, focused, parentFocus] )} ); } export const Focusable = React.forwardRef(FocusableComponent);