@applicaster/zapp-react-native-ui-components 15.0.0-rc.144 → 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
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import { View } from "react-native";
2
3
 
3
4
  import { BaseFocusable } from "../BaseFocusable";
4
5
  import { FocusableGroupContextProvider } from "../../Contexts/FocusableGroupContext";
@@ -75,7 +76,7 @@ class FocusableGroup extends BaseFocusable<Props> {
75
76
  const focusableId = `focusable-group-${id}`;
76
77
 
77
78
  return (
78
- <div
79
+ <View
79
80
  id={focusableId}
80
81
  data-testid={focusableId}
81
82
  style={style}
@@ -84,7 +85,7 @@ class FocusableGroup extends BaseFocusable<Props> {
84
85
  <FocusableGroupContextProvider id={id}>
85
86
  {children}
86
87
  </FocusableGroupContextProvider>
87
- </div>
88
+ </View>
88
89
  );
89
90
  }
90
91
  }
@@ -9,6 +9,11 @@ exports[`Layout TV renders 1`] = `
9
9
  excludeFromFocusSearching={true}
10
10
  id="/river/A1234"
11
11
  preferredFocus={true}
12
+ style={
13
+ {
14
+ "flex": 1,
15
+ }
16
+ }
12
17
  >
13
18
  <View
14
19
  Components={
@@ -0,0 +1,144 @@
1
+ # Config Builder -> React Component Migration Guide
2
+
3
+ This guide explains how to migrate MasterCell config-builders to React components incrementally, without breaking existing config-node rendering.
4
+
5
+ ## Why This Exists
6
+
7
+ MasterCell currently supports config-node builders such as:
8
+
9
+ ```ts
10
+ {
11
+ type: "View",
12
+ style: {},
13
+ additionalProps: {},
14
+ data: [],
15
+ elements: []
16
+ }
17
+ ```
18
+
19
+ This is still valid and should continue to work.
20
+
21
+ For incremental migration, MasterCell also supports inline React nodes:
22
+
23
+ ```ts
24
+ {
25
+ type: "ReactComponent",
26
+ style: {},
27
+ additionalProps: {
28
+ component: MyComponent
29
+ }
30
+ }
31
+ ```
32
+
33
+ This lets you migrate one builder at a time.
34
+
35
+ ## Rendering Pipeline
36
+
37
+ 1. Builder returns node config (`type`, `style`, `additionalProps`, `data`, `elements`).
38
+ 2. `configInflater` converts `additionalProps` into runtime `props` and resolves `data`.
39
+ 3. `elementMapper` renders:
40
+ 4. String `type` nodes via component registry (`components[type]`).
41
+ 5. `type: "ReactComponent"` nodes via `props.component`.
42
+
43
+ ## Migration Pattern
44
+
45
+ 1. Keep existing builder API unchanged.
46
+ 2. Add a dedicated React component for the node being migrated.
47
+ 3. Change builder output from string `type` to `type: "ReactComponent"`.
48
+ 4. Pass component reference through `additionalProps.component`.
49
+ 5. Keep behavioral props unchanged (`testID`, roles, accessibility props).
50
+ 6. Set `renderChildren` explicitly:
51
+ 7. `false` when React component renders everything itself.
52
+ 8. `true` (or omit) when builder still provides `elements` children.
53
+ 9. Set `requiresCellUUID: true` only if the component needs `cellUUID`.
54
+ 10. Update builder-shape tests and rendering tests together.
55
+
56
+ ## Example Using `TextLabelsContainer.ts`
57
+
58
+ Current builder (config-node only):
59
+
60
+ ```ts
61
+ export const TextLabelsContainer = ({ actionIdentifier, testID, style, extraProps }) => ({
62
+ type: "View",
63
+ additionalProps: {
64
+ mobileActionRole: "label_container",
65
+ },
66
+ elements: [
67
+ {
68
+ type: "Text",
69
+ style,
70
+ additionalProps: {
71
+ label: { context: actionIdentifier, name: "label_1" },
72
+ state: "default",
73
+ mobileActionRole: "label",
74
+ testID: testID ? `${testID}-label` : undefined,
75
+ ...extraProps,
76
+ },
77
+ },
78
+ ],
79
+ });
80
+ ```
81
+
82
+ Incremental migration target:
83
+
84
+ ```ts
85
+ import { TextLabelsContainerView } from "./TextLabelsContainerView";
86
+
87
+ export const TextLabelsContainer = ({ actionIdentifier, testID, style, extraProps }) => ({
88
+ type: "ReactComponent",
89
+ additionalProps: {
90
+ component: TextLabelsContainerView,
91
+ renderChildren: false,
92
+ mobileActionRole: "label_container",
93
+ actionIdentifier,
94
+ testID,
95
+ textStyle: style,
96
+ textProps: extraProps,
97
+ },
98
+ });
99
+ ```
100
+
101
+ And React component:
102
+
103
+ ```tsx
104
+ export const TextLabelsContainerView = ({
105
+ actionIdentifier,
106
+ testID,
107
+ textStyle,
108
+ textProps,
109
+ mobileActionRole,
110
+ }) => (
111
+ <View mobileActionRole={mobileActionRole}>
112
+ <Text
113
+ style={textStyle}
114
+ label={{ context: actionIdentifier, name: "label_1" }}
115
+ state="default"
116
+ mobileActionRole="label"
117
+ testID={testID ? `${testID}-label` : undefined}
118
+ {...textProps}
119
+ />
120
+ </View>
121
+ );
122
+ ```
123
+
124
+ This keeps external behavior and props contract intact while moving implementation to React.
125
+
126
+ ## Rules for Safe Incremental Migration
127
+
128
+ 1. Do not change role markers used by parent logic (for example `mobileActionRole`).
129
+ 2. Do not rename `testID` contracts during migration.
130
+ 3. Keep output shape stable for parent builders (`null` handling, `elements` presence).
131
+ 4. Keep data-mapping semantics identical (`label`, `state`, `entry`, etc.).
132
+ 5. Migrate one node per PR where possible.
133
+
134
+ ## Testing Checklist
135
+
136
+ 1. Builder test: node type and required props (`component`, role, `testID`) are correct.
137
+ 2. Rendering test: `configInflater + elementMapper` still renders expected UI.
138
+ 3. Behavior test: parent container logic that clones/filters children still works.
139
+ 4. Regression test: hidden/empty states still return `null` as before.
140
+
141
+ ## Important Limitation
142
+
143
+ `additionalProps.component` is a function reference and is suitable for local JS-built trees.
144
+ If a tree must be fully serialized JSON, use a string `componentKey` and resolve it in a registry instead of storing a function directly.
@@ -0,0 +1,165 @@
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from "react";
8
+ import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
9
+
10
+ type ActionDefinition = {
11
+ identifier?: string;
12
+ };
13
+
14
+ type ControllerRenderProps = {
15
+ actionContext: any;
16
+ actionState: any;
17
+ entry: any;
18
+ isActive: boolean;
19
+ onPress: () => Promise<unknown> | unknown;
20
+ };
21
+
22
+ type Props = {
23
+ action?: ActionDefinition;
24
+ pluginIdentifier?: string;
25
+ entry?: any;
26
+ onMissingActionContext?: (identifier?: string) => void;
27
+ children: (props: ControllerRenderProps) => React.ReactNode;
28
+ };
29
+
30
+ const resolveIsActive = (actionState: any, fallbackSelected = false) => {
31
+ if (actionState == null) {
32
+ return fallbackSelected;
33
+ }
34
+
35
+ return Boolean(
36
+ actionState?.active ??
37
+ actionState?.isActive ??
38
+ actionState?.selected ??
39
+ actionState?.isSelected ??
40
+ fallbackSelected
41
+ );
42
+ };
43
+
44
+ const buildLegacySelection = (entry: any, actionContext: any) => {
45
+ const defaultIsSelected = Array.isArray(actionContext?.state)
46
+ ? (actionContext?.state || []).includes(entry)
47
+ : false;
48
+
49
+ return actionContext?.masterCell?.isSelected
50
+ ? actionContext.masterCell.isSelected(entry)
51
+ : defaultIsSelected;
52
+ };
53
+
54
+ export function ActionButtonController({
55
+ action,
56
+ pluginIdentifier,
57
+ entry,
58
+ onMissingActionContext,
59
+ children,
60
+ }: Props) {
61
+ const resolvedEntry = entry;
62
+ const resolvedIdentifier = action?.identifier || pluginIdentifier || "";
63
+ const actionContext = useActions(resolvedIdentifier);
64
+
65
+ const actionDisabled =
66
+ typeof actionContext?.isActionAvailable === "function" &&
67
+ !actionContext.isActionAvailable(resolvedEntry);
68
+
69
+ const supportsEntryState =
70
+ typeof actionContext?.initialEntryState === "function" && !actionDisabled;
71
+
72
+ const hydrationKey = `${resolvedIdentifier}::${String(resolvedEntry?.id ?? "")}`;
73
+ const lastHydratedKeyRef = useRef<string | null>(null);
74
+
75
+ const [actionState, setActionState] = useState(() =>
76
+ supportsEntryState ? actionContext.initialEntryState(resolvedEntry) : null
77
+ );
78
+
79
+ useEffect(() => {
80
+ if (supportsEntryState) {
81
+ if (lastHydratedKeyRef.current === hydrationKey) {
82
+ return;
83
+ }
84
+
85
+ lastHydratedKeyRef.current = hydrationKey;
86
+ setActionState(actionContext.initialEntryState(resolvedEntry));
87
+ }
88
+ }, [supportsEntryState, actionContext, resolvedEntry, hydrationKey]);
89
+
90
+ useEffect(() => {
91
+ if (typeof actionContext?.addListener === "function") {
92
+ return actionContext.addListener(String(resolvedEntry?.id), (nextState) =>
93
+ setActionState(nextState)
94
+ );
95
+ }
96
+
97
+ if (typeof actionContext?.addListeners === "function") {
98
+ return actionContext.addListeners(
99
+ ({ entryState, entry: updatedEntry }) => {
100
+ if (updatedEntry?.id === resolvedEntry?.id) {
101
+ setActionState(entryState);
102
+ }
103
+ }
104
+ );
105
+ }
106
+
107
+ return undefined;
108
+ }, [
109
+ actionContext,
110
+ resolvedEntry?.id,
111
+ resolvedIdentifier,
112
+ actionContext?.addListener,
113
+ actionContext?.addListeners,
114
+ ]);
115
+
116
+ const legacySelected = useMemo(() => {
117
+ if (!actionContext || supportsEntryState) {
118
+ return false;
119
+ }
120
+
121
+ return buildLegacySelection(resolvedEntry, actionContext);
122
+ }, [actionContext, supportsEntryState, resolvedEntry]);
123
+
124
+ const isActive = resolveIsActive(actionState, legacySelected);
125
+
126
+ const onPress = useCallback(async () => {
127
+ if (!actionContext) {
128
+ return;
129
+ }
130
+
131
+ if (supportsEntryState) {
132
+ return actionContext.invokeAction?.(resolvedEntry, {
133
+ updateState: setActionState,
134
+ });
135
+ }
136
+
137
+ const favouritesAction = legacySelected
138
+ ? actionContext.removeFavourite
139
+ : actionContext.addFavourite;
140
+
141
+ const toggleAction = actionContext?.invokeAction ?? favouritesAction;
142
+
143
+ return toggleAction?.(resolvedEntry);
144
+ }, [actionContext, supportsEntryState, resolvedEntry, legacySelected]);
145
+
146
+ if (!actionContext || actionDisabled) {
147
+ if (!actionContext) {
148
+ onMissingActionContext?.(resolvedIdentifier);
149
+ }
150
+
151
+ return null;
152
+ }
153
+
154
+ return (
155
+ <>
156
+ {children({
157
+ actionContext,
158
+ actionState,
159
+ entry: resolvedEntry,
160
+ isActive,
161
+ onPress,
162
+ })}
163
+ </>
164
+ );
165
+ }