@applicaster/zapp-react-native-ui-components 15.0.0-rc.143 → 15.0.0-rc.145

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/Components/FocusableGroup/index.tsx +3 -2
  2. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +5 -0
  3. package/Components/MasterCell/CONFIG_BUILDER_TO_REACT_COMPONENT.md +144 -0
  4. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +165 -0
  5. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +405 -0
  6. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/index.ts +1 -0
  7. package/Components/MasterCell/DefaultComponents/ButtonContainerView/components/HorizontalSeparator.tsx +8 -0
  8. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +15 -0
  9. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tv.android.tsx +58 -0
  10. package/Components/MasterCell/DefaultComponents/{tv/ButtonContainerView/index.tsx → ButtonContainerView/index.tv.tsx} +3 -11
  11. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.web.ts +1 -0
  12. package/Components/MasterCell/DefaultComponents/ButtonContainerView/types.ts +40 -0
  13. package/Components/MasterCell/DefaultComponents/DataProvider/index.tsx +163 -0
  14. package/Components/MasterCell/DefaultComponents/FocusableView/index.android.tsx +2 -23
  15. package/Components/MasterCell/DefaultComponents/FocusableView/index.tsx +4 -22
  16. package/Components/MasterCell/DefaultComponents/PressableView.tsx +7 -234
  17. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +11 -0
  18. package/Components/MasterCell/DefaultComponents/__tests__/DataProvider.test.tsx +141 -0
  19. package/Components/MasterCell/DefaultComponents/index.ts +7 -3
  20. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +135 -0
  21. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +20 -29
  22. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +22 -0
  23. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +67 -69
  24. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +21 -16
  25. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +207 -9
  26. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/builders.test.ts +56 -55
  27. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +137 -16
  28. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +49 -31
  29. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +165 -0
  30. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Asset.ts +4 -18
  31. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Button.ts +24 -73
  32. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TextLabelsContainer.ts +37 -18
  33. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TvActionButton.tsx +27 -0
  34. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/index.test.ts +24 -21
  35. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/renderedTree.test.tsx +231 -0
  36. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +24 -12
  37. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +62 -0
  38. package/Components/MasterCell/MappingFunctions/index.js +3 -2
  39. package/Components/MasterCell/README.md +4 -0
  40. package/Components/MasterCell/__tests__/__snapshots__/dataAdapter.test.js.snap +24 -0
  41. package/Components/MasterCell/__tests__/configInflater.test.js +1 -0
  42. package/Components/MasterCell/__tests__/elementMapper.test.js +46 -0
  43. package/Components/MasterCell/dataAdapter.ts +4 -1
  44. package/Components/MasterCell/elementMapper.tsx +51 -7
  45. package/Components/MasterCell/utils/__tests__/cloneChildrenWithIds.test.tsx +43 -0
  46. package/Components/MasterCell/utils/__tests__/useFilterChildren.test.tsx +80 -0
  47. package/Components/MasterCell/utils/index.ts +85 -15
  48. package/Components/Navigator/StackNavigator.tsx +6 -0
  49. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +4 -1
  50. package/package.json +5 -5
  51. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ButtonContainerView.ts +0 -23
  52. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/index.android.tsx +0 -135
  53. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/types.ts +0 -25
@@ -8,6 +8,8 @@ type ElementProps = {
8
8
  key: string;
9
9
  };
10
10
 
11
+ const REACT_COMPONENT_TYPE = "ReactComponent";
12
+
11
13
  export function mapElementWithKey(fn) {
12
14
  return function (element, key) {
13
15
  return fn({ ...element, key });
@@ -18,12 +20,23 @@ export function mapElementWithKey(fn) {
18
20
  const focusableTypes = new Set([
19
21
  "View",
20
22
  "ButtonContainerView",
21
- "FocusableView",
23
+ "TvActionButton",
22
24
  "PressableView",
23
25
  "CollapsibleTextContainer",
24
26
  "BorderContainerView",
25
27
  ]);
26
28
 
29
+ const childrenRenderingTypes = new Set([
30
+ "View",
31
+ "ButtonContainerView",
32
+ "PressableView",
33
+ "CollapsibleTextContainer",
34
+ "BorderContainerView",
35
+ "DataProvider",
36
+ "TvActionButton",
37
+ "MobileActionButton",
38
+ ]);
39
+
27
40
  const cellPlayerTypes = new Set([
28
41
  "Image",
29
42
  "LiveImage",
@@ -31,6 +44,23 @@ const cellPlayerTypes = new Set([
31
44
  "DynamicBadge",
32
45
  ]);
33
46
 
47
+ const resolveComponent = (type, props, components) => {
48
+ if (type === REACT_COMPONENT_TYPE) {
49
+ return props?.component;
50
+ }
51
+
52
+ return components[type];
53
+ };
54
+
55
+ const shouldInjectCellUUID = (type, props) =>
56
+ cellPlayerTypes.has(type) ||
57
+ (type === REACT_COMPONENT_TYPE && Boolean(props?.requiresCellUUID));
58
+
59
+ const shouldRenderChildren = (type, props) =>
60
+ type === REACT_COMPONENT_TYPE
61
+ ? props?.renderChildren !== false
62
+ : childrenRenderingTypes.has(type);
63
+
34
64
  /**
35
65
  * Maps a provided node in a master cell configuration to a React Component tree with appropriate props & styles
36
66
  * Curried function of the form elementMapper(components)(element)
@@ -59,7 +89,7 @@ export function elementMapper(
59
89
  elements = [],
60
90
  key,
61
91
  }: ElementProps) {
62
- const propsForCellPlayer = cellPlayerTypes.has(type)
92
+ const propsForCellPlayer = shouldInjectCellUUID(type, props)
63
93
  ? {
64
94
  cellUUID: otherProps?.cellUUID,
65
95
  }
@@ -87,14 +117,28 @@ export function elementMapper(
87
117
 
88
118
  if (hidden) return null;
89
119
 
90
- const Component = components[type];
120
+ const Component = resolveComponent(type, componentProps, components);
121
+
122
+ if (!Component) return null;
123
+
124
+ const componentPropsToRender =
125
+ type === REACT_COMPONENT_TYPE
126
+ ? (({
127
+ component: _component,
128
+ renderChildren: _renderChildren,
129
+ requiresCellUUID: _requiresCellUUID,
130
+ ...rest
131
+ }: Record<string, any>) => rest)(componentProps)
132
+ : componentProps;
133
+
134
+ const canRenderChildren =
135
+ elements.length > 0 && shouldRenderChildren(type, componentProps);
136
+
91
137
  const fn = mapElementWithKey(elementMapper(components, otherProps));
92
138
 
93
139
  return (
94
- <Component key={key} {...componentProps}>
95
- {focusableTypes.has(type) && elements.length > 0
96
- ? elements.map(fn)
97
- : null}
140
+ <Component key={key} {...componentPropsToRender}>
141
+ {canRenderChildren ? elements.map(fn) : null}
98
142
  </Component>
99
143
  );
100
144
  };
@@ -0,0 +1,43 @@
1
+ import React from "react";
2
+
3
+ import { DataProvider } from "../../DefaultComponents/DataProvider";
4
+ import { cloneChildrenWithIds } from "..";
5
+
6
+ const Probe = () => null;
7
+
8
+ describe("cloneChildrenWithIds", () => {
9
+ it("injects next focus props into plain children", () => {
10
+ const children = [
11
+ <Probe key="one" />,
12
+ <Probe key="two" />,
13
+ <Probe key="three" />,
14
+ ];
15
+
16
+ const result = cloneChildrenWithIds(["id-1", "id-2", "id-3"], children);
17
+
18
+ expect(result[0].props.nextFocusLeft).toBeUndefined();
19
+ expect(result[0].props.nextFocusRight).toBe("id-2");
20
+ expect(result[1].props.nextFocusLeft).toBe("id-1");
21
+ expect(result[1].props.nextFocusRight).toBe("id-3");
22
+ expect(result[2].props.nextFocusLeft).toBe("id-2");
23
+ expect(result[2].props.nextFocusRight).toBeUndefined();
24
+ });
25
+
26
+ it("injects next focus props into DataProvider-wrapped children", () => {
27
+ const children = [
28
+ <DataProvider key="one" entry={{ id: "entry-1" }}>
29
+ <Probe suffixId="button_1" />
30
+ </DataProvider>,
31
+ <DataProvider key="two" entry={{ id: "entry-2" }}>
32
+ <Probe suffixId="button_2" />
33
+ </DataProvider>,
34
+ ];
35
+
36
+ const result = cloneChildrenWithIds(["id-1", "id-2"], children);
37
+
38
+ expect(result[0].props.children.props.nextFocusLeft).toBeUndefined();
39
+ expect(result[0].props.children.props.nextFocusRight).toBe("id-2");
40
+ expect(result[1].props.children.props.nextFocusLeft).toBe("id-1");
41
+ expect(result[1].props.children.props.nextFocusRight).toBeUndefined();
42
+ });
43
+ });
@@ -0,0 +1,80 @@
1
+ import React from "react";
2
+ import { renderHook } from "@testing-library/react-native";
3
+ import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
4
+
5
+ import { DataProvider } from "../../DefaultComponents/DataProvider";
6
+ import { useFilterChildren } from "..";
7
+
8
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/actions", () => ({
9
+ useActions: jest.fn(),
10
+ }));
11
+
12
+ const mockUseActions = useActions as jest.Mock;
13
+ const Probe = () => null;
14
+
15
+ describe("useFilterChildren", () => {
16
+ beforeEach(() => {
17
+ jest.clearAllMocks();
18
+ });
19
+
20
+ it("filters DataProvider-wrapped button children using the wrapped button props", () => {
21
+ const available = jest.fn(() => true);
22
+ const unavailable = jest.fn(() => false);
23
+
24
+ mockUseActions.mockReturnValue({
25
+ actions: {
26
+ available_action: {
27
+ module: {
28
+ context: {
29
+ _currentValue: {
30
+ isActionAvailable: available,
31
+ },
32
+ },
33
+ },
34
+ },
35
+ unavailable_action: {
36
+ module: {
37
+ context: {
38
+ _currentValue: {
39
+ isActionAvailable: unavailable,
40
+ },
41
+ },
42
+ },
43
+ },
44
+ },
45
+ });
46
+
47
+ const availableItem = { id: "entry-1" };
48
+ const unavailableItem = { id: "entry-2" };
49
+
50
+ const children = [
51
+ <DataProvider key="available" entry={availableItem}>
52
+ <Probe
53
+ entry={availableItem}
54
+ pluginIdentifier="available_action"
55
+ suffixId="button_1"
56
+ cellUUID="cell-1"
57
+ />
58
+ </DataProvider>,
59
+ <DataProvider key="unavailable" entry={unavailableItem}>
60
+ <Probe
61
+ entry={unavailableItem}
62
+ pluginIdentifier="unavailable_action"
63
+ suffixId="button_2"
64
+ cellUUID="cell-1"
65
+ />
66
+ </DataProvider>,
67
+ ];
68
+
69
+ const { result } = renderHook(() => useFilterChildren(children));
70
+
71
+ expect(result.current).toHaveLength(1);
72
+
73
+ expect(result.current[0].props.children.props.pluginIdentifier).toBe(
74
+ "available_action"
75
+ );
76
+
77
+ expect(available).toHaveBeenCalledWith(availableItem);
78
+ expect(unavailable).toHaveBeenCalledWith(unavailableItem);
79
+ });
80
+ });
@@ -13,6 +13,7 @@ import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks";
13
13
  import { useScreenStateStore } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useScreenStateStore";
14
14
  import memoizee from "memoizee";
15
15
  import stringify from "fast-json-stable-stringify";
16
+ import { DataProvider } from "../DefaultComponents/DataProvider";
16
17
 
17
18
  const hasElementSpecificViewType = (viewType) => (element) => {
18
19
  if (R.isNil(element)) {
@@ -126,10 +127,50 @@ export function isVideoPreviewEnabled({
126
127
  return enable_video_preview && !R.isEmpty(player_screen_id);
127
128
  }
128
129
 
130
+ export const unwrapDataProviderChild = (
131
+ child: React.ReactElement
132
+ ): React.ReactElement => {
133
+ if ((child as any)?.type !== DataProvider) {
134
+ return child;
135
+ }
136
+
137
+ const [wrappedChild] = React.Children.toArray((child as any).props.children);
138
+
139
+ return (
140
+ React.isValidElement(wrappedChild) ? wrappedChild : child
141
+ ) as React.ReactElement;
142
+ };
143
+
144
+ /**
145
+ * Resolves the entry value passed by DataProvider.
146
+ * Prefer the direct `entry` prop and fall back to `dataProviderProps[_dataKey]`
147
+ * for older wrappers that still read from namespaced provider props.
148
+ */
149
+ const getEntryFromProps = (props: {
150
+ dataProviderProps?: {
151
+ _dataKey: string;
152
+ [key: string]: unknown;
153
+ };
154
+ entry?: any;
155
+ }) => {
156
+ const dataProviderProps = props.dataProviderProps;
157
+
158
+ return (
159
+ props.entry ??
160
+ (dataProviderProps?._dataKey
161
+ ? dataProviderProps[dataProviderProps._dataKey]
162
+ : undefined)
163
+ );
164
+ };
165
+
129
166
  export const useFilterChildren = <
130
167
  T extends {
131
168
  props: {
132
- item: any;
169
+ dataProviderProps?: {
170
+ _dataKey: string;
171
+ [key: string]: unknown;
172
+ };
173
+ entry?: any;
133
174
  pluginIdentifier: string;
134
175
  };
135
176
  },
@@ -139,8 +180,13 @@ export const useFilterChildren = <
139
180
  const actions = useActions("");
140
181
 
141
182
  const filteredChildren = children.filter((child) => {
142
- const item = child.props.item;
143
- const actionIdentifier = child.props.pluginIdentifier;
183
+ const wrappedChild = unwrapDataProviderChild(
184
+ child as unknown as React.ReactElement
185
+ ) as unknown as T;
186
+
187
+ const item = getEntryFromProps(wrappedChild.props);
188
+
189
+ const actionIdentifier = wrappedChild.props.pluginIdentifier;
144
190
  const action = actions.actions[actionIdentifier];
145
191
 
146
192
  // context value of specific plugin
@@ -154,7 +200,7 @@ export const useFilterChildren = <
154
200
 
155
201
  masterCellLogger.error({
156
202
  message: `Action plugin for ${actionIdentifier} could not be found, check the configuration of your action button`,
157
- data: { item, action: child.props.pluginIdentifier },
203
+ data: { item, action: wrappedChild.props.pluginIdentifier },
158
204
  });
159
205
 
160
206
  return false;
@@ -202,17 +248,42 @@ export const recursiveCloneElementsWithState = (focused: boolean, children) => {
202
248
  const next = (currentIndex, items) => items[currentIndex + 1];
203
249
  const previous = (currentIndex, items) => items[currentIndex - 1];
204
250
 
205
- export const cloneElementsWithIds = (ids, children) => {
251
+ export const cloneChildrenWithIds = (
252
+ ids: string[],
253
+ children: React.ReactElement[]
254
+ ) => {
206
255
  if (R.isNil(children) || R.isEmpty(children)) {
207
256
  return undefined;
208
257
  }
209
258
 
210
- return React.Children.map(children, (element, index) =>
211
- React.cloneElement(element, {
212
- nextFocusLeft: previous(index, ids),
213
- nextFocusRight: next(index, ids),
214
- })
215
- );
259
+ return React.Children.map(children, (element, index) => {
260
+ const nextFocusLeft = previous(index, ids);
261
+ const nextFocusRight = next(index, ids);
262
+
263
+ if (element?.type !== DataProvider) {
264
+ return React.cloneElement(element, {
265
+ nextFocusLeft,
266
+ nextFocusRight,
267
+ });
268
+ }
269
+
270
+ const wrappedChild = unwrapDataProviderChild(element) as React.ReactElement<
271
+ Record<string, unknown>
272
+ >;
273
+
274
+ if (!React.isValidElement(wrappedChild)) {
275
+ return element;
276
+ }
277
+
278
+ const injectedWrappedChild = React.cloneElement(wrappedChild, {
279
+ nextFocusLeft,
280
+ nextFocusRight,
281
+ });
282
+
283
+ return React.cloneElement(element, {
284
+ children: injectedWrappedChild,
285
+ });
286
+ });
216
287
  };
217
288
 
218
289
  export const getFocusedButtonId = (focusable) => {
@@ -252,10 +323,9 @@ export const useCellState = ({
252
323
  export const hasFocusableInsideBuilder = (elementsBuilder) => (item) => {
253
324
  const elements = elementsBuilder({ entry: item });
254
325
 
255
- return R.anyPass([
256
- hasElementsSpecificViewType("ButtonContainerView"),
257
- hasElementsSpecificViewType("FocusableView"),
258
- ])(elements);
326
+ return R.anyPass([hasElementsSpecificViewType("ButtonContainerView")])(
327
+ elements
328
+ );
259
329
  };
260
330
 
261
331
  export function getEntryState(state, selected) {
@@ -6,11 +6,16 @@ import { getPathAttributes } from "@applicaster/zapp-react-native-utils/navigati
6
6
  import { FocusableGroup } from "@applicaster/zapp-react-native-ui-components/Components/FocusableGroup";
7
7
  import { ScreenDataContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenDataContext";
8
8
  import { PathnameContext } from "@applicaster/zapp-react-native-ui-components/Contexts/PathnameContext";
9
+ import { StyleSheet } from "react-native";
9
10
 
10
11
  import { Screen } from "../Screen/TV/index.web";
11
12
  import { isLast } from "@applicaster/zapp-react-native-utils/arrayUtils";
12
13
  import { ScreenContextProvider } from "../../Contexts/ScreenContext";
13
14
 
15
+ const styles = StyleSheet.create({
16
+ container: { flex: 1 },
17
+ });
18
+
14
19
  type Components = {
15
20
  NavBar: React.ComponentType<any>;
16
21
  Background: React.ComponentType<any>;
@@ -41,6 +46,7 @@ export const StackNavigator = ({ Components }: Props) => {
41
46
  preferredFocus={isLast(index, mainStack.length)}
42
47
  excludeFromFocusSearching
43
48
  key={routeId}
49
+ style={styles.container}
44
50
  >
45
51
  <ScreenDataContext.Provider value={state}>
46
52
  <PathnameContext.Provider value={route}>
@@ -25,6 +25,7 @@ export const SHOWN = 1; // opacity = 1
25
25
 
26
26
  type Props = {
27
27
  componentsToRender: ZappUIComponent[];
28
+ backgroundColor?: string;
28
29
  };
29
30
 
30
31
  export const withScreenRevealManager = (Component) => {
@@ -97,7 +98,9 @@ export const withScreenRevealManager = (Component) => {
97
98
  styles.container,
98
99
  {
99
100
  opacity: opacityRef.current,
100
- backgroundColor: theme.app_background_color,
101
+ // TODO: we should support background image as well, but for now we will use background color from theme
102
+ backgroundColor:
103
+ props.backgroundColor ?? theme.app_background_color,
101
104
  },
102
105
  ]}
103
106
  testID="animated-component"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "15.0.0-rc.143",
3
+ "version": "15.0.0-rc.145",
4
4
  "description": "Applicaster Zapp React Native ui components for the Quick Brick App",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,10 +28,10 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "15.0.0-rc.143",
32
- "@applicaster/zapp-react-native-bridge": "15.0.0-rc.143",
33
- "@applicaster/zapp-react-native-redux": "15.0.0-rc.143",
34
- "@applicaster/zapp-react-native-utils": "15.0.0-rc.143",
31
+ "@applicaster/applicaster-types": "15.0.0-rc.145",
32
+ "@applicaster/zapp-react-native-bridge": "15.0.0-rc.145",
33
+ "@applicaster/zapp-react-native-redux": "15.0.0-rc.145",
34
+ "@applicaster/zapp-react-native-utils": "15.0.0-rc.145",
35
35
  "fast-json-stable-stringify": "^2.1.0",
36
36
  "promise": "^8.3.0",
37
37
  "url": "^0.11.0",
@@ -1,23 +0,0 @@
1
- type Props = {
2
- style: Record<string, unknown>;
3
- contentStyle: Record<string, unknown>;
4
- elements: Array<Record<string, unknown>>;
5
- };
6
-
7
- export const ButtonContainerView = ({
8
- style,
9
- contentStyle,
10
- elements,
11
- }: Props) => {
12
- return {
13
- type: "View",
14
- style,
15
- elements: [
16
- {
17
- type: "View",
18
- style: contentStyle,
19
- elements,
20
- },
21
- ],
22
- };
23
- };
@@ -1,135 +0,0 @@
1
- import React from "react";
2
- import { View, ButtonProps } from "react-native";
3
- import { Focusable } from "@applicaster/zapp-react-native-ui-components/Components/Focusable";
4
- import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
5
- import * as R from "ramda";
6
- import { useInitialFocus } from "@applicaster/zapp-react-native-utils/focusManager";
7
- import { getXray } from "@applicaster/zapp-react-native-utils/logger";
8
- import { useFocusable } from "@applicaster/zapp-react-native-ui-components/Components/Focusable/index.android";
9
- import {
10
- useIsRTL,
11
- applyRTLStylesIfNeeded,
12
- } from "@applicaster/zapp-react-native-utils/localizationUtils";
13
-
14
- const { Logger } = getXray();
15
-
16
- import {
17
- cloneElementsWithIds,
18
- insertBetween,
19
- recursiveCloneElementsWithState,
20
- useFilterChildren,
21
- } from "../../../utils";
22
-
23
- import type {
24
- HorizontalSeparatorProps,
25
- ContainerProps,
26
- ContainerChildren,
27
- } from "./types";
28
-
29
- const logger = new Logger("plugin", "plugins/navigation-action");
30
-
31
- const generateId = (cellUUID, suffixId) => `${cellUUID}--${suffixId}`;
32
-
33
- const HorizontalSeparator = ({ width }: HorizontalSeparatorProps) => (
34
- <View style={{ width }} />
35
- );
36
-
37
- export function ButtonContainerView({
38
- style,
39
- children,
40
- ...otherProps
41
- }: ContainerProps) {
42
- const isRTL = useIsRTL();
43
-
44
- const horizontalGutter = R.pathOr(0, ["horizontalGutter"], otherProps);
45
-
46
- const filteredChildren = useFilterChildren<ContainerChildren>(children);
47
-
48
- const buttonIds = filteredChildren.map((child) => {
49
- const { cellUUID, suffixId } = child.props;
50
-
51
- return generateId(cellUUID, suffixId);
52
- });
53
-
54
- useInitialFocus(otherProps.state === "focused", R.head(buttonIds));
55
-
56
- if (R.isEmpty(filteredChildren)) {
57
- return null;
58
- }
59
-
60
- return (
61
- <View style={applyRTLStylesIfNeeded(style, isRTL)}>
62
- {insertBetween(
63
- (index) => (
64
- <HorizontalSeparator
65
- key={`separator_${index}`}
66
- width={horizontalGutter}
67
- />
68
- ),
69
- cloneElementsWithIds(buttonIds, filteredChildren)
70
- )}
71
- </View>
72
- );
73
- }
74
-
75
- export function FocusableViewComponent(
76
- { style, children, item, ...otherProps }: ButtonProps,
77
- ref
78
- ) {
79
- const {
80
- cellUUID,
81
- groupId,
82
- suffixId,
83
- normalStyles,
84
- focusedStyles,
85
- nextFocusLeft,
86
- nextFocusRight,
87
- pluginIdentifier,
88
- disableFocus,
89
- } = otherProps;
90
-
91
- const parentFocus = useFocusable();
92
-
93
- const actionContext = useActions(pluginIdentifier);
94
-
95
- const onPress = () => {
96
- if (!actionContext) {
97
- logger.warning(
98
- `Cannot resolve action context for ${pluginIdentifier} - please make sure the plugin is installed and up to date`
99
- );
100
-
101
- return;
102
- }
103
-
104
- actionContext?.invokeAction?.(item);
105
- };
106
-
107
- return (
108
- <Focusable
109
- id={generateId(cellUUID, suffixId)}
110
- disableFocus={disableFocus}
111
- groupId={groupId}
112
- onPress={onPress}
113
- nextFocusUp={parentFocus?.nextFocusUp}
114
- nextFocusDown={parentFocus?.nextFocusDown}
115
- nextFocusLeft={nextFocusLeft || parentFocus?.nextFocusLeft}
116
- nextFocusRight={nextFocusRight || parentFocus?.nextFocusRight}
117
- ref={ref}
118
- >
119
- {(isFocused) => {
120
- const additionalStyles = isFocused ? focusedStyles : normalStyles;
121
-
122
- return (
123
- <View
124
- style={{
125
- ...style,
126
- ...additionalStyles,
127
- }}
128
- >
129
- {recursiveCloneElementsWithState(isFocused, children)}
130
- </View>
131
- );
132
- }}
133
- </Focusable>
134
- );
135
- }
@@ -1,25 +0,0 @@
1
- import { ImageStyle } from "react-native";
2
-
3
- export type ContainerProps = Record<string, any> & {
4
- buttonsToggleEnabled: boolean;
5
- skipButtons: boolean;
6
- style: ImageStyle;
7
- children: ContainerChildren[];
8
- };
9
-
10
- export type ButtonProps = Record<string, any> & {
11
- style: ImageStyle;
12
- };
13
-
14
- export type HorizontalSeparatorProps = {
15
- width: number;
16
- };
17
-
18
- export type ContainerChildren = {
19
- props: {
20
- item: any;
21
- pluginIdentifier: string;
22
- cellUUID: string;
23
- suffixId: string;
24
- };
25
- };