UNPKG

@applicaster/zapp-react-native-utils

Version:

Applicaster Zapp React Native utilities package

343 lines (282 loc) • 9.63 kB
import { allPass, pathOr, startsWith, isNil } from "ramda"; import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers"; import { isZero } from "@applicaster/zapp-react-native-utils/numberUtils"; import { QUICK_BRICK_NAVBAR } from "@applicaster/quick-brick-core/const"; import { rejectItemByPath } from "../../../../objectUtils"; import { measureDistance, measureEuclidianDistanceToCenter, } from "../../../../rectUtils"; import { capitalize } from "../../../../stringUtils"; export const FocusDirection = { LEFT: 1, TOP: 2, RIGHT: 4, BOTTOM: 8, }; /** * Function search down the tree for priority item in given children array. * Priorities are: * Priority 1: Currently selected item - like a menu item * Priority 2: Previously focused item in the parent group * Priority 3: Preferrable items - items which have been marked as preferable * Priority 4: First item on the list - if by any accident above priories are not satisfied * @private * @param {Array} children * @returns focusable priority item. */ export function findPriorityItem(children) { let preferedGroups = getPriorityItems(children); if (preferedGroups.length > 0 && !preferedGroups[0].children) { return preferedGroups; } while ( preferedGroups.length > 0 && preferedGroups[0].children && preferedGroups[0].children.length > 0 ) { preferedGroups = preferedGroups.filter( (it) => it.children && it.children.length > 0 ); preferedGroups = getPriorityItems(preferedGroups[0].children); } return preferedGroups; } /** * Function search for currently selected item - like a menu item. * @private * @param {Array} children * @returns array of selected items */ export function getSelectedItems(children) { if (children) { const selectedItems = children.filter( (obj) => obj && obj?.component?.props?.selected ); return selectedItems; } return []; } /** * Function search for previously focused item in the parent group. * @private * @param {Array} children * @returns array of last focus items */ export function getLastFocusedItems(children) { if (children) { const lastFocusedItems = children.filter( (obj) => obj && obj?.parent?.component?.isWithMemory?.() && obj.id === obj.parent.lastFocusedItem ); return lastFocusedItems; } return []; } /** * Function search for items which have been marked as preferable. * @private * @param {Array} children * @returns array of prefereable focus items */ export function getPreferableItems(children) { if (children) { const preferredItems = children.filter( (obj) => obj && obj.component?.props.preferredFocus ); return preferredItems; } return []; } /** * Function search for priority items in children array. Priorities are: * Priority 1: Currently selected item - like a menu item * Priority 2: Previously focused item in the parent group * Priority 3: Preferrable items - items which have been marked as preferable * Priority 4: First item on the list - if by any accident above priories are not satisfied * @private * @param {Array} children * @returns array of pririty focus items */ export function getPriorityItems(children) { if (!children) { return []; } const selectedItems = getSelectedItems(children); const lastFocusedItems = getLastFocusedItems(children); const preferredItems = getPreferableItems(children); if (selectedItems.length > 0) { return selectedItems; } else if (lastFocusedItems.length > 0) { return lastFocusedItems; } else if (preferredItems.length > 0) { return preferredItems; } else if (children.length > 0) { return [children[0]]; } return []; } const hasChildren = (target) => !(target?.component?.isGroup && isNilOrEmpty(target?.children)); const hasComponent = (target) => !!target?.component; const hasComponentAndChildren = allPass([hasComponent, hasChildren]); const positiveOrZero = (value) => value > 0 || isZero(value); const excludeFromFocusSearching = pathOr(false, [ "component", "props", "excludeFromFocusSearching", ]); const isNavBar = (target) => startsWith(QUICK_BRICK_NAVBAR, target?.id); const ignoreMenuIfMultipleTargets = (nodes) => { if (nodes.length > 1) { return nodes.filter((n) => !isNavBar(n?.target)); } return nodes; }; const toDistance = ({ target, direction, comparisonRect }) => { const componentRect = target.component?.getRect(); const orientationDistance = measureDistance( direction, comparisonRect, componentRect ); if (orientationDistance === -1) { // ignores offscreen targets when moving up/down return null; } const euclidDistance = measureEuclidianDistanceToCenter( comparisonRect, componentRect ); return orientationDistance > 0 ? euclidDistance : orientationDistance; }; /** * Get target in shortest distance in specified direction * @private * @param {Object} direction of the navigation, which led to this focus change * @param {Object} excludedNode node which should be excluded from target group * @param {Array} targetNodes array of input target nodes * @param {Object} comparisonRect rectangle to compare sides in direction. * @returns {Object} target in direction. */ export function getTargetInDirection( direction, currentNode, targetNodes, comparisonRect ) { const overridePropName = `nextFocus${capitalize(direction.value)}`; const overrideTarget = currentNode?.component?.props[overridePropName]; if (overrideTarget) { const target = targetNodes.find((n) => n?.id === overrideTarget); if (target) return target; } const targets = rejectItemByPath(["id"], currentNode.id, targetNodes); const distances = targets .filter(hasComponentAndChildren) .filter((target) => !excludeFromFocusSearching(target)) .map((target) => ({ distance: toDistance({ target, direction, comparisonRect }), target, })) .filter(({ distance }) => positiveOrZero(distance)); const match = distances .sort((a, b) => a.distance - b.distance) // sorts the distances .filter(ignoreMenuIfMultipleTargets)[0]; // filters out multiple targets if necessary // gets the first element return match?.target; } /** * Find focusable node in direction from currentFocusedNode. * If it does not find a node in current group it seach parent node * incuding deep search inside simbilngs of parent. Search goes up the tree * until find next focusable item or tree ends. * @public * @param {Object} direction of the navigation, which led to this focus change * @param {Object} currentFocusNode currently focused node as start point of search * @returns {Object} focusable node in direction. */ export function findFocusableNode(direction, currentFocusNode) { const currentFocusable = currentFocusNode.component || null; if (currentFocusable) { let focusableNode = getTargetInDirection( direction, currentFocusNode, currentFocusNode.parent.children, currentFocusNode.component.getRect() ); const parentHasPriority = currentFocusNode.parent.component && hasPriorityInDirection(direction, currentFocusNode.parent.component); const canFindParentSibling = findFocusableNode( direction, currentFocusNode.parent ); if (parentHasPriority && canFindParentSibling) { return canFindParentSibling; } else if (focusableNode && focusableNode.component) { // inside group return focusableNode; } else { // outside group let parentFocusableNode = currentFocusNode; // going up the structure untill sibling node while ( !focusableNode && parentFocusableNode && parentFocusableNode.parent && parentFocusableNode.parent.parent ) { parentFocusableNode = parentFocusableNode.parent; focusableNode = getTargetInDirection( direction, parentFocusableNode, // excluded sibling parent of current focuable parentFocusableNode.parent.children, // siblings of parent currentFocusNode.component.getRect() ); } if (focusableNode) { const preferred = findPriorityItem(focusableNode.children); return preferred[0]; } } } } /** * Checks of component has priority in the direction. It is used to navigate * outside of the group if group has priotity in specified direction. * @public * @param {Object} direction of the navigation, which led to this focus change * @param {Object} component - object to check props.prioritiseFocusOn * @returns {Boolean} TRUE if object has priority in direction. * @returns {Boolean} FALSE if object has NO priority in direction. */ export function hasPriorityInDirection(direction, component) { const { prioritiseFocusOn = 0 } = component.props; const directionMapper = { left: 1, up: 2, right: 4, down: 8, }; const mappedDirection = directionMapper[direction.value]; return (prioritiseFocusOn & mappedDirection) > 0; } export const getNodeBeforeRoot = (node) => { if (isNil(node)) { return undefined; } if (isNil(node.parent)) { return undefined; } if (node.parent.id === "root") { return node; } return getNodeBeforeRoot(node.parent); }; export const haveSameParentBeforeRoot = (currentNode, nextNode) => { const routeForCurrentNode = getNodeBeforeRoot(currentNode); const routeForNextNode = getNodeBeforeRoot(nextNode); return routeForCurrentNode?.id === routeForNextNode?.id; };