UNPKG

@applicaster/zapp-react-native-utils

Version:

Applicaster Zapp React Native utilities package

407 lines (332 loc) • 10.7 kB
import { NativeModules } from "react-native"; import * as R from "ramda"; import { Tree } from "./treeDataStructure/Tree"; import { findFocusableNode } from "./treeDataStructure/Utils"; import { subscriber } from "../../functionUtils"; const { FocusableManagerModule } = NativeModules; /** * Request native tvOS focus engine to focus on requested item immidiatelly * * @param {String} groupId Id of the selected item * @param {String} itemId Id of the selected group * @param {function} callback Callback from native side that focusable item was focused * */ export function forceFocusableFocus( groupId: string, itemId: string, callback: (() => void) | null = null ) { FocusableManagerModule.forceFocus(groupId, itemId, () => { callback && callback(); }); } /** * Request native tvOS focus engine to set this item as preffered to focus without forceing to focus on it * * @param {String} groupId Id of the selected item * @param {String} itemId Id of the selected group * */ export function setPreferredFocus(groupId, itemId) { FocusableManagerModule.setPreferredFocus(groupId, itemId); } export const focusManager = (function () { const focusables = {}; const focusableGroups = {}; const eventHandler = subscriber(); const { on, invokeHandler, removeHandler } = eventHandler; const focusableTree = new Tree(() => { // use this function to debug the tree of nodes when new elements are registered }); let currentFocus = null; function getPreferredFocusInGroup({ groupId }) { const node = focusableTree.findInTree(groupId); if (node?.children?.length > 0) { const preferredFocus = R.compose( R.find(R.pathEq(["component", "props", "preferredFocus"], true)), R.prop("children") )(node); if (preferredFocus?.component?.isGroup) { return getPreferredFocusInGroup({ groupId: preferredFocus?.id }); } return preferredFocus; } return node; } /** * Find first item inside subchildren of the group wherre first focusable item, * has initialFocus = true in specific group. * * @param {String} groupId Unique group id where item will be searched * @param {function} callback Callback from native side that focusable item was focused * */ function findInitiallyFocusedItemInGroup({ groupId }) { if (R.isNil(groupId)) { return; } // Define focusables by groupId const focusableByGroups = R.compose( R.groupBy(R.path(["props", "groupId"])), R.values )(focusables); if (R.isNil(focusableByGroups?.[groupId])) { const childrenGroups = R.compose( R.filter(R.pathEq(["props", "parentFocusableGroupId"], groupId)), R.values )(focusableGroups); const targetInChild = R.reduce( (result, current) => { return ( result || findInitiallyFocusedItemInGroup({ groupId: current.props.id }) ); }, null, childrenGroups ); if (targetInChild) return targetInChild; } // Try to find intial focusable item in a group const findFocusableInGroup = R.curry((predicateFn, groupId) => R.compose( R.unless(R.isNil, R.find(predicateFn)), R.prop(R.__, focusableByGroups) )(groupId) ); // Predicate that matches search result const predicate = R.pathEq(["props", "initialFocus"], true); const searchedInitialFocusableItem = findFocusableInGroup( predicate, groupId ); if (searchedInitialFocusableItem) return searchedInitialFocusableItem; return null; } function setInitialAppFocus({ groupId, initialFocusOnContent, callback }) { if (initialFocusOnContent) { const preferredFocus = getPreferredFocusInGroup({ groupId }); forceFocusableFocus( preferredFocus?.parent?.id, preferredFocus?.id, callback ); } } function registerGroup(id, component) { focusableGroups[id] = component; component.onRegister(component); focusableTree.addNode({ id, component }); return component; } function registerItem(id, component) { focusables[id] = component; component.onRegister(component); focusableTree.addNode({ id, component }); return component; } function unregisterGroup(id) { const focusableGroup = focusableGroups[id]; focusableTree.findAndRemoveNode(id); focusableGroup?.onUnregister(focusableGroup); delete focusableGroups[id]; } function unregisterItem(id) { const focusableItem = focusables[id]; focusableTree.findAndRemoveNode(id); if (id === currentFocus) { currentFocus = null; } if (focusableItem) { focusableItem.onUnregister(focusableItem); } delete focusables[id]; } function register({ id, component }) { const { isGroup = false } = component; return isGroup ? registerGroup(id, component) : registerItem(id, component); } function unregister(id, { group = false } = {}) { group ? unregisterGroup(id) : unregisterItem(id); } function getCurrentFocus() { if (!currentFocus) { return null; } return focusables[currentFocus] || null; } function getGroupById(groupId) { return focusableGroups[groupId] || focusables[groupId]; } function getGroupRootById(groupId) { const group = focusableTree.findInTree(groupId); return group || null; } // recursively check if group.children and children's children have component.state.focus as true const hasFocus = (node, id) => { if (node.children?.length > 0) { if (id) { return node.children .filter((item) => item.id.includes(id)) .some((item) => hasFocus(item, id)); } return node.children.some((item) => hasFocus(item, id)); } else if (node.component.state.focused) { return true; } return false; }; function isGroupItemFocused(groupId, id) { const group = getGroupRootById(groupId); if (!(group || group?.component)) { return false; } else { return hasFocus(group, id); } } function getCurrentGroup() { return getGroupById(R.path(["props", "groupId"], getCurrentFocus())); } function focus(direction, callback) { const currentFocusable = getCurrentFocus(); if (currentFocusable && !currentFocusable.isGroup) { currentFocusable.setFocus(direction, callback); currentFocusable.hasReceivedFocus(getCurrentFocus(), direction); } } function blur(direction) { const currentFocusable = getCurrentFocus(); if (currentFocusable && !currentFocusable.isGroup) { currentFocusable.blur(direction); currentFocusable.hasLostFocus(getCurrentFocus(), direction); } } function press() { getCurrentFocus().press(null); } function setFocus( id: string, direction?: FocusManager.IOS.Direction, options?: Partial<{ groupFocusedChanged: boolean }>, callback?: any ) { blur(direction); currentFocus = id; // Extract groupFocusedChanged with a default value of false let groupFocusedChanged = false; // Check if options is provided and is an object if (typeof options === "object" && options !== null) { groupFocusedChanged = options.groupFocusedChanged || false; } const currentGroup = getCurrentGroup(); // Check for groupFocusedChanged and currentGroup if (groupFocusedChanged && currentGroup) { currentGroup.hasLostFocus(currentGroup, direction); return; } // Check if callback is a function before calling it if (typeof callback === "function") { focus(direction, callback); } else { focus(direction, null); } } /** * Make focus on focusable item. * * @param {Object} focusableItem Item to focus * @param {function} callback Callback from native side that focusable item was focused * */ function forceFocusOnItem({ focusableItem, callback }) { if (R.compose(R.not, R.isNil)(focusableItem)) { focusableItem.willReceiveFocus(null); setFocus(focusableItem.props.id, null, {}, callback); } } /** * Try to focus on first item inside subchildren of the group wherre first focusable item has initialFocus = true. * * @param {String} groupId Unique group id where item will be searched * @param {function} callback Callback from native side that focusable item was focused * */ function forceFocusOnInitialFocusable({ groupId, callback }) { if (R.isNil(groupId)) { return; } const focusableItem = findInitiallyFocusedItemInGroup({ groupId, }); forceFocusOnItem({ focusableItem, callback }); } function moveFocus(direction: FocusManager.IOS.Direction) { let currentFocusNode = focusableTree.findInTree( getCurrentFocus()?.props?.id ); if (currentFocusNode) { currentFocusNode.component.willLoseFocus(currentFocusNode, direction); const nextFocusable = findFocusableNode(direction, currentFocusNode); if (nextFocusable?.component) { const nextRect = nextFocusable.component.getRect(); if (nextRect && nextRect.width > 0) { nextFocusable.component.willReceiveFocus( nextFocusable.component, direction ); blur(direction); const groupChanged = nextFocusable.component.props.groupId !== currentFocusNode?.component?.props?.groupId; currentFocusNode = nextFocusable; setFocus( nextFocusable.id, direction, { groupFocusedChanged: groupChanged, }, null ); } } else { currentFocusNode.component.failedLostFocus(currentFocusNode, direction); } } } /** * Try to focus on item with id * * @param {String} itemId Unique item id to focus. * @param {function} callback Callback from native side that focusable item was focused * */ function forceFocusOnFocusable({ itemId, callback }) { if (R.isNil(itemId)) { return; } const focusableItem = focusables[itemId]; forceFocusOnItem({ focusableItem, callback }); } return { on, invokeHandler, removeHandler, register, unregister, getCurrentFocus, getGroupById, setFocus, press, focus, forceFocusOnFocusable, forceFocusOnInitialFocusable, setInitialAppFocus, forceFocusOnItem, moveFocus, focusableTree, getCurrentGroup, getGroupRootById, isGroupItemFocused, }; })();