@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,80 @@
1
+ import { buildActionButtonsModel } from "../model";
2
+
3
+ describe("buildActionButtonsModel", () => {
4
+ it("returns null when the container is disabled", () => {
5
+ const configuration = {
6
+ mobile_buttons_container_buttons_enabled: false,
7
+ mobile_button_1_button_enabled: true,
8
+ };
9
+
10
+ const value = (key) => configuration[key];
11
+
12
+ expect(
13
+ buildActionButtonsModel({
14
+ configuration,
15
+ value,
16
+ containerPrefix: "mobile_buttons_container",
17
+ buttonPrefix: "mobile_button",
18
+ })
19
+ ).toBeNull();
20
+ });
21
+
22
+ it("returns explicit enabled slots and semantic container data", () => {
23
+ const configuration = {
24
+ mobile_buttons_container_buttons_enabled: true,
25
+ mobile_buttons_container_align: "right",
26
+ mobile_buttons_container_margin_top: 1,
27
+ mobile_buttons_container_margin_right: 2,
28
+ mobile_buttons_container_margin_bottom: 3,
29
+ mobile_buttons_container_margin_left: 4,
30
+ mobile_buttons_container_stacking: "vertical",
31
+ mobile_buttons_container_horizontal_gutter: 8,
32
+ mobile_buttons_container_vertical_gutter: 12,
33
+ mobile_buttons_container_independent_styles: false,
34
+ mobile_button_1_button_enabled: true,
35
+ mobile_button_2_button_enabled: false,
36
+ mobile_button_3_button_enabled: true,
37
+ };
38
+
39
+ const value = (key) => configuration[key];
40
+
41
+ expect(
42
+ buildActionButtonsModel({
43
+ configuration,
44
+ value,
45
+ containerPrefix: "mobile_buttons_container",
46
+ buttonPrefix: "mobile_button",
47
+ })
48
+ ).toEqual({
49
+ enabledSlots: [1, 3],
50
+ buttonsCount: 2,
51
+ container: {
52
+ horizontalAlign: "flex-end",
53
+ margins: {
54
+ top: 1,
55
+ right: 2,
56
+ bottom: 3,
57
+ left: 4,
58
+ },
59
+ stacking: "vertical",
60
+ horizontalGutter: 8,
61
+ verticalGutter: 12,
62
+ independentStyles: false,
63
+ },
64
+ buttons: [
65
+ {
66
+ slot: 1,
67
+ renderIndex: 0,
68
+ specificPrefix: "mobile_button_1",
69
+ stylePrefix: "mobile_button_1",
70
+ },
71
+ {
72
+ slot: 3,
73
+ renderIndex: 1,
74
+ specificPrefix: "mobile_button_3",
75
+ stylePrefix: "mobile_button_1",
76
+ },
77
+ ],
78
+ });
79
+ });
80
+ });
@@ -0,0 +1,187 @@
1
+ import {
2
+ insertBetweenLabelContainers,
3
+ insertBetweenLabels,
4
+ } from "../placement";
5
+
6
+ describe("ActionButtonsCore placement", () => {
7
+ const buttons = { type: "View", name: "buttons" };
8
+
9
+ const above_labels = [
10
+ { name: "above_label_1" },
11
+ { name: "above_label_2" },
12
+ { name: "above_label_3" },
13
+ ];
14
+
15
+ const below_labels = [
16
+ { name: "below_label_1" },
17
+ { name: "below_label_2" },
18
+ { name: "below_label_3" },
19
+ ];
20
+
21
+ it("inserts buttons after the matching label", () => {
22
+ expect(
23
+ insertBetweenLabels({ position: "below_label_2" }, buttons, below_labels)
24
+ ).toEqual([below_labels[0], below_labels[1], buttons, below_labels[2]]);
25
+ });
26
+
27
+ it("inserts buttons before the matching label", () => {
28
+ expect(
29
+ insertBetweenLabels({ position: "above_label_2" }, buttons, above_labels)
30
+ ).toEqual([above_labels[0], buttons, above_labels[1], above_labels[2]]);
31
+ });
32
+
33
+ it("prepends buttons only when on_top is allowed", () => {
34
+ expect(
35
+ insertBetweenLabels(
36
+ { position: "on_top", allowOnTop: true },
37
+ buttons,
38
+ below_labels
39
+ )
40
+ ).toEqual([buttons, ...below_labels]);
41
+
42
+ expect(
43
+ insertBetweenLabels(
44
+ { position: "on_top", allowOnTop: false },
45
+ buttons,
46
+ below_labels
47
+ )
48
+ ).toEqual(below_labels);
49
+ });
50
+
51
+ it("appends buttons when appendWhenMissing is enabled", () => {
52
+ expect(
53
+ insertBetweenLabels(
54
+ { position: "unknown", appendWhenMissing: true },
55
+ buttons,
56
+ below_labels
57
+ )
58
+ ).toEqual([...below_labels, buttons]);
59
+ });
60
+
61
+ it("returns labels unchanged when appendWhenMissing is disabled", () => {
62
+ expect(
63
+ insertBetweenLabels(
64
+ { position: "unknown", appendWhenMissing: false },
65
+ buttons,
66
+ below_labels
67
+ )
68
+ ).toEqual(below_labels);
69
+ });
70
+
71
+ const labelContainers = [
72
+ {
73
+ elements: [
74
+ {
75
+ elements: [{ name: "top_label_1" }, { name: "top_label_2" }],
76
+ },
77
+ ],
78
+ },
79
+ {
80
+ elements: [
81
+ {
82
+ elements: [{ name: "bottom_label_1" }],
83
+ },
84
+ ],
85
+ },
86
+ ];
87
+
88
+ it("inserts buttons after the matching label in a nested container", () => {
89
+ expect(
90
+ insertBetweenLabelContainers(
91
+ { position: "below_top_label_2" },
92
+ buttons,
93
+ labelContainers
94
+ )
95
+ ).toEqual([
96
+ {
97
+ elements: [
98
+ {
99
+ elements: [
100
+ { name: "top_label_1" },
101
+ { name: "top_label_2" },
102
+ buttons,
103
+ ],
104
+ },
105
+ ],
106
+ },
107
+ labelContainers[1],
108
+ ]);
109
+ });
110
+
111
+ it("inserts buttons before the matching label in a nested container", () => {
112
+ expect(
113
+ insertBetweenLabelContainers(
114
+ { position: "above_top_label_2" },
115
+ buttons,
116
+ labelContainers
117
+ )
118
+ ).toEqual([
119
+ {
120
+ elements: [
121
+ {
122
+ elements: [
123
+ { name: "top_label_1" },
124
+ buttons,
125
+ { name: "top_label_2" },
126
+ ],
127
+ },
128
+ ],
129
+ },
130
+ labelContainers[1],
131
+ ]);
132
+ });
133
+
134
+ it("prepends buttons into the first container only when on_top is allowed", () => {
135
+ expect(
136
+ insertBetweenLabelContainers(
137
+ { position: "on_top", allowOnTop: true },
138
+ buttons,
139
+ labelContainers
140
+ )
141
+ ).toEqual([
142
+ {
143
+ elements: [buttons, ...labelContainers[0].elements],
144
+ },
145
+ labelContainers[1],
146
+ ]);
147
+ });
148
+
149
+ it("appends buttons into the last container when configured", () => {
150
+ expect(
151
+ insertBetweenLabelContainers(
152
+ { position: "unknown", appendWhenMissing: true },
153
+ buttons,
154
+ labelContainers
155
+ )
156
+ ).toEqual([
157
+ labelContainers[0],
158
+ {
159
+ elements: [...labelContainers[1].elements, buttons],
160
+ },
161
+ ]);
162
+ });
163
+
164
+ it("inserts buttons after a matching label in a direct two-level container", () => {
165
+ const directLabelContainers = [
166
+ {
167
+ elements: [{ name: "top_label_1" }, { name: "top_label_2" }],
168
+ },
169
+ {
170
+ elements: [{ name: "bottom_label_1" }],
171
+ },
172
+ ];
173
+
174
+ expect(
175
+ insertBetweenLabelContainers(
176
+ { position: "below_top_label_2" },
177
+ buttons,
178
+ directLabelContainers
179
+ )
180
+ ).toEqual([
181
+ {
182
+ elements: [{ name: "top_label_1" }, { name: "top_label_2" }, buttons],
183
+ },
184
+ directLabelContainers[1],
185
+ ]);
186
+ });
187
+ });
@@ -0,0 +1,45 @@
1
+ import {
2
+ getButtonSlotPrefix,
3
+ getEnabledButtonSlots,
4
+ getStylePrefix,
5
+ } from "../selectors";
6
+
7
+ describe("ActionButtonsCore selectors", () => {
8
+ it("returns explicit enabled button slots without collapsing sparse slots", () => {
9
+ const configuration = {
10
+ mobile_button_1_button_enabled: true,
11
+ mobile_button_2_button_enabled: false,
12
+ mobile_button_3_button_enabled: true,
13
+ };
14
+
15
+ expect(getEnabledButtonSlots(configuration, "mobile_button")).toEqual([
16
+ 1, 3,
17
+ ]);
18
+ });
19
+
20
+ it("returns slot-based prefixes", () => {
21
+ expect(getButtonSlotPrefix("tv_buttons_button", 3)).toBe(
22
+ "tv_buttons_button_3"
23
+ );
24
+ });
25
+
26
+ it("reuses button 1 styles when independent styles are disabled", () => {
27
+ expect(
28
+ getStylePrefix({
29
+ slot: 3,
30
+ independentStyles: false,
31
+ buttonPrefix: "mobile_button",
32
+ })
33
+ ).toBe("mobile_button_1");
34
+ });
35
+
36
+ it("uses the explicit slot prefix when independent styles are enabled", () => {
37
+ expect(
38
+ getStylePrefix({
39
+ slot: 3,
40
+ independentStyles: true,
41
+ buttonPrefix: "mobile_button",
42
+ })
43
+ ).toBe("mobile_button_3");
44
+ });
45
+ });
@@ -0,0 +1,49 @@
1
+ import { buildContainerLayout, getContainerMargins } from "../style";
2
+
3
+ describe("ActionButtonsCore style helpers", () => {
4
+ it("returns numeric margins with zero defaults", () => {
5
+ const configuration = {
6
+ mobile_buttons_container_margin_top: "4",
7
+ mobile_buttons_container_margin_left: 6,
8
+ };
9
+
10
+ const value = (key) => configuration[key];
11
+
12
+ expect(getContainerMargins(value, "mobile_buttons_container")).toEqual({
13
+ top: 4,
14
+ right: 0,
15
+ bottom: 0,
16
+ left: 6,
17
+ });
18
+ });
19
+
20
+ it("returns semantic layout data using shared alignment mapping", () => {
21
+ const configuration = {
22
+ tv_buttons_container_align: "middle",
23
+ tv_buttons_container_margin_top: 1,
24
+ tv_buttons_container_margin_right: 2,
25
+ tv_buttons_container_margin_bottom: 3,
26
+ tv_buttons_container_margin_left: 4,
27
+ tv_buttons_container_stacking: "vertical",
28
+ tv_buttons_container_horizontal_gutter: 8,
29
+ tv_buttons_container_vertical_gutter: 12,
30
+ tv_buttons_container_independent_styles: true,
31
+ };
32
+
33
+ const value = (key) => configuration[key];
34
+
35
+ expect(buildContainerLayout(value, "tv_buttons_container")).toEqual({
36
+ horizontalAlign: "center",
37
+ margins: {
38
+ top: 1,
39
+ right: 2,
40
+ bottom: 3,
41
+ left: 4,
42
+ },
43
+ stacking: "vertical",
44
+ horizontalGutter: 8,
45
+ verticalGutter: 12,
46
+ independentStyles: true,
47
+ });
48
+ });
49
+ });
@@ -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
+ }