import * as React from "react"; import * as R from "ramda"; import { BaseFocusable as BaseFocusableInterface, BoundingRect, } from "./BaseFocusable.interface"; import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager"; import { noop } from "@applicaster/zapp-react-native-utils/functionUtils"; type ScrollDirection = FocusManager.IOS.Direction | FocusManager.Web.Direction; type Props = { initialFocus?: boolean; id: string; groupId?: string; preferredFocus?: boolean; onRegister?: (focusable: FocusManager.TouchableRef) => void; onUnregister?: (focusable: FocusManager.TouchableRef) => void; willReceiveFocus?: FocusManager.FocusEventCB; hasReceivedFocus?: FocusManager.FocusEventCB; willLoseFocus?: FocusManager.FocusEventCB; hasLostFocus?: FocusManager.FocusEventCB; failedLostFocus?: FocusManager.FocusEventCB; onPress?: (nativeEvent?: Nullable) => void; onLongPress?: (nativeEvent?: Nullable) => void; onPressIn?: (nativeEvent?: Nullable) => void; onPressOut?: (nativeEvent?: Nullable) => void; onFocus?: FocusManager.FocusEventCB; onBlur?: FocusManager.FocusEventCB; selected?: boolean; skipFocusManagerRegistration?: boolean; }; export class BaseFocusable< T extends Props = Props, > extends BaseFocusableInterface { constructor(props) { super(props); const { initialFocus } = this.props; this.state = { focused: initialFocus || false, }; this.ref = React.createRef(); } componentDidMount() { this._isMounted = true; const component = this; const { skipFocusManagerRegistration } = this.props; if (skipFocusManagerRegistration) { return; } focusManager.register({ id: this.getId(), component: component, }); const { initialFocus } = this.props; if (initialFocus) { focusManager.setFocus(this.getId()); } } getId(props?: { id: string; groupId: string }) { const { id } = props || this.props; return id; } getRect() { let boundingRect: BoundingRect = {}; if (this.ref && this.ref.current) { boundingRect = this.ref.current.getBoundingClientRect(); } return boundingRect; } isMounted() { return this._isMounted; } componentWillUnmount() { this._isMounted = false; const { skipFocusManagerRegistration } = this.props; if (skipFocusManagerRegistration) { return; } focusManager.unregister(this.getId(), { group: this.isGroup || false }); } componentDidUpdate(prevProps) { const { skipFocusManagerRegistration } = this.props; if (skipFocusManagerRegistration) { return; } if (prevProps?.id && this.props?.id && prevProps.id !== this.props.id) { focusManager.unregister(this.getId(prevProps), { group: this.isGroup || false, }); } } measureView() {} onRegister = (focusable?: FocusManager.FocusableRef) => { const { onRegister = noop } = this.props; onRegister(focusable); }; onUnregister = (focusable?: FocusManager.FocusableRef) => { const { onUnregister = noop } = this.props; onUnregister(focusable); }; willLoseFocus: FocusManager.FocusEventCB = (focusable, scrollDirection) => { const { willLoseFocus = noop } = this.props; willLoseFocus(focusable, scrollDirection); }; willReceiveFocus: FocusManager.FocusEventCB = ( focusable, scrollDirection ) => { const { willReceiveFocus = noop } = this.props; willReceiveFocus(focusable, scrollDirection); }; /** * will invoke the underlying component's focus method * @param {Object} focusable - sender * @param {Object} scrollDirection * @returns {Promise} */ onFocus: FocusManager.FocusEventCB = ( focusable, scrollDirection, context ) => { const { onFocus = noop } = this.props; this.setFocusedState(true); onFocus(focusable, scrollDirection, context); }; /** * will invoke the underlying component's hasReceivedFocus method * @param {Object} focusable - sender * @param {Object} scrollDirection * @returns {Promise} */ hasReceivedFocus: FocusManager.FocusEventCB = ( focusable, scrollDirection ) => { const { hasReceivedFocus = noop } = this.props; hasReceivedFocus(focusable, scrollDirection); }; /** * will invoke the underlying component's hasLostFocus method * @param {Object} focusable - sender * @param {Object} scrollDirection * @returns {Promise} */ hasLostFocus: FocusManager.FocusEventCB = (focusable, scrollDirection) => { const { hasLostFocus = noop } = this.props; hasLostFocus(focusable, scrollDirection); }; /** * will invoke the underlying component's failedLostFocus method * @param {Object} focusable - sender * @param {Object} scrollDirection * @returns {Promise} */ failedLostFocus: FocusManager.FocusEventCB = ( focusable: FocusManager.FocusableRef | undefined, scrollDirection?: ScrollDirection ) => { const { failedLostFocus = noop } = this.props; failedLostFocus(focusable, scrollDirection); }; /** * will invoke the underlying component's onBlur method * @param {Object} focusable - sender * @param {Object} scrollDirection * @returns {Promise} */ onBlur: FocusManager.FocusEventCB = (focusable, scrollDirection) => { const { onBlur = noop } = this.props; this.setFocusedState(false); onBlur(focusable, scrollDirection); }; onPress = (keyEvent?: Nullable) => { const { onPress = noop } = this.props; onPress(keyEvent); }; /** * will invoke the underlying component's onLongPress method * @param {Object} keyEvent * @returns {Promise} */ onLongPress = (keyEvent?: Nullable) => { const { onLongPress = noop } = this.props; onLongPress(keyEvent); }; onPressOut = (keyEvent?: Nullable) => { const { onPressOut = noop } = this.props; onPressOut(keyEvent); }; onPressIn = (keyEvent?: Nullable) => { const { onPressIn = noop } = this.props; onPressIn(keyEvent); }; isInGroup(group: FocusManager.TouchableRef) { const { groupId } = this.props; const { id } = group.props; return id === groupId; } setFocusedState = (focused) => { if (this._isMounted) { this.setState({ focused }); } }; /** * will invoke the underlying component's focus method * @param {Object} scrollDirection * @returns {Promise} */ focus(_, scrollDirection, context?: FocusManager.FocusContext) { return this.onFocus(this, scrollDirection, context); // invokeComponentMethod(this, "onFocus", scrollDirection, context); } /** * will invoke the underlying component's blur method * @param {Object} scrollDirection * @returns {Promise} */ blur( _, scrollDirection?: FocusManager.Web.Direction | FocusManager.IOS.Direction, context?: FocusManager.FocusContext ) { return this.onBlur(this, scrollDirection, context); } /** * Sets the focus on this item. Will trigger sequentially a sequence of * functions (willReceiveFocus, focus, hasReceivedFocus). If these functions (defined in the Focusable * Item underlying component) return promises, execution will wait before it proceeds to the next. This * is useful for triggering sequential operations that specifically require to fully run before or after * focus is actually set on that item. * @param {string} scrollDirection string representation of the direction of the navigation which landed * to this item being focused */ _executeFocusSequence(methodNames, scrollDirection, context) { return R.reduce( (sequence, methodName) => { const method = this[methodName]; // Access the method by name if (typeof method !== "function") { throw new Error( `Method '${methodName}' not found or not a function.` ); } return sequence .then(() => method.call(this, this, scrollDirection, context)) .catch((e) => { throw e; // Re-throw for consistent error handling }); }, Promise.resolve(), methodNames ); } setFocus( scrollDirection?: ScrollDirection, context?: FocusManager.FocusContext ) { const focusMethods = ["willReceiveFocus", "focus", "hasReceivedFocus"]; return this._executeFocusSequence(focusMethods, scrollDirection, context); } setBlur( scrollDirection?: ScrollDirection, context?: FocusManager.FocusContext ) { const blurMethods = ["willLoseFocus", "blur", "hasLostFocus"]; return this._executeFocusSequence(blurMethods, scrollDirection, context); } }