import * as React from "react"; import { join, merge, path } from "ramda"; import { View } from "react-native"; import { Focusable } from "@applicaster/zapp-react-native-ui-components/Components/Focusable"; import { FocusableCell } from "@applicaster/zapp-react-native-ui-components/Components/FocusableCell"; import { getItemType } from "@applicaster/zapp-react-native-utils/navigationUtils"; import { SCREEN_TYPES } from "@applicaster/zapp-react-native-utils/navigationUtils/itemTypes"; import { sendSelectCellEvent } from "@applicaster/zapp-react-native-utils/analyticsUtils"; import { noop } from "@applicaster/zapp-react-native-utils/functionUtils"; import { CellFocusedStateContextProvider } from "@applicaster/zapp-react-native-ui-components/Contexts/CellFocusedStateContext"; import { CellWithFocusable } from "./CellWithFocusable"; import { BaseFocusable } from "../BaseFocusable"; import { AccessibilityManager } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager"; import { styles } from "./styles"; type Props = { dataLength: number; item: ZappEntry; index: number; shouldScrollHorizontally: (arg1: [any]) => boolean | null | undefined; shouldScrollVertically: (...args: any) => boolean | null | undefined; riverOffsetUpdater: (config: { focusable: BaseFocusable; componentAnchorPointY: number; headerOffset?: number; extraAnchorPointYOffset?: number; componentPaddingTop?: number; }) => void; offsetUpdater: (arg1: string, arg2: number, arg3: number) => number; componentId: string; component: { id: string; styles?: { component_padding_top?: number; }; }; selected?: boolean; CellRenderer: React.FunctionComponent & { hasFocusableInside: (item: ZappEntry) => boolean; }; preferredFocus: boolean; navigator: { push: (arg1: Record) => void; }; onFocus: (focusable?: any, mouse?: any) => void | null | undefined; onBlur: (arg1?: any) => void | null | undefined; onPress: (arg1?: any, index?: any) => void | null | undefined; willReceiveFocus: (arg1?: any) => void | null | undefined; hasReceivedFocus: (arg1?: any) => void | null | undefined; hasHeader: boolean; isScreenWrappedInContainer: boolean | null | undefined; sendOnClickEvent: () => void; scrollToIndex: (index: number) => void; appData: { layoutVersion: "v1" | "v2"; }; componentAnchorPointY: number; // | null | undefined; screenLayout: any; getHeaderOffset: () => { headerOffset?: number; headersAbove?: number; }; focused: boolean; groupId: string; isFocusable: boolean; skipFocusManagerRegistration?: boolean; shouldUpdate: boolean; behavior: Behavior; componentsMapOffset: number; }; type State = { hasFocusableInside: boolean; cellFocused: boolean; }; export class CellComponent extends React.Component { accessibilityManager: AccessibilityManager; constructor(props) { super(props); this.onPress = this.onPress.bind(this); this.onFocus = this.onFocus.bind(this); this.onBlur = this.onBlur.bind(this); this.willReceiveFocus = this.willReceiveFocus.bind(this); this.hasReceivedFocus = this.hasReceivedFocus.bind(this); this.scrollVertically = this.scrollVertically.bind(this); this.scrollToIndex = this.scrollToIndex.bind(this); this.handleAccessibilityFocus = this.handleAccessibilityFocus.bind(this); this.state = { hasFocusableInside: props.CellRenderer.hasFocusableInside?.(props.item), cellFocused: false, }; this.accessibilityManager = AccessibilityManager.getInstance(); } setScreenLayout(componentAnchorPointY, screenLayout) { if (componentAnchorPointY && screenLayout) { const updateLayout = componentAnchorPointY !== screenLayout?.componentAnchorPointY; updateLayout && screenLayout?.setScreenLayout({ componentAnchorPointY }); } } onPress() { const { onPress, item, index } = this.props; if (onPress) { onPress(item, index); } else { const { item, navigator, component, appData } = this.props; const itemTargetScreen = path(["screen_type"], item); const componentTargetScreen = path(["data", "target"], component); const itemType = getItemType(item, appData?.layoutVersion); sendSelectCellEvent(item, component, item.title, index); if ( itemType !== SCREEN_TYPES.PLAYABLE && componentTargetScreen && !itemTargetScreen ) { navigator.push(merge(item, { screen_type: componentTargetScreen })); return; } navigator.push(item); } } onFocus(focusable) { const { item: { id, title }, shouldScrollVertically, shouldUpdate, isFocusable = true, } = this.props; if (isFocusable) { this.setState({ cellFocused: true }); if (shouldUpdate && shouldScrollVertically?.(focusable, id, title)) { this.scrollVertically(focusable); } } } onBlur() { this.setState({ cellFocused: false }); } willReceiveFocus() {} hasReceivedFocus() {} scrollVertically(focusable) { // TODO | https://applicaster.monday.com/boards/3659728745/pulses/5873048938 | refactor to remove riverOffsetUpdater, componentAnchorPointY, screenLayout, getHeaderOffset, const { riverOffsetUpdater, componentAnchorPointY, screenLayout, getHeaderOffset, } = this.props; if (!focusable?.mouse) { const { headerOffset } = getHeaderOffset(); const extraAnchorPointYOffset = screenLayout?.extraAnchorPointYOffset || 0; riverOffsetUpdater({ focusable, componentAnchorPointY, headerOffset, extraAnchorPointYOffset, componentPaddingTop: this.props?.component?.styles?.component_padding_top, }); } } scrollToIndex(focusable: { mouse?: boolean } = {}) { return (index: number) => { if (!focusable?.mouse) { this.props?.scrollToIndex?.(index); } }; } isCellFocused(focusableFocused) { const { focused, isFocusable = true } = this.props; return !isFocusable ? false : focused || focusableFocused; } handleAccessibilityFocus(index, dataLength) { // For loop scrolling, calculate the correct logical index const logicalIndex = dataLength ? index % dataLength : index; const positionLabel = dataLength ? `item ${logicalIndex + 1} of ${dataLength}` : ""; if (this.state.hasFocusableInside) { this.accessibilityManager.readText({ text: " ", }); } else if (this.state.cellFocused) { this.accessibilityManager.readText({ text: `${positionLabel}`, }); } } componentDidUpdate(prevProps: Readonly, prevState: Readonly) { if (prevProps.item !== this.props.item) { this.setState({ hasFocusableInside: this.props.CellRenderer.hasFocusableInside?.( this.props.item ), }); } if ( prevState.cellFocused !== this.state.cellFocused || this.state.hasFocusableInside ) { this.handleAccessibilityFocus(this.props.index, this.props.dataLength); } } render() { const { item, index, component, offsetUpdater, preferredFocus, CellRenderer, onFocus, onBlur, willReceiveFocus, hasReceivedFocus, groupId, isFocusable = true, skipFocusManagerRegistration, behavior, } = this.props; const { id } = item; const focusableId = join("-", [component?.id, id, index]); const handleFocus = (focusable, mouse) => { const focusFn = onFocus || noop; focusFn(focusable, mouse); this.onFocus(focusable); }; if (this.state.hasFocusableInside) { return ( ); } return ( {(focused, event) => { const isFocused = this.isCellFocused(focused); return ( ); }} ); } }