@applicaster/zapp-react-native-utils
Version:
Applicaster Zapp React Native utilities package
161 lines (126 loc) • 4.14 kB
text/typescript
import * as React from "react";
import * as R from "ramda";
import { focusManager } from "./FocusManager";
import { NON_FOCUSABLE_COMPONENTS } from "./const";
import { useFocusable } from "@applicaster/zapp-react-native-ui-components/Components/Focusable/index.android";
import { toBooleanWithDefaultTrue } from "../booleanUtils";
import { isNilOrEmpty } from "../reactUtils/helpers";
import { isAndroidTVPlatform } from "../reactUtils";
import { noop } from "../functionUtils";
const EMPTY_GROUP_COMPONENT = "empty_group_component";
export function useFocusManager() {
return focusManager;
}
export function useInitialFocus(
focused: boolean,
initialRef?: FocusManager.TouchableReactRef | string,
options: {
refsList?: Array<FocusManager.TouchableReactRef> | Array<string>;
withStateMemory?: boolean;
initialFocusDirection?: FocusManager.Android.FocusNavigationDirections;
initialScrollIndex?: number;
} = {}
): ((index: number) => void) | (() => void) {
const { withStateMemory, refsList } = options;
const currentlyFocusedIndex = React.useRef<number>(
options?.initialScrollIndex || 0
);
const setCurrentlyFocusedIndex = React.useCallback(
(index: number) => (currentlyFocusedIndex.current = index),
[]
);
const focusManager = useFocusManager();
const nextFocus =
(withStateMemory && refsList?.[currentlyFocusedIndex.current]) ||
initialRef;
React.useEffect(() => {
if (focused && nextFocus) {
focusManager.setFocus(nextFocus, {
initialFocusDirection: options?.initialFocusDirection,
});
}
}, [focused, initialRef]);
if (withStateMemory) {
return setCurrentlyFocusedIndex;
}
return noop;
}
export function useFocusRefs() {
const { current } = React.useRef([]);
return current;
}
export function useFocusEffect(onFocus, focusWatcher) {
const [focused] = focusWatcher;
React.useLayoutEffect(() => {
let onBlur = () => null;
if (focused) {
onBlur = onFocus?.();
} else {
onBlur?.();
}
}, focusWatcher);
}
export function shouldShowEmptyGroup(component) {
return (
component?.ui_components?.filter(
({ component_type }) => component_type === EMPTY_GROUP_COMPONENT
)?.length > 0
);
}
export const isFocusable = (component: ZappEntry | ZappUIComponent | any) =>
R.tryCatch(
R.compose(
R.not,
R.includes(R.__, NON_FOCUSABLE_COMPONENTS),
R.prop("component_type")
),
R.T
)(component);
type Props = {
component: ZappUIComponent;
zappPipesData?: ZappPipesData;
} & Record<string, any>;
export const useIsFocusable = (props: Props): void => {
const { component, zappPipesData } = props;
const { setIsFocusable } = useFocusable();
const componentCellsFocusable = toBooleanWithDefaultTrue(
component?.rules?.component_cells_focusable
);
const hideIfDataEmpty = toBooleanWithDefaultTrue(
component.rules.hide_component_if_data_is_empty
);
const isLoadingData = !!zappPipesData?.loading;
const hasNoData =
isNilOrEmpty(zappPipesData?.data) ||
isNilOrEmpty(zappPipesData?.data?.entry);
const hasNoUIComponents = isNilOrEmpty(component?.ui_components);
const hasDataError = !!zappPipesData?.error;
const hasEmptyGroup = shouldShowEmptyGroup(component);
const isGallery = component?.component_type === "gallery-qb";
const isGroup = component?.component_type === "group-qb";
const isHiddenDueToEmptyData =
!isLoadingData && hasNoData && hideIfDataEmpty && !hasEmptyGroup;
const isEmptyGroup = isGroup && hasNoUIComponents;
const defaultFocusableLogic =
componentCellsFocusable &&
(!hasNoData || !hideIfDataEmpty) &&
!isEmptyGroup;
React.useEffect(() => {
if (isAndroidTVPlatform()) {
let isFocusableNew = false;
if (isGallery) {
isFocusableNew = true;
} else if (isFocusable(component)) {
isFocusableNew = defaultFocusableLogic;
}
setIsFocusable(isFocusableNew);
}
}, [
defaultFocusableLogic,
isHiddenDueToEmptyData,
isGallery,
setIsFocusable,
hasDataError,
hasEmptyGroup,
]);
};