@applicaster/zapp-react-native-ui-components 15.1.4 → 16.0.0-alpha.1042942411

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 (202) hide show
  1. package/Components/BackgroundImage/index.ts +1 -8
  2. package/Components/BaseFocusable/index.ios.ts +12 -2
  3. package/Components/Cell/FocusableWrapper.tsx +3 -0
  4. package/Components/Cell/TvOSCellComponent.tsx +6 -3
  5. package/Components/CellRendererResolver/index.ts +1 -1
  6. package/Components/Focusable/Focusable.tsx +4 -2
  7. package/Components/Focusable/FocusableTvOS.tsx +18 -1
  8. package/Components/Focusable/__tests__/__snapshots__/FocusableTvOS.test.tsx.snap +1 -0
  9. package/Components/FocusableGroup/FocusableTvOS.tsx +30 -1
  10. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +39 -0
  11. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  12. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  13. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  14. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  15. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +124 -0
  16. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  17. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  18. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  19. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +72 -0
  20. package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
  21. package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
  22. package/Components/GeneralContentScreen/index.ts +2 -0
  23. package/Components/GeneralContentScreen/utils/__tests__/useCurationAPI.test.js +1 -1
  24. package/Components/HandlePlayable/HandlePlayable.tsx +13 -8
  25. package/Components/Layout/TV/NavBarContainer.tsx +1 -10
  26. package/Components/Layout/TV/ScreenContainer.tsx +2 -6
  27. package/Components/Layout/TV/__tests__/__snapshots__/NavBarContainer.test.tsx.snap +7 -12
  28. package/Components/Layout/TV/__tests__/__snapshots__/ScreenContainer.test.tsx.snap +7 -12
  29. package/Components/Layout/TV/index.tsx +3 -4
  30. package/Components/Layout/TV/index.web.tsx +3 -4
  31. package/Components/LinkHandler/LinkHandler.tsx +2 -2
  32. package/Components/MasterCell/CONFIG_BUILDER_TO_REACT_COMPONENT.md +144 -0
  33. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +27 -48
  34. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/model.test.ts +80 -0
  35. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +213 -0
  36. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/selectors.test.ts +45 -0
  37. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/style.test.ts +49 -0
  38. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +165 -0
  39. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +405 -0
  40. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/index.ts +1 -0
  41. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/model.ts +47 -0
  42. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +198 -0
  43. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/selectors.ts +26 -0
  44. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/style.ts +29 -0
  45. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/types.ts +37 -0
  46. package/Components/MasterCell/DefaultComponents/BorderContainerView/__tests__/index.test.tsx +217 -46
  47. package/Components/MasterCell/DefaultComponents/BorderContainerView/index.tsx +50 -119
  48. package/Components/MasterCell/DefaultComponents/Button.tsx +50 -58
  49. package/Components/MasterCell/DefaultComponents/ButtonContainerView/components/HorizontalSeparator.tsx +8 -0
  50. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.android.tv.tsx +58 -0
  51. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +15 -0
  52. package/Components/MasterCell/DefaultComponents/{tv/ButtonContainerView/index.tsx → ButtonContainerView/index.tv.tsx} +3 -11
  53. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.web.ts +1 -0
  54. package/Components/MasterCell/DefaultComponents/ButtonContainerView/types.ts +40 -0
  55. package/Components/MasterCell/DefaultComponents/CellBadge/__tests__/__snapshots__/index.test.tsx.snap +18 -0
  56. package/Components/MasterCell/DefaultComponents/CellBadge/__tests__/index.test.tsx +90 -0
  57. package/Components/MasterCell/DefaultComponents/CellBadge/index.tsx +13 -14
  58. package/Components/MasterCell/DefaultComponents/DataProvider/index.tsx +163 -0
  59. package/Components/MasterCell/DefaultComponents/FocusableView/index.android.tsx +2 -23
  60. package/Components/MasterCell/DefaultComponents/FocusableView/index.tsx +4 -22
  61. package/Components/MasterCell/DefaultComponents/Image/Image.android.tsx +8 -2
  62. package/Components/MasterCell/DefaultComponents/Image/Image.ios.tsx +11 -3
  63. package/Components/MasterCell/DefaultComponents/Image/Image.web.tsx +9 -1
  64. package/Components/MasterCell/DefaultComponents/Image/hooks/useImage.ts +15 -14
  65. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  66. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +1 -2
  67. package/Components/MasterCell/DefaultComponents/PressableView.tsx +54 -0
  68. package/Components/MasterCell/DefaultComponents/SecondaryImage/Image.tsx +46 -50
  69. package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/__snapshots__/Image.test.tsx.snap +47 -39
  70. package/Components/MasterCell/DefaultComponents/SecondaryImage/hooks/__tests__/useGetImageDimensions.test.ts +7 -6
  71. package/Components/MasterCell/DefaultComponents/SecondaryImage/utils.ts +19 -6
  72. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +15 -0
  73. package/Components/MasterCell/DefaultComponents/Text/index.tsx +45 -15
  74. package/Components/MasterCell/DefaultComponents/UIKitAdapters/Badge.tsx +49 -0
  75. package/Components/MasterCell/DefaultComponents/__tests__/ActionButton.test.tsx +122 -0
  76. package/Components/MasterCell/DefaultComponents/__tests__/Button.test.tsx +186 -0
  77. package/Components/MasterCell/DefaultComponents/__tests__/DataProvider.test.tsx +141 -0
  78. package/Components/MasterCell/DefaultComponents/__tests__/DynamicBadge.test.tsx +156 -0
  79. package/Components/MasterCell/DefaultComponents/__tests__/__snapshots__/ActionButton.test.tsx.snap +60 -0
  80. package/Components/MasterCell/DefaultComponents/__tests__/text.test.tsx +196 -67
  81. package/Components/MasterCell/DefaultComponents/index.ts +13 -3
  82. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +189 -0
  83. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +33 -0
  84. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +33 -0
  85. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +159 -0
  86. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +18 -0
  87. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabel.ts +67 -0
  88. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +39 -0
  89. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +402 -0
  90. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/builders.test.ts +141 -0
  91. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +354 -0
  92. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/helpers.ts +105 -0
  93. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +129 -0
  94. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +123 -0
  95. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +238 -0
  96. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Asset.ts +4 -18
  97. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/Button.ts +24 -73
  98. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TextLabelsContainer.ts +37 -18
  99. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/TvActionButton.tsx +27 -0
  100. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/index.test.ts +89 -0
  101. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/__tests__/renderedTree.test.tsx +231 -0
  102. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/index.ts +47 -48
  103. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +115 -29
  104. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  105. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +101 -144
  106. package/Components/MasterCell/MappingFunctions/index.js +3 -2
  107. package/Components/MasterCell/README.md +4 -0
  108. package/Components/MasterCell/__tests__/__snapshots__/dataAdapter.test.js.snap +24 -0
  109. package/Components/MasterCell/__tests__/configInflater.test.js +1 -0
  110. package/Components/MasterCell/__tests__/elementMapper.test.js +46 -0
  111. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  112. package/Components/MasterCell/dataAdapter.ts +4 -1
  113. package/Components/MasterCell/elementMapper.tsx +54 -7
  114. package/Components/MasterCell/hooks/index.ts +2 -0
  115. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  116. package/Components/MasterCell/index.tsx +20 -4
  117. package/Components/MasterCell/utils/__tests__/cloneChildrenWithIds.test.tsx +43 -0
  118. package/Components/MasterCell/utils/__tests__/useFilterChildren.test.tsx +80 -0
  119. package/Components/MasterCell/utils/index.ts +82 -16
  120. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +33 -69
  121. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +25 -47
  122. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +122 -39
  123. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +671 -0
  124. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +257 -88
  125. package/Components/ModalComponent/AudioPlayer/Components/index.ts +6 -4
  126. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  127. package/Components/ModalComponent/BottomSheetModalContent.tsx +17 -10
  128. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  129. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  130. package/Components/OfflineHandler/__tests__/index.test.tsx +101 -32
  131. package/Components/OfflineHandler/hooks.ts +123 -0
  132. package/Components/OfflineHandler/index.tsx +51 -73
  133. package/Components/OfflineHandler/utils/index.ts +1 -13
  134. package/Components/PlayerContainer/PlayerContainer.tsx +6 -5
  135. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +2 -4
  136. package/Components/PreloaderWrapper/index.tsx +1 -1
  137. package/Components/River/ComponentsMap/ComponentsMap.tsx +8 -21
  138. package/Components/River/ComponentsMap/hooks/__tests__/useLoadingState.test.ts +1 -1
  139. package/Components/River/RefreshControl.tsx +19 -105
  140. package/Components/River/River.tsx +9 -82
  141. package/Components/River/TV/River.tsx +31 -14
  142. package/Components/River/TV/index.tsx +8 -4
  143. package/Components/River/TV/utils/__tests__/toStringOrEmpty.test.ts +30 -0
  144. package/Components/River/TV/utils/index.ts +4 -0
  145. package/Components/River/TV/withFocusableGroupForContent.tsx +71 -0
  146. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +4 -0
  147. package/Components/River/__tests__/componentsMap.test.js +38 -0
  148. package/Components/River/hooks/__tests__/usePullToRefresh.test.ts +132 -0
  149. package/Components/River/hooks/index.ts +1 -0
  150. package/Components/River/hooks/usePullToRefresh.ts +51 -0
  151. package/Components/Screen/TV/index.web.tsx +4 -2
  152. package/Components/Screen/__tests__/Screen.test.tsx +65 -42
  153. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +68 -44
  154. package/Components/Screen/hooks.ts +2 -3
  155. package/Components/Screen/index.tsx +2 -3
  156. package/Components/Screen/orientationHandler.ts +3 -3
  157. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  158. package/Components/ScreenResolver/index.tsx +9 -5
  159. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  160. package/Components/ScreenRevealManager/ScreenRevealManager.ts +40 -8
  161. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +86 -69
  162. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +1 -1
  163. package/Components/Tabs/TabContent.tsx +7 -4
  164. package/Components/TopCutoffOverlay/__tests__/TopCutoffOverlay.test.tsx +201 -0
  165. package/Components/TopCutoffOverlay/hooks/__tests__/useMarginTop.test.ts +130 -0
  166. package/Components/TopCutoffOverlay/hooks/index.ts +1 -0
  167. package/Components/TopCutoffOverlay/hooks/useMarginTop.ts +59 -0
  168. package/Components/TopCutoffOverlay/index.tsx +55 -0
  169. package/Components/Transitioner/index.js +3 -3
  170. package/Components/VideoModal/ModalAnimation/ModalAnimationContext.tsx +5 -5
  171. package/Components/VideoModal/hooks/__tests__/useDelayedPlayerDetails.test.ts +15 -7
  172. package/Components/VideoModal/utils.ts +12 -10
  173. package/Components/Viewport/ViewportAware/__tests__/viewportAware.test.js +0 -2
  174. package/Components/Viewport/ViewportAware/index.tsx +16 -7
  175. package/Components/Viewport/ViewportEvents/__tests__/viewportEvents.test.js +1 -1
  176. package/Components/default-cell-renderer/viewTrees/mobile/index.ts +0 -3
  177. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  178. package/Contexts/ScreenContext/index.tsx +25 -18
  179. package/Contexts/ScreenTrackedViewPositionsContext/__tests__/index.test.tsx +1 -1
  180. package/Decorators/Analytics/index.tsx +6 -5
  181. package/Decorators/ConfigurationWrapper/__tests__/__snapshots__/withConfigurationProvider.test.tsx.snap +1 -0
  182. package/Decorators/ConfigurationWrapper/const.ts +1 -0
  183. package/Decorators/ZappPipesDataConnector/__tests__/UrlFeedResolver.test.tsx +39 -21
  184. package/Decorators/ZappPipesDataConnector/__tests__/zappPipesDataConnector.test.js +1 -1
  185. package/Decorators/ZappPipesDataConnector/index.tsx +2 -2
  186. package/Decorators/ZappPipesDataConnector/resolvers/StaticFeedResolver.tsx +1 -1
  187. package/Decorators/ZappPipesDataConnector/resolvers/UrlFeedResolver.tsx +18 -7
  188. package/Helpers/DataSourceHelper/__tests__/itemLimitForData.test.ts +80 -0
  189. package/Helpers/DataSourceHelper/index.ts +19 -0
  190. package/package.json +5 -5
  191. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/index.android.tsx +0 -135
  192. package/Components/MasterCell/DefaultComponents/tv/ButtonContainerView/types.ts +0 -25
  193. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  194. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  195. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  196. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -67
  197. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  198. package/Components/River/TV/withTVEventHandler.tsx +0 -36
  199. package/Helpers/DataSourceHelper/index.js +0 -19
  200. /package/Components/BackgroundImage/{BackgroundImage.tv.android.tsx → BackgroundImage.android.tv.tsx} +0 -0
  201. /package/Components/BackgroundImage/{BackgroundImage.tv.ios.tsx → BackgroundImage.ios.tv.tsx} +0 -0
  202. /package/Components/BackgroundImage/{BackgroundImage.tv.web.tsx → BackgroundImage.tsx} +0 -0
@@ -0,0 +1,198 @@
1
+ type Label = {
2
+ name?: string;
3
+ elements?: Array<Label | unknown>;
4
+ };
5
+
6
+ type LabelContainer = {
7
+ elements?: Array<Label | unknown>;
8
+ };
9
+
10
+ type LabelExtraContainer = {
11
+ elements?: Array<LabelContainer | unknown>;
12
+ };
13
+
14
+ type InsertOptions = {
15
+ position?: string;
16
+ allowOnTop?: boolean;
17
+ appendWhenMissing?: boolean;
18
+ };
19
+
20
+ type Slot = {
21
+ name: string;
22
+ element?: unknown;
23
+ };
24
+
25
+ const hasLabelName = (value: unknown): value is Label =>
26
+ !!value &&
27
+ typeof value === "object" &&
28
+ typeof (value as Label).name === "string" &&
29
+ (value as Label).name !== "";
30
+
31
+ const withButtons = (
32
+ labelName: string | undefined,
33
+ buttons: unknown,
34
+ labels: Label[]
35
+ ) =>
36
+ labels.reduce<Array<Label | unknown>>((acc, label) => {
37
+ if (!!label?.name && labelName?.includes(label.name)) {
38
+ // handle above_*
39
+ if (labelName?.startsWith("above_")) {
40
+ return [...acc, buttons, label];
41
+ }
42
+
43
+ // handle below_* or plain exact match (default: insert after (covers below_* AND plain names))
44
+ return [...acc, label, buttons];
45
+ }
46
+
47
+ return [...acc, label];
48
+ }, []);
49
+
50
+ const withButtonsInNestedViews = (
51
+ labelName: string | undefined,
52
+ buttons: unknown,
53
+ views: Array<LabelContainer | unknown>
54
+ ) => {
55
+ const hasDirectLabels = views.some((view) => hasLabelName(view));
56
+
57
+ if (hasDirectLabels) {
58
+ return withButtons(labelName, buttons, views as Label[]);
59
+ }
60
+
61
+ return views.map((view) => {
62
+ if (
63
+ !view ||
64
+ typeof view !== "object" ||
65
+ !Array.isArray((view as LabelContainer).elements)
66
+ ) {
67
+ return view;
68
+ }
69
+
70
+ return {
71
+ ...(view as LabelContainer),
72
+ elements: withButtons(
73
+ labelName,
74
+ buttons,
75
+ (view as LabelContainer).elements || []
76
+ ),
77
+ };
78
+ });
79
+ };
80
+
81
+ const containsNestedLabel = (
82
+ labelName: string | undefined,
83
+ labelContainers: LabelExtraContainer[]
84
+ ) =>
85
+ labelContainers.some((labelContainer) =>
86
+ (labelContainer.elements || []).some((view) => {
87
+ if (hasLabelName(view)) {
88
+ return labelName?.includes(view.name) ?? false;
89
+ }
90
+
91
+ if (
92
+ !view ||
93
+ typeof view !== "object" ||
94
+ !Array.isArray((view as LabelContainer).elements)
95
+ ) {
96
+ return false;
97
+ }
98
+
99
+ return ((view as LabelContainer).elements || []).some((label) =>
100
+ hasLabelName(label) ? (labelName?.includes(label.name) ?? false) : false
101
+ );
102
+ })
103
+ );
104
+
105
+ export const insertBetweenLabels = (
106
+ { position, allowOnTop = false, appendWhenMissing = false }: InsertOptions,
107
+ buttons: unknown,
108
+ slots: Slot[] = []
109
+ ) => {
110
+ // Slots are defined by the canonical label list, so an empty (null) label
111
+ // still keeps its position — buttons placed above/below it survive.
112
+ const labels = slots
113
+ .map((slot) => slot.element)
114
+ .filter((element) => element != null);
115
+
116
+ if (buttons == null) {
117
+ return labels;
118
+ }
119
+
120
+ if (allowOnTop && position === "on_top") {
121
+ return [buttons, ...labels];
122
+ }
123
+
124
+ let inserted = false;
125
+ const result: unknown[] = [];
126
+
127
+ for (const { name, element } of slots) {
128
+ if (position === `above_${name}`) {
129
+ result.push(buttons);
130
+ inserted = true;
131
+ }
132
+
133
+ if (element != null) {
134
+ result.push(element);
135
+ }
136
+
137
+ // Insert after on `below_*` or a plain label-name match.
138
+ if (position === `below_${name}` || position === name) {
139
+ result.push(buttons);
140
+ inserted = true;
141
+ }
142
+ }
143
+
144
+ if (!inserted) {
145
+ return appendWhenMissing ? [...labels, buttons] : labels;
146
+ }
147
+
148
+ return result;
149
+ };
150
+
151
+ export const insertBetweenLabelContainers = (
152
+ { position, allowOnTop = false, appendWhenMissing = false }: InsertOptions,
153
+ buttons: unknown,
154
+ labelContainers: LabelExtraContainer[] = []
155
+ ) => {
156
+ if (buttons == null) {
157
+ return labelContainers;
158
+ }
159
+
160
+ if (labelContainers.length === 0) {
161
+ return [buttons];
162
+ }
163
+
164
+ if (allowOnTop && position === "on_top") {
165
+ return labelContainers.map((labelContainer, index) =>
166
+ index === 0
167
+ ? {
168
+ ...labelContainer,
169
+ elements: [buttons, ...(labelContainer.elements || [])],
170
+ }
171
+ : labelContainer
172
+ );
173
+ }
174
+
175
+ const hasMatchingLabel = containsNestedLabel(position, labelContainers);
176
+
177
+ const labelContainersWithButtons = labelContainers.map((labelContainer) => ({
178
+ ...labelContainer,
179
+ elements: withButtonsInNestedViews(
180
+ position,
181
+ buttons,
182
+ labelContainer.elements || []
183
+ ),
184
+ }));
185
+
186
+ if (!hasMatchingLabel && appendWhenMissing) {
187
+ return labelContainers.map((labelContainer, index) =>
188
+ index === labelContainers.length - 1
189
+ ? {
190
+ ...labelContainer,
191
+ elements: [...(labelContainer.elements || []), buttons],
192
+ }
193
+ : labelContainer
194
+ );
195
+ }
196
+
197
+ return hasMatchingLabel ? labelContainersWithButtons : labelContainers;
198
+ };
@@ -0,0 +1,26 @@
1
+ export const getButtonSlotPrefix = (buttonPrefix: string, slot: number) =>
2
+ `${buttonPrefix}_${slot}`;
3
+
4
+ export const getEnabledButtonSlots = (
5
+ configuration: Record<string, unknown>,
6
+ buttonPrefix: string,
7
+ maxButtons = 3
8
+ ): number[] =>
9
+ Array.from({ length: maxButtons }, (_, index) => index + 1).filter((slot) =>
10
+ Boolean(
11
+ configuration[`${getButtonSlotPrefix(buttonPrefix, slot)}_button_enabled`]
12
+ )
13
+ );
14
+
15
+ export const getStylePrefix = ({
16
+ slot,
17
+ independentStyles,
18
+ buttonPrefix,
19
+ }: {
20
+ slot: number;
21
+ independentStyles: boolean;
22
+ buttonPrefix: string;
23
+ }) =>
24
+ independentStyles
25
+ ? getButtonSlotPrefix(buttonPrefix, slot)
26
+ : getButtonSlotPrefix(buttonPrefix, 1);
@@ -0,0 +1,29 @@
1
+ import { mapSelfAlignment } from "@applicaster/zapp-react-native-utils/cellUtils";
2
+ import { toNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/numberUtils";
3
+
4
+ import { ActionButtonsContainerLayout } from "./types";
5
+
6
+ export const getContainerMargins = (
7
+ value: (key: string) => unknown,
8
+ prefix: string
9
+ ) => ({
10
+ top: toNumberWithDefaultZero(value(`${prefix}_margin_top`)),
11
+ right: toNumberWithDefaultZero(value(`${prefix}_margin_right`)),
12
+ bottom: toNumberWithDefaultZero(value(`${prefix}_margin_bottom`)),
13
+ left: toNumberWithDefaultZero(value(`${prefix}_margin_left`)),
14
+ });
15
+
16
+ export const buildContainerLayout = (
17
+ value: (key: string) => unknown,
18
+ prefix: string
19
+ ): ActionButtonsContainerLayout => ({
20
+ horizontalAlign: mapSelfAlignment(value(`${prefix}_align`)),
21
+ margins: getContainerMargins(value, prefix),
22
+ stacking:
23
+ value(`${prefix}_stacking`) === "vertical" ? "vertical" : "horizontal",
24
+ horizontalGutter: toNumberWithDefaultZero(
25
+ value(`${prefix}_horizontal_gutter`)
26
+ ),
27
+ verticalGutter: toNumberWithDefaultZero(value(`${prefix}_vertical_gutter`)),
28
+ independentStyles: Boolean(value(`${prefix}_independent_styles`)),
29
+ });
@@ -0,0 +1,37 @@
1
+ export type ActionButtonSlot = number;
2
+
3
+ export type ActionButtonDescriptor = {
4
+ slot: ActionButtonSlot;
5
+ renderIndex: number;
6
+ specificPrefix: string;
7
+ stylePrefix: string;
8
+ };
9
+
10
+ export type ActionButtonsContainerLayout = {
11
+ horizontalAlign: string;
12
+ margins: {
13
+ top: number;
14
+ right: number;
15
+ bottom: number;
16
+ left: number;
17
+ };
18
+ stacking: "horizontal" | "vertical";
19
+ horizontalGutter: number;
20
+ verticalGutter: number;
21
+ independentStyles: boolean;
22
+ };
23
+
24
+ export type ActionButtonsModel = {
25
+ enabledSlots: ActionButtonSlot[];
26
+ buttonsCount: number;
27
+ container: ActionButtonsContainerLayout;
28
+ buttons: ActionButtonDescriptor[];
29
+ };
30
+
31
+ export type BuildActionButtonsModelOptions = {
32
+ configuration: Record<string, unknown>;
33
+ value: (key: string) => unknown;
34
+ containerPrefix: string;
35
+ buttonPrefix: string;
36
+ maxButtons?: number;
37
+ };
@@ -1,26 +1,85 @@
1
1
  import * as React from "react";
2
- import {
3
- BorderContainerView,
4
- getBorderPadding, // Export for testing (using a double underscore prefix is a common convention)
5
- } from "../index";
6
- import { render } from "@testing-library/react-native";
7
- import { toNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/numberUtils";
8
- import { View } from "react-native";
9
-
10
- jest.mock("@applicaster/zapp-react-native-utils/numberUtils", () => ({
11
- toNumberWithDefaultZero: jest.fn((value) => Number(value) || 0),
12
- }));
2
+ import { render, waitFor } from "@testing-library/react-native";
3
+ import { StyleSheet, View } from "react-native";
4
+
5
+ import { BorderContainerView, getBorderPadding } from "../index";
6
+ import { ImageBorderContainer } from "../../ImageBorderContainer";
7
+ import { MeasurementPortalContext } from "../../../../MeasurmentsPortal/MeasurementsPortal";
8
+
9
+ const mockAddHeading = jest.fn();
13
10
 
14
11
  jest.mock(
15
12
  "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager/hooks",
16
13
  () => ({
17
14
  useAccessibilityManager: jest.fn(() => ({
18
- addHeading: jest.fn(),
15
+ addHeading: mockAddHeading,
19
16
  })),
20
17
  })
21
18
  );
22
19
 
20
+ const createMockValue = () => {
21
+ const values = {
22
+ cell_padding_top: 1,
23
+ cell_padding_left: 2,
24
+ cell_padding_right: 3,
25
+ cell_padding_bottom: 4,
26
+ image_border_size: 5,
27
+ image_focused_border_color: "red",
28
+ image_selected_border_color: "blue",
29
+ image_focused_selected_border_color: "green",
30
+ image_border_color: "black",
31
+ image_corner_radius: 10,
32
+ image_border_position: "outside",
33
+ image_border_padding_top: 6,
34
+ image_border_padding_right: 7,
35
+ image_border_padding_bottom: 8,
36
+ image_border_padding_left: 9,
37
+ image_margin_top: 11,
38
+ image_margin_left: 12,
39
+ image_margin_right: 13,
40
+ image_margin_bottom: 14,
41
+ };
42
+
43
+ return (key) => values[key] || 0;
44
+ };
45
+
46
+ const renderBorderContainer = (
47
+ overrideProps: Partial<any> = {},
48
+ measurementValue: React.ContextType<typeof MeasurementPortalContext> = null
49
+ ) => {
50
+ const props = {
51
+ style: {
52
+ borderWidth: 4,
53
+ borderColor: "red",
54
+ borderRadius: 8,
55
+ backgroundColor: "navy",
56
+ },
57
+ testID: "border-container",
58
+ borderPosition: "outside" as const,
59
+ borderPaddingTop: 1,
60
+ borderPaddingRight: 2,
61
+ borderPaddingBottom: 3,
62
+ borderPaddingLeft: 4,
63
+ hasFocusableInside: jest.fn(() => false),
64
+ entry: { id: "test-entry", title: "Test title" } as ZappEntry,
65
+ state: "focused" as CellState,
66
+ hasTextLabels: false,
67
+ children: <View testID="child" />,
68
+ ...overrideProps,
69
+ };
70
+
71
+ return render(
72
+ <MeasurementPortalContext.Provider value={measurementValue}>
73
+ <BorderContainerView {...props} />
74
+ </MeasurementPortalContext.Provider>
75
+ );
76
+ };
77
+
23
78
  describe("BorderContainerView", () => {
79
+ beforeEach(() => {
80
+ jest.clearAllMocks();
81
+ });
82
+
24
83
  describe("getBorderPadding", () => {
25
84
  it("returns 0 for inside", () => {
26
85
  expect(getBorderPadding("inside", 10)).toBe(0);
@@ -35,47 +94,159 @@ describe("BorderContainerView", () => {
35
94
  });
36
95
 
37
96
  it("returns borderWidth for invalid position", () => {
38
- // @ts-ignore
97
+ // @ts-ignore preserve current fallback semantics
39
98
  expect(getBorderPadding("other_value", 10)).toBe(10);
40
99
  });
41
100
  });
42
101
 
43
- it("Border component renders null if no borderPosition", () => {
44
- const style = { borderWidth: 5, borderRadius: 10, borderColor: "red" };
45
-
46
- const padding = {
47
- paddingTop: 2,
48
- paddingRight: 3,
49
- paddingBottom: 4,
50
- paddingLeft: 5,
51
- };
52
-
53
- const borderPosition = null;
54
- const mockEntry = { id: "test-entry" } as ZappEntry;
55
- const mockHasFocusableInside = jest.fn(() => false);
56
-
57
- const { queryByTestId } = render(
58
- <BorderContainerView
59
- style={style}
60
- testID="border-container"
61
- borderPosition={borderPosition}
62
- borderPaddingTop={toNumberWithDefaultZero(padding.paddingTop)}
63
- borderPaddingRight={toNumberWithDefaultZero(padding.paddingRight)}
64
- borderPaddingBottom={toNumberWithDefaultZero(padding.paddingBottom)}
65
- borderPaddingLeft={toNumberWithDefaultZero(padding.paddingLeft)}
66
- hasFocusableInside={mockHasFocusableInside}
67
- entry={mockEntry}
68
- state="focused"
69
- hasTextLabels={false}
70
- >
71
- <View testID="child" />
72
- </BorderContainerView>
102
+ it("extracts explicit border props from flattened style while preserving root style", () => {
103
+ const style = [
104
+ { backgroundColor: "purple", shadowOpacity: 0.6 },
105
+ { borderWidth: "6", borderColor: "orange", borderRadius: 12 },
106
+ ];
107
+
108
+ const { getByTestId } = renderBorderContainer({
109
+ style,
110
+ borderPaddingTop: 1,
111
+ borderPaddingRight: 2,
112
+ borderPaddingBottom: 3,
113
+ borderPaddingLeft: 4,
114
+ });
115
+
116
+ const root = getByTestId("border-container");
117
+ const overlay = root.children[0] as any;
118
+ const overlayHost = overlay.children[0] as any;
119
+ const child = root.children[1] as any;
120
+
121
+ expect(child.props.testID).toBe("child");
122
+
123
+ const flattenedRootStyle = StyleSheet.flatten(root.props.style);
124
+ expect(flattenedRootStyle.backgroundColor).toBe("purple");
125
+ expect(flattenedRootStyle.shadowOpacity).toBe(0.6);
126
+ expect(flattenedRootStyle.borderRadius).toBe(12);
127
+
128
+ expect(overlay.props.borderWidth).toBe(6);
129
+ expect(overlay.props.borderColor).toBe("orange");
130
+ expect(overlay.props.borderRadius).toBe(12);
131
+
132
+ const flattenedOverlayStyle = StyleSheet.flatten(overlayHost.props.style);
133
+ expect(flattenedOverlayStyle.borderWidth).toBe(6);
134
+ expect(flattenedOverlayStyle.borderColor).toBe("orange");
135
+ expect(flattenedOverlayStyle.top).toBe(-7);
136
+ expect(flattenedOverlayStyle.right).toBe(-8);
137
+ expect(flattenedOverlayStyle.bottom).toBe(-9);
138
+ expect(flattenedOverlayStyle.left).toBe(-10);
139
+ expect(flattenedOverlayStyle.borderTopLeftRadius).toBe(19);
140
+ expect(flattenedOverlayStyle.borderTopRightRadius).toBe(19);
141
+ expect(flattenedOverlayStyle.borderBottomLeftRadius).toBe(21);
142
+ expect(flattenedOverlayStyle.borderBottomRightRadius).toBe(20);
143
+ });
144
+
145
+ it("renders no overlay when borderPosition is missing", () => {
146
+ const { getByTestId } = renderBorderContainer({
147
+ borderPosition: null,
148
+ });
149
+
150
+ const root = getByTestId("border-container");
151
+
152
+ expect(root.children).toHaveLength(2);
153
+ expect((root.children[0] as any).children).toHaveLength(0);
154
+ expect((root.children[1] as any).props.testID).toBe("child");
155
+ });
156
+
157
+ it("preserves root pointerEvents defaults and caller overrides", () => {
158
+ const defaultRender = renderBorderContainer();
159
+
160
+ expect(
161
+ defaultRender.getByTestId("border-container").props.pointerEvents
162
+ ).toBe("box-none");
163
+
164
+ const overrideRender = renderBorderContainer({
165
+ pointerEvents: "none",
166
+ testID: "override-border-container",
167
+ });
168
+
169
+ expect(
170
+ overrideRender.getByTestId("override-border-container").props
171
+ .pointerEvents
172
+ ).toBe("none");
173
+ });
174
+
175
+ it("keeps the heading announcement logic in the QuickBrick wrapper", async () => {
176
+ renderBorderContainer({
177
+ entry: { id: "entry-1", title: "Hero title" } as ZappEntry,
178
+ state: "focused",
179
+ hasTextLabels: false,
180
+ hasFocusableInside: jest.fn(() => false),
181
+ });
182
+
183
+ await waitFor(() => {
184
+ expect(mockAddHeading).toHaveBeenCalledWith("Hero title");
185
+ });
186
+ });
187
+
188
+ it("suppresses the heading announcement while measurement is in progress", async () => {
189
+ renderBorderContainer(
190
+ {
191
+ entry: { id: "entry-1", title: "Hero title" } as ZappEntry,
192
+ },
193
+ { measuringInProgress: true }
73
194
  );
74
195
 
75
- const children = queryByTestId("border-container").children;
196
+ await waitFor(() => {
197
+ expect(mockAddHeading).not.toHaveBeenCalled();
198
+ });
199
+ });
200
+
201
+ it("stays compatible with the ImageBorderContainer caller path", () => {
202
+ const imageBorderConfig = ImageBorderContainer({
203
+ value: createMockValue(),
204
+ state: "default",
205
+ imageStyles: {
206
+ width: 100,
207
+ height: 60,
208
+ },
209
+ });
210
+
211
+ const { getByTestId } = renderBorderContainer({
212
+ style: imageBorderConfig.style,
213
+ pointerEvents: imageBorderConfig.additionalProps.pointerEvents,
214
+ borderPosition: imageBorderConfig.additionalProps.borderPosition,
215
+ borderPaddingTop: imageBorderConfig.additionalProps.borderPaddingTop,
216
+ borderPaddingRight: imageBorderConfig.additionalProps.borderPaddingRight,
217
+ borderPaddingBottom:
218
+ imageBorderConfig.additionalProps.borderPaddingBottom,
219
+ borderPaddingLeft: imageBorderConfig.additionalProps.borderPaddingLeft,
220
+ children: <View testID="image-border-child" />,
221
+ state: null,
222
+ hasTextLabels: true,
223
+ });
224
+
225
+ const root = getByTestId("border-container");
226
+ const overlay = root.children[0] as any;
227
+ const overlayHost = overlay.children[0] as any;
228
+ const child = root.children[1] as any;
229
+
230
+ expect(root.props.pointerEvents).toBe("none");
231
+ expect(child.props.testID).toBe("image-border-child");
232
+
233
+ const flattenedRootStyle = StyleSheet.flatten(root.props.style);
234
+ expect(flattenedRootStyle.borderRadius).toBe(10);
235
+ expect(flattenedRootStyle.marginTop).toBe(12);
236
+ expect(flattenedRootStyle.marginLeft).toBe(14);
237
+ expect(flattenedRootStyle.marginRight).toBe(16);
238
+ expect(flattenedRootStyle.marginBottom).toBe(18);
76
239
 
77
- expect(children[1].props.testID).toBe("child");
240
+ expect(overlay.props.borderWidth).toBe(5);
241
+ expect(overlay.props.borderColor).toBe("black");
242
+ expect(overlay.props.borderRadius).toBe(10);
78
243
 
79
- expect(children[0].children.length).toBe(0);
244
+ const flattenedOverlayStyle = StyleSheet.flatten(overlayHost.props.style);
245
+ expect(flattenedOverlayStyle.borderWidth).toBe(5);
246
+ expect(flattenedOverlayStyle.borderColor).toBe("black");
247
+ expect(flattenedOverlayStyle.borderTopLeftRadius).toBe(21);
248
+ expect(flattenedOverlayStyle.borderTopRightRadius).toBe(21);
249
+ expect(flattenedOverlayStyle.borderBottomLeftRadius).toBe(23);
250
+ expect(flattenedOverlayStyle.borderBottomRightRadius).toBe(22);
80
251
  });
81
252
  });