@applicaster/zapp-react-native-utils
Version:
Applicaster Zapp React Native utilities package
407 lines (332 loc) • 10.7 kB
text/typescript
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,
};
})();