@applicaster/zapp-react-native-ui-components 15.1.0-rc.1 → 16.0.0-rc.1

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 (144) hide show
  1. package/Components/BaseFocusable/index.ios.ts +12 -2
  2. package/Components/Cell/FocusableWrapper.tsx +3 -0
  3. package/Components/Cell/TvOSCellComponent.tsx +6 -3
  4. package/Components/Focusable/Focusable.tsx +4 -2
  5. package/Components/Focusable/FocusableTvOS.tsx +18 -1
  6. package/Components/Focusable/__tests__/__snapshots__/FocusableTvOS.test.tsx.snap +1 -0
  7. package/Components/FocusableGroup/FocusableTvOS.tsx +30 -1
  8. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +1 -1
  9. package/Components/HandlePlayable/HandlePlayable.tsx +13 -8
  10. package/Components/Layout/TV/LayoutBackground.tsx +5 -2
  11. package/Components/Layout/TV/NavBarContainer.tsx +1 -10
  12. package/Components/Layout/TV/ScreenContainer.tsx +2 -6
  13. package/Components/Layout/TV/__tests__/__snapshots__/NavBarContainer.test.tsx.snap +7 -12
  14. package/Components/Layout/TV/__tests__/__snapshots__/ScreenContainer.test.tsx.snap +7 -12
  15. package/Components/Layout/TV/index.tsx +3 -4
  16. package/Components/Layout/TV/index.web.tsx +3 -4
  17. package/Components/LinkHandler/LinkHandler.tsx +2 -2
  18. package/Components/MasterCell/CONFIG_BUILDER_TO_REACT_COMPONENT.md +144 -0
  19. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/model.test.ts +80 -0
  20. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +187 -0
  21. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/selectors.test.ts +45 -0
  22. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/style.test.ts +49 -0
  23. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +165 -0
  24. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +405 -0
  25. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/index.ts +1 -0
  26. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/model.ts +47 -0
  27. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +170 -0
  28. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/selectors.ts +26 -0
  29. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/style.ts +29 -0
  30. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/types.ts +37 -0
  31. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +4 -10
  32. package/Components/MasterCell/DefaultComponents/Button.tsx +0 -15
  33. package/Components/MasterCell/DefaultComponents/ButtonContainerView/components/HorizontalSeparator.tsx +8 -0
  34. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +15 -0
  35. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tv.android.tsx +58 -0
  36. package/Components/MasterCell/DefaultComponents/{tv/ButtonContainerView/index.tsx → ButtonContainerView/index.tv.tsx} +3 -11
  37. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.web.ts +1 -0
  38. package/Components/MasterCell/DefaultComponents/ButtonContainerView/types.ts +40 -0
  39. package/Components/MasterCell/DefaultComponents/DataProvider/index.tsx +163 -0
  40. package/Components/MasterCell/DefaultComponents/FocusableView/index.android.tsx +2 -23
  41. package/Components/MasterCell/DefaultComponents/FocusableView/index.tsx +4 -22
  42. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +8 -2
  43. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +11 -3
  44. package/Components/MasterCell/DefaultComponents/Image/Image.web.tsx +9 -1
  45. package/Components/MasterCell/DefaultComponents/Image/hooks/useImage.ts +15 -14
  46. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +1 -2
  47. package/Components/MasterCell/DefaultComponents/PressableView.tsx +34 -0
  48. package/Components/MasterCell/DefaultComponents/SecondaryImage/hooks/__tests__/useGetImageDimensions.test.ts +7 -6
  49. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +11 -0
  50. package/Components/MasterCell/DefaultComponents/__tests__/DataProvider.test.tsx +141 -0
  51. package/Components/MasterCell/DefaultComponents/index.ts +9 -3
  52. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +135 -0
  53. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +33 -0
  54. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +22 -0
  55. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +125 -0
  56. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +16 -0
  57. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabel.ts +67 -0
  58. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +37 -0
  59. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +393 -0
  60. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/builders.test.ts +141 -0
  61. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +343 -0
  62. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/helpers.ts +105 -0
  63. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +122 -0
  64. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +118 -0
  65. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +238 -0
  66. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Asset.ts +4 -18
  67. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Button.ts +24 -73
  68. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TextLabelsContainer.ts +37 -18
  69. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TvActionButton.tsx +27 -0
  70. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/index.test.ts +89 -0
  71. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/renderedTree.test.tsx +231 -0
  72. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +47 -48
  73. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +115 -29
  74. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +101 -144
  75. package/Components/MasterCell/MappingFunctions/index.js +3 -2
  76. package/Components/MasterCell/README.md +4 -0
  77. package/Components/MasterCell/__tests__/__snapshots__/dataAdapter.test.js.snap +24 -0
  78. package/Components/MasterCell/__tests__/configInflater.test.js +1 -0
  79. package/Components/MasterCell/__tests__/elementMapper.test.js +46 -0
  80. package/Components/MasterCell/dataAdapter.ts +4 -1
  81. package/Components/MasterCell/elementMapper.tsx +52 -7
  82. package/Components/MasterCell/utils/__tests__/cloneChildrenWithIds.test.tsx +43 -0
  83. package/Components/MasterCell/utils/__tests__/useFilterChildren.test.tsx +80 -0
  84. package/Components/MasterCell/utils/index.ts +85 -15
  85. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +2 -2
  86. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +17 -18
  87. package/Components/OfflineHandler/__tests__/index.test.tsx +27 -18
  88. package/Components/PlayerContainer/PlayerContainer.tsx +14 -13
  89. package/Components/River/ComponentsMap/ComponentsMap.tsx +6 -19
  90. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +1 -1
  91. package/Components/River/RefreshControl.tsx +19 -88
  92. package/Components/River/River.tsx +9 -82
  93. package/Components/River/TV/River.tsx +31 -14
  94. package/Components/River/TV/index.tsx +8 -4
  95. package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +30 -0
  96. package/Components/River/TV/utils/index.ts +4 -0
  97. package/Components/River/TV/withFocusableGroupForContent.tsx +71 -0
  98. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +1 -0
  99. package/Components/River/__tests__/componentsMap.test.js +38 -0
  100. package/Components/River/hooks/__tests__/usePullToRefresh.test.ts +132 -0
  101. package/Components/River/hooks/index.ts +1 -0
  102. package/Components/River/hooks/usePullToRefresh.ts +51 -0
  103. package/Components/Screen/TV/index.web.tsx +4 -2
  104. package/Components/Screen/__tests__/Screen.test.tsx +65 -42
  105. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +68 -44
  106. package/Components/Screen/hooks.ts +2 -3
  107. package/Components/Screen/index.tsx +2 -3
  108. package/Components/Screen/orientationHandler.ts +3 -3
  109. package/Components/ScreenResolver/index.tsx +9 -5
  110. package/Components/ScreenRevealManager/ScreenRevealManager.ts +40 -8
  111. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +86 -69
  112. package/Components/Tabs/TabContent.tsx +7 -4
  113. package/Components/TopCutoffOverlay/__tests__/TopCutoffOverlay.test.tsx +201 -0
  114. package/Components/TopCutoffOverlay/hooks/__tests__/useMarginTop.test.ts +130 -0
  115. package/Components/TopCutoffOverlay/hooks/index.ts +1 -0
  116. package/Components/TopCutoffOverlay/hooks/useMarginTop.ts +59 -0
  117. package/Components/TopCutoffOverlay/index.tsx +55 -0
  118. package/Components/Transitioner/index.js +3 -3
  119. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +5 -5
  120. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +15 -7
  121. package/Components/VideoModal/utils.ts +12 -9
  122. package/Components/Viewport/ViewportAware/__tests__/viewportAware.test.js +0 -2
  123. package/Components/Viewport/ViewportAware/index.tsx +16 -7
  124. package/Components/Viewport/ViewportEvents/__tests__/viewportEvents.test.js +1 -1
  125. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +4 -6
  126. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  127. package/Components/default-cell-renderer/viewTrees/mobile/index.ts +0 -3
  128. package/Contexts/ScreenContext/index.tsx +25 -18
  129. package/Contexts/ScreenTrackedViewPositionsContext/__tests__/index.test.tsx +1 -1
  130. package/Decorators/Analytics/index.tsx +6 -5
  131. package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +1 -0
  132. package/Decorators/ConfigurationWrapper/const.ts +1 -0
  133. package/Decorators/ZappPipesDataConnector/__tests__/UrlFeedResolver.test.tsx +39 -21
  134. package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
  135. package/Decorators/ZappPipesDataConnector/index.tsx +2 -2
  136. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
  137. package/Decorators/ZappPipesDataConnector/resolvers/UrlFeedResolver.tsx +18 -7
  138. package/Helpers/DataSourceHelper/__tests__/itemLimitForData.test.ts +80 -0
  139. package/Helpers/DataSourceHelper/index.ts +19 -0
  140. package/package.json +5 -5
  141. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/index.android.tsx +0 -135
  142. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/types.ts +0 -25
  143. package/Components/River/TV/withTVEventHandler.tsx +0 -36
  144. package/Helpers/DataSourceHelper/index.js +0 -19
@@ -0,0 +1,343 @@
1
+ import { MobileActionButtons } from "..";
2
+
3
+ describe("MobileActionButtons", () => {
4
+ const configuration = {
5
+ mobile_buttons_container_buttons_enabled: true,
6
+ mobile_buttons_container_position: "over_image",
7
+ mobile_buttons_container_align: "left",
8
+ mobile_buttons_container_margin_top: 0,
9
+ mobile_buttons_container_margin_right: 0,
10
+ mobile_buttons_container_margin_bottom: 0,
11
+ mobile_buttons_container_margin_left: 0,
12
+ mobile_buttons_container_stacking: "horizontal",
13
+ mobile_buttons_container_horizontal_gutter: 8,
14
+ mobile_buttons_container_vertical_gutter: 8,
15
+ mobile_buttons_container_independent_styles: true,
16
+ mobile_buttons_container_over_image_position: "top_right",
17
+ mobile_button_1_button_enabled: true,
18
+ mobile_button_1_assign_action: "navigation_action",
19
+ mobile_button_1_display_mode: "dynamic",
20
+ mobile_button_1_contents_alignment: "center",
21
+ mobile_button_1_background_color: "rgba(1,1,1,1)",
22
+ mobile_button_1_border_color: "rgba(0,0,0,0)",
23
+ mobile_button_1_border_size: 0,
24
+ mobile_button_1_corner_radius: 8,
25
+ mobile_button_1_padding_top: 10,
26
+ mobile_button_1_padding_right: 10,
27
+ mobile_button_1_padding_bottom: 10,
28
+ mobile_button_1_padding_left: 10,
29
+ mobile_button_1_asset_width: 24,
30
+ mobile_button_1_asset_height: 24,
31
+ mobile_button_1_asset_margin_top: 0,
32
+ mobile_button_1_asset_margin_right: 0,
33
+ mobile_button_1_asset_margin_bottom: 0,
34
+ mobile_button_1_asset_margin_left: 0,
35
+ mobile_button_1_asset_enabled: true,
36
+ mobile_button_1_label_enabled: true,
37
+ mobile_button_1_font_color: "rgba(255,255,255,1)",
38
+ mobile_button_1_focused_font_color: "rgba(255,0,0,1)",
39
+ };
40
+
41
+ const value = (key) => configuration[key];
42
+
43
+ it("renders over-image buttons only for over_image placement", () => {
44
+ const result = MobileActionButtons({
45
+ value,
46
+ configuration,
47
+ placement: "over_image",
48
+ });
49
+
50
+ expect(result).toBeTruthy();
51
+ expect(result.type).toBe("ButtonContainerView");
52
+ expect(result.style.position).toBe("absolute");
53
+ expect(result.style.top).toBe(0);
54
+ expect(result.style.right).toBe(0);
55
+
56
+ // Content view
57
+ expect(result.additionalProps.contentStyle.flexDirection).toBe("row");
58
+
59
+ // First button wrapped in DataProvider
60
+ const dataProvider = result.elements[0];
61
+ expect(dataProvider.type).toBe("DataProvider");
62
+
63
+ expect(dataProvider.data).toEqual([
64
+ {
65
+ func: "identity",
66
+ args: [],
67
+ propName: "entry",
68
+ },
69
+ ]);
70
+
71
+ const button = dataProvider.elements[0];
72
+ expect(button.type).toBe("MobileActionButton");
73
+
74
+ expect(button.elements).toEqual(
75
+ expect.arrayContaining([
76
+ expect.objectContaining({ type: "ReactComponent" }),
77
+ expect.objectContaining({ type: "View" }),
78
+ ])
79
+ );
80
+ });
81
+
82
+ it("does not render label placement when position is over_image", () => {
83
+ const result = MobileActionButtons({
84
+ value,
85
+ configuration,
86
+ placement: "labels",
87
+ });
88
+
89
+ expect(result).toBeNull();
90
+ });
91
+
92
+ it("renders button 1 when only button 1 is enabled", () => {
93
+ const configurationWithSingleButton = {
94
+ ...configuration,
95
+ mobile_button_1_button_enabled: true,
96
+ mobile_button_2_button_enabled: false,
97
+ mobile_button_3_button_enabled: false,
98
+ };
99
+
100
+ const singleButtonValue = (key) => configurationWithSingleButton[key];
101
+
102
+ const result = MobileActionButtons({
103
+ value: singleButtonValue,
104
+ configuration: configurationWithSingleButton,
105
+ placement: "over_image",
106
+ });
107
+
108
+ expect(result?.elements?.[0]?.elements).toHaveLength(1);
109
+
110
+ expect(result?.elements?.[0]?.type).toBe("DataProvider");
111
+
112
+ expect(result?.elements?.[0]?.elements?.[0]?.type).toBe(
113
+ "MobileActionButton"
114
+ );
115
+ });
116
+
117
+ it("renders button 2 when only button 2 is enabled", () => {
118
+ const configurationWithSingleButton = {
119
+ ...configuration,
120
+ mobile_button_1_button_enabled: false,
121
+ mobile_button_2_button_enabled: true,
122
+ mobile_button_2_asset_enabled: true,
123
+ mobile_button_3_button_enabled: false,
124
+ };
125
+
126
+ const singleButtonValue = (key) => configurationWithSingleButton[key];
127
+
128
+ const result = MobileActionButtons({
129
+ value: singleButtonValue,
130
+ configuration: configurationWithSingleButton,
131
+ placement: "over_image",
132
+ });
133
+
134
+ expect(result?.elements?.[0]?.elements).toHaveLength(1);
135
+
136
+ expect(result?.elements?.[0]?.elements?.[0]?.type).toBe(
137
+ "MobileActionButton"
138
+ );
139
+
140
+ expect(
141
+ result?.elements?.[0]?.elements?.[0]?.elements?.[0]?.additionalProps
142
+ ?.action?.identifier
143
+ ).toBe(configurationWithSingleButton.mobile_button_2_assign_action);
144
+ });
145
+
146
+ it("renders button 3 when only button 3 is enabled", () => {
147
+ const configurationWithSingleButton = {
148
+ ...configuration,
149
+ mobile_button_1_button_enabled: false,
150
+ mobile_button_2_button_enabled: false,
151
+ mobile_button_3_button_enabled: true,
152
+ mobile_button_3_assign_action: "local_storage_favourites_action",
153
+ mobile_button_3_asset_enabled: true,
154
+ };
155
+
156
+ const singleButtonValue = (key) => configurationWithSingleButton[key];
157
+
158
+ const result = MobileActionButtons({
159
+ value: singleButtonValue,
160
+ configuration: configurationWithSingleButton,
161
+ placement: "over_image",
162
+ });
163
+
164
+ expect(result?.elements?.[0]?.elements).toHaveLength(1);
165
+
166
+ expect(result?.elements?.[0]?.elements?.[0]?.type).toBe(
167
+ "MobileActionButton"
168
+ );
169
+
170
+ expect(
171
+ result?.elements?.[0]?.elements?.[0]?.additionalProps?.action?.identifier
172
+ ).toBe(configurationWithSingleButton.mobile_button_3_assign_action);
173
+ });
174
+
175
+ it("renders sparse slots contiguously while preserving slot-specific actions", () => {
176
+ const configurationWithSparseButtons = {
177
+ ...configuration,
178
+ mobile_button_1_button_enabled: true,
179
+ mobile_button_1_assign_action: "action_1",
180
+ mobile_button_2_button_enabled: false,
181
+ mobile_button_3_button_enabled: true,
182
+ mobile_button_3_assign_action: "action_3",
183
+ mobile_button_3_asset_enabled: true,
184
+ };
185
+
186
+ const sparseValue = (key) => configurationWithSparseButtons[key];
187
+
188
+ const result = MobileActionButtons({
189
+ value: sparseValue,
190
+ configuration: configurationWithSparseButtons,
191
+ placement: "over_image",
192
+ });
193
+
194
+ // Content view has 2 DataProvider children (one per enabled button)
195
+ expect(result?.elements).toHaveLength(2);
196
+
197
+ expect(
198
+ result?.elements?.[0]?.elements?.[0]?.additionalProps?.action?.identifier
199
+ ).toBe("action_1");
200
+
201
+ expect(
202
+ result?.elements?.[1]?.elements?.[0]?.additionalProps?.action?.identifier
203
+ ).toBe("action_3");
204
+
205
+ expect(result?.elements?.[0]?.elements?.[0]?.additionalProps?.testID).toBe(
206
+ "mobile_action_button_1"
207
+ );
208
+
209
+ expect(result?.elements?.[1]?.elements?.[0]?.additionalProps?.testID).toBe(
210
+ "mobile_action_button_2"
211
+ );
212
+ });
213
+
214
+ it("maps shared semantic layout data into content styles", () => {
215
+ const configurationWithLayout = {
216
+ ...configuration,
217
+ mobile_buttons_container_align: "middle",
218
+ mobile_buttons_container_margin_top: 5,
219
+ mobile_buttons_container_margin_right: 6,
220
+ mobile_buttons_container_margin_bottom: 7,
221
+ mobile_buttons_container_margin_left: 8,
222
+ mobile_buttons_container_stacking: "vertical",
223
+ };
224
+
225
+ const layoutValue = (key) => configurationWithLayout[key];
226
+
227
+ const result = MobileActionButtons({
228
+ value: layoutValue,
229
+ configuration: configurationWithLayout,
230
+ placement: "labels",
231
+ });
232
+
233
+ expect(result).toBeNull();
234
+
235
+ const overImageResult = MobileActionButtons({
236
+ value: layoutValue,
237
+ configuration: configurationWithLayout,
238
+ placement: "over_image",
239
+ });
240
+
241
+ expect(overImageResult?.additionalProps?.contentStyle).toMatchObject({
242
+ flexDirection: "column",
243
+ alignItems: "center",
244
+ marginTop: 5,
245
+ marginRight: 6,
246
+ marginBottom: 7,
247
+ marginLeft: 8,
248
+ });
249
+
250
+ expect(
251
+ overImageResult?.additionalProps?.contentStyle?.alignSelf
252
+ ).toBeUndefined();
253
+ });
254
+
255
+ describe("button content visibility", () => {
256
+ const makeConfig = (overrides) => ({
257
+ ...configuration,
258
+ ...overrides,
259
+ });
260
+
261
+ it("renders only an asset when only asset is enabled", () => {
262
+ const config = makeConfig({
263
+ mobile_button_1_asset_enabled: true,
264
+ mobile_button_1_label_enabled: false,
265
+ });
266
+
267
+ const result = MobileActionButtons({
268
+ value: (key) => config[key],
269
+ configuration: config,
270
+ placement: "over_image",
271
+ });
272
+
273
+ const buttonElements =
274
+ result?.elements?.[0]?.elements?.[0]?.elements ?? [];
275
+
276
+ expect(buttonElements.some((el) => el.type === "ReactComponent")).toBe(
277
+ true
278
+ );
279
+
280
+ expect(buttonElements.some((el) => el.type === "View")).toBe(false);
281
+ });
282
+
283
+ it("renders only a label container when only label is enabled", () => {
284
+ const config = makeConfig({
285
+ mobile_button_1_asset_enabled: false,
286
+ mobile_button_1_label_enabled: true,
287
+ });
288
+
289
+ const result = MobileActionButtons({
290
+ value: (key) => config[key],
291
+ configuration: config,
292
+ placement: "over_image",
293
+ });
294
+
295
+ const buttonElements =
296
+ result?.elements?.[0]?.elements?.[0]?.elements ?? [];
297
+
298
+ expect(buttonElements.some((el) => el.type === "ReactComponent")).toBe(
299
+ false
300
+ );
301
+
302
+ expect(buttonElements.some((el) => el.type === "View")).toBe(true);
303
+ });
304
+
305
+ it("renders both asset and label container when both are enabled", () => {
306
+ const config = makeConfig({
307
+ mobile_button_1_asset_enabled: true,
308
+ mobile_button_1_label_enabled: true,
309
+ });
310
+
311
+ const result = MobileActionButtons({
312
+ value: (key) => config[key],
313
+ configuration: config,
314
+ placement: "over_image",
315
+ });
316
+
317
+ const buttonElements =
318
+ result?.elements?.[0]?.elements?.[0]?.elements ?? [];
319
+
320
+ expect(buttonElements.some((el) => el.type === "ReactComponent")).toBe(
321
+ true
322
+ );
323
+
324
+ expect(buttonElements.some((el) => el.type === "View")).toBe(true);
325
+ });
326
+
327
+ it("omits the button entirely when neither asset nor label is enabled", () => {
328
+ const config = makeConfig({
329
+ mobile_button_1_asset_enabled: false,
330
+ mobile_button_1_label_enabled: false,
331
+ });
332
+
333
+ const result = MobileActionButtons({
334
+ value: (key) => config[key],
335
+ configuration: config,
336
+ placement: "over_image",
337
+ });
338
+
339
+ // Button() returns null so compact filters the DataProvider wrapper
340
+ expect(result?.elements).toHaveLength(0);
341
+ });
342
+ });
343
+ });
@@ -0,0 +1,105 @@
1
+ import { Platform } from "react-native";
2
+
3
+ export function isStringAsset(asset) {
4
+ return typeof asset === "string" || Array.isArray(asset);
5
+ }
6
+
7
+ export function getAssetValue(asset, flavour, fallbackAsset = null) {
8
+ if (!asset) {
9
+ return fallbackAsset;
10
+ }
11
+
12
+ if (typeof asset === "string") {
13
+ return asset;
14
+ }
15
+
16
+ if (Array.isArray(asset)) {
17
+ const flavourIndex = Number(String(flavour || "").replace("flavour_", ""));
18
+
19
+ if (!Number.isNaN(flavourIndex) && flavourIndex > 0) {
20
+ return asset[flavourIndex - 1];
21
+ }
22
+
23
+ return asset[0];
24
+ }
25
+
26
+ return asset.src || fallbackAsset;
27
+ }
28
+
29
+ export function getContentDirection(alignment = "left") {
30
+ switch (alignment) {
31
+ case "right":
32
+ return "row-reverse";
33
+ case "above":
34
+ return "column";
35
+ case "below":
36
+ return "column-reverse";
37
+ case "left":
38
+ default:
39
+ return "row";
40
+ }
41
+ }
42
+
43
+ export function getContentsAlignment(alignment = "center") {
44
+ switch (alignment) {
45
+ case "left":
46
+ return "flex-start";
47
+ case "right":
48
+ return "flex-end";
49
+ case "center":
50
+ default:
51
+ return "center";
52
+ }
53
+ }
54
+
55
+ export function resolveLabelText(label) {
56
+ if (typeof label === "string") {
57
+ return label;
58
+ }
59
+
60
+ return label?.label_1 || "";
61
+ }
62
+
63
+ export function resolveIsActive(actionState, fallbackSelected = false) {
64
+ if (actionState == null) {
65
+ return fallbackSelected;
66
+ }
67
+
68
+ return Boolean(
69
+ actionState?.active ??
70
+ actionState?.isActive ??
71
+ actionState?.selected ??
72
+ actionState?.isSelected ??
73
+ fallbackSelected
74
+ );
75
+ }
76
+
77
+ export function buildLegacySelection(item, actionContext) {
78
+ const defaultIsSelected = (actionContext?.state || []).includes(item);
79
+
80
+ return actionContext?.masterCell?.isSelected
81
+ ? actionContext?.masterCell?.isSelected(item)
82
+ : defaultIsSelected;
83
+ }
84
+
85
+ export function buildLabelStyle(label) {
86
+ if (!label?.enabled) {
87
+ return {};
88
+ }
89
+
90
+ const platformFontFamily =
91
+ Platform.OS === "ios"
92
+ ? label?.iosFontFamily || label?.normalStyle?.fontFamily
93
+ : label?.androidFontFamily || label?.normalStyle?.fontFamily;
94
+
95
+ const platformLetterSpacing =
96
+ Platform.OS === "ios"
97
+ ? label?.iosLetterSpacing
98
+ : label?.androidLetterSpacing;
99
+
100
+ return {
101
+ ...label?.normalStyle,
102
+ fontFamily: platformFontFamily,
103
+ letterSpacing: platformLetterSpacing,
104
+ };
105
+ }
@@ -0,0 +1,122 @@
1
+ import { compact } from "@applicaster/zapp-react-native-utils/cellUtils";
2
+ import {
3
+ insertButtonsBetweenLabels,
4
+ insertButtonsBetweenLabelContainers,
5
+ mobileOverImagePositionStyles,
6
+ } from "./utils";
7
+ import { Button } from "./Button";
8
+ import { buildActionButtonsModel } from "../../ActionButtonsCore/model";
9
+
10
+ const CONTAINER_PREFIX = "mobile_buttons_container";
11
+ const BUTTON_PREFIX = "mobile_button";
12
+
13
+ type MobileActionButtonsProps = {
14
+ value: (key: string) => unknown;
15
+ configuration: Record<string, unknown>;
16
+ placement: "labels" | "over_image"; // Indicates where the component was placed, to apply specific display rules
17
+ };
18
+
19
+ export const MobileActionButtons = ({
20
+ value,
21
+ configuration,
22
+ placement,
23
+ }: MobileActionButtonsProps) => {
24
+ const position = value(`${CONTAINER_PREFIX}_position`);
25
+
26
+ if (placement === "labels" && position === "over_image") {
27
+ return null;
28
+ }
29
+
30
+ if (placement === "over_image" && position !== "over_image") {
31
+ return null;
32
+ }
33
+
34
+ const model = buildActionButtonsModel({
35
+ configuration,
36
+ value,
37
+ containerPrefix: CONTAINER_PREFIX,
38
+ buttonPrefix: BUTTON_PREFIX,
39
+ });
40
+
41
+ if (!model) {
42
+ return null;
43
+ }
44
+
45
+ const style = {
46
+ alignItems: "center",
47
+ ...(placement === "over_image"
48
+ ? {
49
+ position: "absolute",
50
+ zIndex: 10,
51
+ top: 0,
52
+ left: 0,
53
+ right: 0,
54
+ bottom: 0,
55
+ ...mobileOverImagePositionStyles(
56
+ value(`${CONTAINER_PREFIX}_over_image_position`) as string
57
+ ),
58
+ }
59
+ : {}),
60
+ };
61
+
62
+ const contentStyle = {
63
+ flexDirection: model.container.stacking === "vertical" ? "column" : "row",
64
+ alignSelf:
65
+ placement !== "over_image" ? model.container.horizontalAlign : undefined,
66
+ alignItems: model.container.horizontalAlign,
67
+ marginTop: model.container.margins.top,
68
+ marginRight: model.container.margins.right,
69
+ marginBottom: model.container.margins.bottom,
70
+ marginLeft: model.container.margins.left,
71
+ };
72
+
73
+ const elements = compact(
74
+ model.buttons.map(({ renderIndex, specificPrefix, stylePrefix }) => {
75
+ const isNotLast = renderIndex < model.buttons.length - 1;
76
+
77
+ const {
78
+ container: { stacking, verticalGutter, horizontalGutter },
79
+ } = model;
80
+
81
+ const isVertical = stacking === "vertical";
82
+ const gutter = isVertical ? verticalGutter : horizontalGutter;
83
+
84
+ const spacingStyle = {
85
+ [isVertical ? "marginBottom" : "marginRight"]: isNotLast ? gutter : 0,
86
+ };
87
+
88
+ const button = Button({
89
+ index: renderIndex,
90
+ value,
91
+ stylePrefix,
92
+ specificPrefix,
93
+ spacingStyle,
94
+ });
95
+
96
+ if (!button) return null;
97
+
98
+ return {
99
+ type: "DataProvider",
100
+ data: [
101
+ {
102
+ func: "identity",
103
+ args: [],
104
+ propName: "entry",
105
+ },
106
+ ],
107
+ elements: [button],
108
+ };
109
+ })
110
+ );
111
+
112
+ return {
113
+ type: "ButtonContainerView",
114
+ style: style,
115
+ additionalProps: {
116
+ contentStyle,
117
+ },
118
+ elements,
119
+ };
120
+ };
121
+
122
+ export { insertButtonsBetweenLabels, insertButtonsBetweenLabelContainers };
@@ -0,0 +1,118 @@
1
+ import {
2
+ insertButtonsBetweenLabelContainers,
3
+ insertButtonsBetweenLabels,
4
+ mobileOverImagePositionStyles,
5
+ } from "..";
6
+
7
+ describe("mobile action insertion helpers", () => {
8
+ const labels = [{ name: "text_label_1" }, { name: "text_label_2" }];
9
+ const buttons = { type: "View", name: "buttons" };
10
+
11
+ it("inserts label buttons below target label", () => {
12
+ const result = insertButtonsBetweenLabels(
13
+ { mobile_buttons_container_position: "below_text_label_1" },
14
+ buttons,
15
+ labels
16
+ );
17
+
18
+ expect(result).toEqual([labels[0], buttons, labels[1]]);
19
+ });
20
+
21
+ it("returns labels unchanged when the flat-label target is unknown", () => {
22
+ const result = insertButtonsBetweenLabels(
23
+ { mobile_buttons_container_position: "unknown" },
24
+ buttons,
25
+ labels
26
+ );
27
+
28
+ expect(result).toEqual([labels[0], labels[1]]);
29
+ });
30
+
31
+ it("inserts label-container buttons below nested target", () => {
32
+ const labelContainers = [
33
+ {
34
+ elements: [{ elements: [{ name: "top_text_label_1" }] }],
35
+ },
36
+ ];
37
+
38
+ const result = insertButtonsBetweenLabelContainers(
39
+ { mobile_buttons_container_position: "top_text_label_1" },
40
+ buttons,
41
+ labelContainers
42
+ );
43
+
44
+ expect(result).toEqual([
45
+ {
46
+ elements: [{ elements: [{ name: "top_text_label_1" }, buttons] }],
47
+ },
48
+ ]);
49
+ });
50
+
51
+ it("inserts label-container buttons below direct target in two-level structure", () => {
52
+ const labelContainers = [
53
+ {
54
+ elements: [{ name: "top_text_label_1" }],
55
+ },
56
+ ];
57
+
58
+ const result = insertButtonsBetweenLabelContainers(
59
+ { mobile_buttons_container_position: "top_text_label_1" },
60
+ buttons,
61
+ labelContainers
62
+ );
63
+
64
+ expect(result).toEqual([
65
+ {
66
+ elements: [{ name: "top_text_label_1" }, buttons],
67
+ },
68
+ ]);
69
+ });
70
+
71
+ it("appends container buttons into the last container when target is unknown", () => {
72
+ const labelContainers = [
73
+ {
74
+ elements: [{ elements: [{ name: "top_text_label_1" }] }],
75
+ },
76
+ {
77
+ elements: [{ elements: [{ name: "bottom_text_label_1" }] }],
78
+ },
79
+ ];
80
+
81
+ const result = insertButtonsBetweenLabelContainers(
82
+ { mobile_buttons_container_position: "unknown" },
83
+ buttons,
84
+ labelContainers
85
+ );
86
+
87
+ expect(result).toEqual([
88
+ labelContainers[0],
89
+ {
90
+ elements: [...labelContainers[1].elements, buttons],
91
+ },
92
+ ]);
93
+ });
94
+ });
95
+
96
+ describe("mobileOverImagePositionStyles", () => {
97
+ it("maps bottom left anchor to absolute positioning", () => {
98
+ const result = mobileOverImagePositionStyles("bottom_left");
99
+
100
+ expect(result).toEqual({
101
+ justifyContent: "flex-end",
102
+ alignItems: "flex-start",
103
+ });
104
+ });
105
+
106
+ it("maps center anchor to absolute center positioning", () => {
107
+ const result = mobileOverImagePositionStyles("center");
108
+
109
+ expect(result).toEqual({
110
+ justifyContent: "center",
111
+ alignItems: "center",
112
+ top: 0,
113
+ left: 0,
114
+ right: 0,
115
+ bottom: 0,
116
+ });
117
+ });
118
+ });