@applicaster/zapp-react-native-ui-components 15.0.0-alpha.1693300296 → 15.0.0-alpha.1844658165
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.
- package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +12 -2
- package/Components/MasterCell/DefaultComponents/PressableView.tsx +103 -9
- package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +9 -0
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Asset.ts +2 -6
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +14 -3
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +104 -0
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/helpers.ts +7 -4
- package/Components/MasterCell/MappingFunctions/index.js +3 -2
- package/Components/TopCutoffOverlay/hooks/__tests__/useMarginTop.test.ts +130 -0
- package/Components/TopCutoffOverlay/hooks/index.ts +1 -0
- package/Components/TopCutoffOverlay/hooks/useMarginTop.ts +59 -0
- package/Components/TopCutoffOverlay/index.tsx +55 -0
- package/package.json +5 -5
|
@@ -3,17 +3,27 @@ import { isVideoPreviewEnabled } from "@applicaster/zapp-react-native-ui-compone
|
|
|
3
3
|
import { LiveImage } from "../LiveImage";
|
|
4
4
|
import PureImage from "../Image";
|
|
5
5
|
import { useIsScreenActive } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
6
|
+
import { View } from "react-native";
|
|
6
7
|
|
|
7
8
|
type Props = {
|
|
8
9
|
enable_video_preview: boolean;
|
|
9
10
|
player_screen_id: string;
|
|
11
|
+
asset?: string | React.ReactNode;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
export const ImageContainer = (props: Props) => {
|
|
13
15
|
const isActive = useIsScreenActive();
|
|
16
|
+
let Component;
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
// Workaround to render an asset as component...
|
|
19
|
+
if (props.asset) {
|
|
20
|
+
Component = View;
|
|
21
|
+
|
|
22
|
+
return <Component {...props}>{props.asset}</Component>;
|
|
23
|
+
} else {
|
|
24
|
+
Component =
|
|
25
|
+
isVideoPreviewEnabled(props as Props) && isActive ? LiveImage : PureImage;
|
|
26
|
+
}
|
|
17
27
|
|
|
18
28
|
return <Component {...props} />;
|
|
19
29
|
};
|
|
@@ -10,11 +10,14 @@ import {
|
|
|
10
10
|
resolveLabelText,
|
|
11
11
|
} from "./mobile/MobileActionButtons/helpers";
|
|
12
12
|
|
|
13
|
+
import { createUUID } from "@applicaster/zapp-react-native-utils/stringUtils";
|
|
14
|
+
|
|
13
15
|
type ChildElementProps = {
|
|
14
16
|
children?: React.ReactNode;
|
|
15
17
|
mobileActionRole?: string;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
uri?: string;
|
|
19
|
+
asset?: React.ReactNode;
|
|
20
|
+
state?: "default" | "focused";
|
|
18
21
|
};
|
|
19
22
|
|
|
20
23
|
type Props = {
|
|
@@ -22,6 +25,9 @@ type Props = {
|
|
|
22
25
|
item: ZappEntry | ZappFeed;
|
|
23
26
|
action?: {
|
|
24
27
|
identifier?: string;
|
|
28
|
+
flavour?: "flavour_1" | "flavour_2";
|
|
29
|
+
width?: number;
|
|
30
|
+
height?: number;
|
|
25
31
|
};
|
|
26
32
|
style?: Record<string, unknown>;
|
|
27
33
|
focusedStyles?: Record<string, unknown>;
|
|
@@ -35,6 +41,74 @@ const isValidElement = (
|
|
|
35
41
|
): child is React.ReactElement<ChildElementProps> =>
|
|
36
42
|
React.isValidElement(child);
|
|
37
43
|
|
|
44
|
+
/** retrieves asset uri for a given flavour,
|
|
45
|
+
* if flavour is not provided, returns the default asset from `asset` or selected state asset (if available)
|
|
46
|
+
* asset can be:
|
|
47
|
+
* provided as asset path,
|
|
48
|
+
* provided as [default || flavour_1, alternative || flavour_2] array.
|
|
49
|
+
* mobileButtonAssets asset can be:
|
|
50
|
+
* provided as asset path,
|
|
51
|
+
* provided as [default || flavour_1, alternative || flavour_2] array,
|
|
52
|
+
* provided as [[] as flavour_1, [] as flavour_2] array for multiple flavours, where each flavour can be either a path or [default, alternative] array,
|
|
53
|
+
* provided as a React component accepting flavour as prop.
|
|
54
|
+
*
|
|
55
|
+
* isActive reflect the state of the action and can be used to render different asset for active/inactive state if asset is provided as array
|
|
56
|
+
*
|
|
57
|
+
* */
|
|
58
|
+
const selectByAssetFlavour = (
|
|
59
|
+
actionState: {
|
|
60
|
+
asset?: string | [string, string] | CellActionAssetComponent;
|
|
61
|
+
mobileButtonAssets?:
|
|
62
|
+
| [string, string]
|
|
63
|
+
| [string, string][]
|
|
64
|
+
| CellActionAssetComponent;
|
|
65
|
+
},
|
|
66
|
+
flavour?: "flavour_1" | "flavour_2",
|
|
67
|
+
isActive?: boolean
|
|
68
|
+
): string | CellActionAssetComponent => {
|
|
69
|
+
if (actionState.mobileButtonAssets) {
|
|
70
|
+
if (typeof actionState.mobileButtonAssets === "function") {
|
|
71
|
+
return actionState.mobileButtonAssets;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (flavour) {
|
|
75
|
+
if (flavour === "flavour_1") {
|
|
76
|
+
if (typeof actionState.mobileButtonAssets[0] === "string") {
|
|
77
|
+
return actionState.mobileButtonAssets[0];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (Array.isArray(actionState.mobileButtonAssets[0])) {
|
|
81
|
+
return actionState.mobileButtonAssets[0][isActive ? 1 : 0];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return actionState.mobileButtonAssets[0];
|
|
85
|
+
} else if (flavour === "flavour_2") {
|
|
86
|
+
if (typeof actionState.mobileButtonAssets[1] === "string") {
|
|
87
|
+
return actionState.mobileButtonAssets[1];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (Array.isArray(actionState.mobileButtonAssets[1])) {
|
|
91
|
+
return actionState.mobileButtonAssets[1][isActive ? 1 : 0];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return actionState.mobileButtonAssets[1];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return Array.isArray(actionState.mobileButtonAssets[0])
|
|
99
|
+
? actionState.mobileButtonAssets[0][isActive ? 1 : 0]
|
|
100
|
+
: actionState.mobileButtonAssets[0];
|
|
101
|
+
} else {
|
|
102
|
+
if (typeof actionState?.asset === "function") {
|
|
103
|
+
return actionState.asset;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return typeof actionState?.asset === "string"
|
|
107
|
+
? actionState.asset
|
|
108
|
+
: actionState.asset[isActive ? 1 : 0];
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
38
112
|
export function PressableView({
|
|
39
113
|
children,
|
|
40
114
|
item,
|
|
@@ -92,15 +166,13 @@ export function PressableView({
|
|
|
92
166
|
|
|
93
167
|
const isActive = resolveIsActive(actionState, legacySelected);
|
|
94
168
|
|
|
95
|
-
const labelText = supportsEntryState
|
|
96
|
-
? resolveLabelText(actionState?.label)
|
|
97
|
-
: "";
|
|
98
|
-
|
|
99
169
|
const shouldRenderAsset = Boolean(
|
|
100
|
-
supportsEntryState ? actionState?.
|
|
170
|
+
supportsEntryState ? actionState?.mobileButtonAssets : false
|
|
101
171
|
);
|
|
102
172
|
|
|
103
|
-
const shouldRenderLabel = Boolean(
|
|
173
|
+
const shouldRenderLabel = Boolean(
|
|
174
|
+
supportsEntryState && resolveLabelText(actionState?.label)
|
|
175
|
+
);
|
|
104
176
|
|
|
105
177
|
const cloneChildrenWithState = (nodes?: React.ReactNode): React.ReactNode => {
|
|
106
178
|
return React.Children.map(nodes, (child) => {
|
|
@@ -129,7 +201,29 @@ export function PressableView({
|
|
|
129
201
|
const nextProps: Partial<ChildElementProps> = {};
|
|
130
202
|
|
|
131
203
|
if (role === "asset") {
|
|
132
|
-
|
|
204
|
+
if (action.flavour) {
|
|
205
|
+
const resolvedAsset = selectByAssetFlavour(
|
|
206
|
+
actionState,
|
|
207
|
+
action.flavour,
|
|
208
|
+
isActive
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (typeof resolvedAsset === "function") {
|
|
212
|
+
const AssetComponent = resolvedAsset as CellActionAssetComponent;
|
|
213
|
+
|
|
214
|
+
// WIP: might require more changes
|
|
215
|
+
nextProps.asset = (
|
|
216
|
+
<AssetComponent
|
|
217
|
+
flavour={action.flavour}
|
|
218
|
+
width={action.width}
|
|
219
|
+
height={action.height}
|
|
220
|
+
cellUUID={createUUID()}
|
|
221
|
+
/>
|
|
222
|
+
);
|
|
223
|
+
} else {
|
|
224
|
+
nextProps.uri = resolvedAsset;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
133
227
|
}
|
|
134
228
|
|
|
135
229
|
if (role === "label") {
|
|
@@ -53,6 +53,15 @@ export const useTextLabel = ({ label, entry }): string => {
|
|
|
53
53
|
});
|
|
54
54
|
}, []);
|
|
55
55
|
|
|
56
|
+
React.useEffect(() => {
|
|
57
|
+
// This are properly updating when state changes (see offline-content-button action)
|
|
58
|
+
if (typeof action?.addListener === "function") {
|
|
59
|
+
return action.addListener(String(entry?.id), (nextState) => {
|
|
60
|
+
setEntryStateLocal(nextState);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
56
65
|
if (context && name && action) {
|
|
57
66
|
return prepareHebrewText(extractLabel(entryStateLocal.label, name), isRTL);
|
|
58
67
|
}
|
|
@@ -3,11 +3,11 @@ import { toNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/nu
|
|
|
3
3
|
type Props = {
|
|
4
4
|
prefix: string;
|
|
5
5
|
value: Function;
|
|
6
|
-
actionIdentifier: string;
|
|
7
6
|
testID?: string;
|
|
8
7
|
};
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
/** Asset used in conjunction with PressableView inside mobile action buttons, uri is provided by PressableView */
|
|
10
|
+
export const Asset = ({ prefix, value, testID }: Props) => {
|
|
11
11
|
if (!value(`${prefix}_asset_enabled`)) {
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
@@ -35,10 +35,6 @@ export const Asset = ({ prefix, value, actionIdentifier, testID }: Props) => {
|
|
|
35
35
|
},
|
|
36
36
|
],
|
|
37
37
|
additionalProps: {
|
|
38
|
-
source: {
|
|
39
|
-
context: actionIdentifier,
|
|
40
|
-
},
|
|
41
|
-
state: "inactive",
|
|
42
38
|
mobileActionRole: "asset",
|
|
43
39
|
testID: testID ? `${testID}-asset` : undefined,
|
|
44
40
|
},
|
|
@@ -46,9 +46,17 @@ export const Button = ({
|
|
|
46
46
|
return null;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
if (
|
|
50
|
+
!value(`${stylePrefix}_asset_enabled`) &&
|
|
51
|
+
!value(`${stylePrefix}_label_enabled`)
|
|
52
|
+
) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
const testID = `mobile_action_button_${index + 1}`;
|
|
50
57
|
const actionIdentifier = value(`${specificPrefix}_assign_action`);
|
|
51
58
|
const assetAlignment = value(`${stylePrefix}_asset_alignment`) || "left";
|
|
59
|
+
const actionAssetFlavour = value(`${stylePrefix}_action_asset_flavour`);
|
|
52
60
|
|
|
53
61
|
const contentsAlignment =
|
|
54
62
|
value(`${stylePrefix}_contents_alignment`) || "center";
|
|
@@ -57,8 +65,9 @@ export const Button = ({
|
|
|
57
65
|
type: "PressableView",
|
|
58
66
|
style: {
|
|
59
67
|
flexDirection: getContentDirection(assetAlignment),
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
alignContent: "center",
|
|
69
|
+
alignItems: getContentsAlignment(contentsAlignment, assetAlignment),
|
|
70
|
+
justifyContent: getContentsAlignment(contentsAlignment, assetAlignment),
|
|
62
71
|
|
|
63
72
|
marginTop: toNumberWithDefaultZero(value(`${stylePrefix}_margin_top`)),
|
|
64
73
|
marginRight: toNumberWithDefaultZero(
|
|
@@ -93,6 +102,9 @@ export const Button = ({
|
|
|
93
102
|
additionalProps: {
|
|
94
103
|
action: {
|
|
95
104
|
identifier: actionIdentifier,
|
|
105
|
+
flavour: actionAssetFlavour,
|
|
106
|
+
width: toNumberWithDefault(24, value(`${stylePrefix}_asset_width`)),
|
|
107
|
+
height: toNumberWithDefault(24, value(`${stylePrefix}_asset_height`)),
|
|
96
108
|
},
|
|
97
109
|
focusedStyles: {
|
|
98
110
|
backgroundColor: value(`${stylePrefix}_focused_background_color`),
|
|
@@ -104,7 +116,6 @@ export const Button = ({
|
|
|
104
116
|
Asset({
|
|
105
117
|
prefix: stylePrefix,
|
|
106
118
|
value,
|
|
107
|
-
actionIdentifier,
|
|
108
119
|
testID,
|
|
109
120
|
}),
|
|
110
121
|
Spacer(),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
2
3
|
import { render, fireEvent } from "@testing-library/react-native";
|
|
3
4
|
import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
|
|
4
5
|
|
|
@@ -57,6 +58,10 @@ const buildActionContext = (entryState = {}) => ({
|
|
|
57
58
|
inactive: "https://example.com/image-inactive.png",
|
|
58
59
|
active: "https://example.com/image-active.png",
|
|
59
60
|
},
|
|
61
|
+
mobileButtonAssets: [
|
|
62
|
+
"https://example.com/image-inactive.png",
|
|
63
|
+
"https://example.com/image-active.png",
|
|
64
|
+
],
|
|
60
65
|
label: {
|
|
61
66
|
label_1: "Play",
|
|
62
67
|
},
|
|
@@ -188,4 +193,103 @@ describe("PressableView", () => {
|
|
|
188
193
|
})
|
|
189
194
|
);
|
|
190
195
|
});
|
|
196
|
+
|
|
197
|
+
describe("component asset via mobileButtonAssets", () => {
|
|
198
|
+
const nodeWithFlavour1 = {
|
|
199
|
+
...baseNode,
|
|
200
|
+
props: {
|
|
201
|
+
...baseNode.props,
|
|
202
|
+
action: {
|
|
203
|
+
identifier: "navigation_action",
|
|
204
|
+
flavour: "flavour_1" as const,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const nodeWithFlavour2 = {
|
|
210
|
+
...baseNode,
|
|
211
|
+
props: {
|
|
212
|
+
...baseNode.props,
|
|
213
|
+
action: {
|
|
214
|
+
identifier: "navigation_action",
|
|
215
|
+
flavour: "flavour_2" as const,
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
it("renders mobileButtonAssets component instead of Image when it is a function", () => {
|
|
221
|
+
const MockAssetComponent = jest.fn(({ testID }) => (
|
|
222
|
+
<View testID={testID} accessibilityLabel="mock-asset" />
|
|
223
|
+
));
|
|
224
|
+
|
|
225
|
+
mockUseActions.mockReturnValue(
|
|
226
|
+
buildActionContext({ mobileButtonAssets: MockAssetComponent })
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const { getByTestId } = renderNode(nodeWithFlavour1);
|
|
230
|
+
|
|
231
|
+
expect(MockAssetComponent).toHaveBeenCalled();
|
|
232
|
+
expect(getByTestId("mobile-action-button-asset")).toBeTruthy();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("passes flavour_1 to the mobileButtonAssets component", () => {
|
|
236
|
+
const MockAssetComponent = jest.fn(({ testID }) => (
|
|
237
|
+
<View testID={testID} />
|
|
238
|
+
));
|
|
239
|
+
|
|
240
|
+
mockUseActions.mockReturnValue(
|
|
241
|
+
buildActionContext({ mobileButtonAssets: MockAssetComponent })
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
renderNode(nodeWithFlavour1);
|
|
245
|
+
|
|
246
|
+
expect(MockAssetComponent).toHaveBeenCalledWith(
|
|
247
|
+
expect.objectContaining({ flavour: "flavour_1" }),
|
|
248
|
+
expect.anything()
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("passes flavour_2 to the mobileButtonAssets component", () => {
|
|
253
|
+
const MockAssetComponent = jest.fn(({ testID }) => (
|
|
254
|
+
<View testID={testID} />
|
|
255
|
+
));
|
|
256
|
+
|
|
257
|
+
mockUseActions.mockReturnValue(
|
|
258
|
+
buildActionContext({ mobileButtonAssets: MockAssetComponent })
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
renderNode(nodeWithFlavour2);
|
|
262
|
+
|
|
263
|
+
expect(MockAssetComponent).toHaveBeenCalledWith(
|
|
264
|
+
expect.objectContaining({ flavour: "flavour_2" }),
|
|
265
|
+
expect.anything()
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("does not render asset when mobileButtonAssets is absent", () => {
|
|
270
|
+
mockUseActions.mockReturnValue(
|
|
271
|
+
buildActionContext({ mobileButtonAssets: undefined })
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const { queryByTestId } = renderNode(nodeWithFlavour1);
|
|
275
|
+
|
|
276
|
+
expect(queryByTestId("mobile-action-button-asset")).toBeNull();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("renders asset when mobileButtonAssets exists even if asset is absent", () => {
|
|
280
|
+
mockUseActions.mockReturnValue(
|
|
281
|
+
buildActionContext({
|
|
282
|
+
asset: undefined,
|
|
283
|
+
mobileButtonAssets: [
|
|
284
|
+
"https://example.com/image-inactive.png",
|
|
285
|
+
"https://example.com/image-active.png",
|
|
286
|
+
],
|
|
287
|
+
})
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const { getByTestId } = renderNode(nodeWithFlavour1);
|
|
291
|
+
|
|
292
|
+
expect(getByTestId("mobile-action-button-asset")).toBeTruthy();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
191
295
|
});
|
|
@@ -40,12 +40,12 @@ export function getContentDirection(alignment = "left") {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function getContentsAlignment(alignment = "center") {
|
|
43
|
+
export function getContentsAlignment(alignment = "center", direction = "left") {
|
|
44
44
|
switch (alignment) {
|
|
45
45
|
case "left":
|
|
46
|
-
return "flex-start";
|
|
46
|
+
return direction === "left" ? "flex-start" : "flex-end";
|
|
47
47
|
case "right":
|
|
48
|
-
return "flex-end";
|
|
48
|
+
return direction === "left" ? "flex-end" : "flex-start";
|
|
49
49
|
case "center":
|
|
50
50
|
default:
|
|
51
51
|
return "center";
|
|
@@ -75,7 +75,10 @@ export function resolveIsActive(actionState, fallbackSelected = false) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export function buildLegacySelection(item, actionContext) {
|
|
78
|
-
|
|
78
|
+
// Some state are not array. In this case we fallback to the default value provided by the action or false
|
|
79
|
+
const defaultIsSelected = Array.isArray(actionContext?.state)
|
|
80
|
+
? (actionContext?.state || []).includes(item)
|
|
81
|
+
: false;
|
|
79
82
|
|
|
80
83
|
return actionContext?.masterCell?.isSelected
|
|
81
84
|
? actionContext?.masterCell?.isSelected(item)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import * as R from "ramda";
|
|
2
1
|
import { isDateValid } from "./Utils";
|
|
3
2
|
import { masterCellLogger } from "../logger";
|
|
4
3
|
import { imageSrcFromMediaItem as imageSrcFromMediaItemConfigUtils } from "@applicaster/zapp-react-native-utils/configurationUtils";
|
|
4
|
+
import { pathOr, identity } from "@applicaster/zapp-react-native-utils/utils";
|
|
5
5
|
|
|
6
6
|
export const imageSrcFromMediaItem = (...args) => {
|
|
7
7
|
__DEV__ &&
|
|
@@ -34,11 +34,12 @@ export function stringifyDateFromPath(obj, path) {
|
|
|
34
34
|
* @returns {any} Found object or empty string
|
|
35
35
|
*/
|
|
36
36
|
export function pathWithFallback(obj, path) {
|
|
37
|
-
return
|
|
37
|
+
return pathOr("", path, obj);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// prettier-ignore
|
|
41
41
|
const functionsNames = {
|
|
42
|
+
"identity": identity,
|
|
42
43
|
"path": pathWithFallback,
|
|
43
44
|
"image_src_from_media_item": imageSrcFromMediaItemConfigUtils,
|
|
44
45
|
"stringify_date_from_path": stringifyDateFromPath,
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react-native";
|
|
2
|
+
import { useMarginTop } from "../useMarginTop";
|
|
3
|
+
|
|
4
|
+
// Mocks
|
|
5
|
+
jest.mock("@applicaster/zapp-react-native-utils/theme", () => ({
|
|
6
|
+
useTheme: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => ({
|
|
10
|
+
useCurrentScreenData: jest.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
jest.mock("@applicaster/zapp-react-native-utils/componentsUtils", () => ({
|
|
14
|
+
isFirstComponentScreenPicker: jest.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Imports after mocks
|
|
18
|
+
import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
|
|
19
|
+
import { useCurrentScreenData } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
20
|
+
import { isFirstComponentScreenPicker } from "@applicaster/zapp-react-native-utils/componentsUtils";
|
|
21
|
+
|
|
22
|
+
describe("useMarginTop", () => {
|
|
23
|
+
const mockTheme = {
|
|
24
|
+
screen_margin_top: 10,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
jest.clearAllMocks();
|
|
29
|
+
|
|
30
|
+
(useTheme as jest.Mock).mockReturnValue(mockTheme);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns 0 when ScreenPicker is first component", () => {
|
|
34
|
+
(useCurrentScreenData as jest.Mock).mockReturnValue({
|
|
35
|
+
ui_components: [],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
(isFirstComponentScreenPicker as jest.Mock).mockReturnValue(true);
|
|
39
|
+
|
|
40
|
+
const { result } = renderHook(() => useMarginTop("screen1"));
|
|
41
|
+
|
|
42
|
+
expect(result.current).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns 0 for player screen", () => {
|
|
46
|
+
(useCurrentScreenData as jest.Mock).mockReturnValue({
|
|
47
|
+
plugin_type: "player",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
(isFirstComponentScreenPicker as jest.Mock).mockReturnValue(false);
|
|
51
|
+
|
|
52
|
+
const { result } = renderHook(() => useMarginTop("screen1"));
|
|
53
|
+
|
|
54
|
+
expect(result.current).toBe(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns theme value when margin is empty string", () => {
|
|
58
|
+
(useCurrentScreenData as jest.Mock).mockReturnValue({
|
|
59
|
+
styles: {
|
|
60
|
+
screen_margin_top: "",
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
(isFirstComponentScreenPicker as jest.Mock).mockReturnValue(false);
|
|
65
|
+
|
|
66
|
+
const { result } = renderHook(() => useMarginTop("screen1"));
|
|
67
|
+
|
|
68
|
+
expect(result.current).toBe(10);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns theme value when undefined and general content screen", () => {
|
|
72
|
+
(useCurrentScreenData as jest.Mock).mockReturnValue({
|
|
73
|
+
plugin_type: "general_content",
|
|
74
|
+
styles: {
|
|
75
|
+
screen_margin_top: undefined,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
(isFirstComponentScreenPicker as jest.Mock).mockReturnValue(false);
|
|
80
|
+
|
|
81
|
+
const { result } = renderHook(() => useMarginTop("screen1"));
|
|
82
|
+
|
|
83
|
+
expect(result.current).toBe(10);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns theme value when undefined and supports ui_components", () => {
|
|
87
|
+
(useCurrentScreenData as jest.Mock).mockReturnValue({
|
|
88
|
+
ui_components: ["something"],
|
|
89
|
+
styles: {
|
|
90
|
+
screen_margin_top: undefined,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
(isFirstComponentScreenPicker as jest.Mock).mockReturnValue(false);
|
|
95
|
+
|
|
96
|
+
const { result } = renderHook(() => useMarginTop("screen1"));
|
|
97
|
+
|
|
98
|
+
expect(result.current).toBe(10);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns 0 when undefined and not general content and no ui_components", () => {
|
|
102
|
+
(useCurrentScreenData as jest.Mock).mockReturnValue({
|
|
103
|
+
plugin_type: "other",
|
|
104
|
+
ui_components: null,
|
|
105
|
+
styles: {
|
|
106
|
+
screen_margin_top: undefined,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
(isFirstComponentScreenPicker as jest.Mock).mockReturnValue(false);
|
|
111
|
+
|
|
112
|
+
const { result } = renderHook(() => useMarginTop("screen1"));
|
|
113
|
+
|
|
114
|
+
expect(result.current).toBe(0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns value from screenData when defined", () => {
|
|
118
|
+
(useCurrentScreenData as jest.Mock).mockReturnValue({
|
|
119
|
+
styles: {
|
|
120
|
+
screen_margin_top: 25,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
(isFirstComponentScreenPicker as jest.Mock).mockReturnValue(false);
|
|
125
|
+
|
|
126
|
+
const { result } = renderHook(() => useMarginTop("screen1"));
|
|
127
|
+
|
|
128
|
+
expect(result.current).toBe(25);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useMarginTop } from "./useMarginTop";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
|
|
2
|
+
import { useCurrentScreenData } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
3
|
+
import { isFirstComponentScreenPicker } from "@applicaster/zapp-react-native-utils/componentsUtils";
|
|
4
|
+
import { toNumberWithDefaultZero } from "@applicaster/zapp-react-native-utils/numberUtils";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The MarginTop is essentially a feature used for managing the visibility of components on your screen.
|
|
8
|
+
* A more accurate term for this might be something like a 'component visibility threshold' or 'cut-off point'.
|
|
9
|
+
* In practical terms, MarginTop determines a specific vertical position (given in the 'y' coordinate)
|
|
10
|
+
* on your screen above which components aren't shown.
|
|
11
|
+
* You might visualize this as a horizontal line across your screen, and any component crossing this line becomes invisible.
|
|
12
|
+
*
|
|
13
|
+
* Classic use case for this feature is making sure that component aren't displayed underneath the navigation bar.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const useMarginTop = (targetScreenId: string): number => {
|
|
17
|
+
const theme = useTheme<BaseThemePropertiesTV>();
|
|
18
|
+
const screenData = useCurrentScreenData(targetScreenId);
|
|
19
|
+
const isGeneralContentScreen = screenData?.plugin_type === "general_content";
|
|
20
|
+
const supportsUiComponents = screenData?.ui_components;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* ScreenPicker is a component but should really be a screen.
|
|
24
|
+
* We need to skip margin top for it as it's already applied to the target screen
|
|
25
|
+
**/
|
|
26
|
+
|
|
27
|
+
// ignore margin on screenPicker
|
|
28
|
+
if (isFirstComponentScreenPicker(screenData?.ui_components)) {
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const isPlayer = screenData?.plugin_type === "player";
|
|
33
|
+
|
|
34
|
+
// ignore margin on inlinePlayer (remove if better way of identifying cases for plugins that don't have marginTop)
|
|
35
|
+
if (isPlayer) {
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Empty string means that value is blank in the CMS. Fallback to theme
|
|
40
|
+
if (String(screenData?.styles?.screen_margin_top) === "") {
|
|
41
|
+
return toNumberWithDefaultZero(theme.screen_margin_top);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* If value is undefined it means one of three things
|
|
46
|
+
* 1. Screen is not a general content screen and it doesn't handle ui components (return 0)
|
|
47
|
+
* 2. Screen is a general content screen but it doesn't have a margin top value (return theme value)
|
|
48
|
+
* 3. Screen isn't general content screen but it handles the ui components (return theme value)
|
|
49
|
+
*/
|
|
50
|
+
if (screenData?.styles?.screen_margin_top === undefined) {
|
|
51
|
+
if (isGeneralContentScreen || supportsUiComponents) {
|
|
52
|
+
return toNumberWithDefaultZero(theme.screen_margin_top);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return toNumberWithDefaultZero(screenData?.styles?.screen_margin_top);
|
|
59
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, StyleSheet } from "react-native";
|
|
3
|
+
import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
|
|
4
|
+
import { useScreenConfiguration } from "@applicaster/zapp-react-native-ui-components/Components/River/useScreenConfiguration";
|
|
5
|
+
|
|
6
|
+
import { useMarginTop } from "./hooks";
|
|
7
|
+
|
|
8
|
+
interface IProps {
|
|
9
|
+
targetScreenId?: string;
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
applyTopCutoff?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const styles = StyleSheet.create({
|
|
15
|
+
container: {
|
|
16
|
+
flex: 1,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const TopCutoffOverlay: React.FC<IProps> = ({
|
|
21
|
+
targetScreenId,
|
|
22
|
+
children,
|
|
23
|
+
applyTopCutoff = true,
|
|
24
|
+
}: IProps) => {
|
|
25
|
+
const cutoffHeight = useMarginTop(targetScreenId);
|
|
26
|
+
|
|
27
|
+
const { backgroundColor: screenBackgroundColor } =
|
|
28
|
+
useScreenConfiguration(targetScreenId);
|
|
29
|
+
|
|
30
|
+
const theme = useTheme();
|
|
31
|
+
const themeBackgroundColor = theme?.app_background_color;
|
|
32
|
+
|
|
33
|
+
if (!applyTopCutoff) {
|
|
34
|
+
return children;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<View style={styles.container}>
|
|
39
|
+
{children}
|
|
40
|
+
|
|
41
|
+
<View
|
|
42
|
+
style={{
|
|
43
|
+
position: "absolute",
|
|
44
|
+
top: 0,
|
|
45
|
+
left: 0,
|
|
46
|
+
right: 0,
|
|
47
|
+
height: cutoffHeight,
|
|
48
|
+
|
|
49
|
+
backgroundColor:
|
|
50
|
+
screenBackgroundColor || themeBackgroundColor || "transparent",
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
</View>
|
|
54
|
+
);
|
|
55
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-ui-components",
|
|
3
|
-
"version": "15.0.0-alpha.
|
|
3
|
+
"version": "15.0.0-alpha.1844658165",
|
|
4
4
|
"description": "Applicaster Zapp React Native ui components for the Quick Brick App",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
},
|
|
29
29
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@applicaster/applicaster-types": "15.0.0-alpha.
|
|
32
|
-
"@applicaster/zapp-react-native-bridge": "15.0.0-alpha.
|
|
33
|
-
"@applicaster/zapp-react-native-redux": "15.0.0-alpha.
|
|
34
|
-
"@applicaster/zapp-react-native-utils": "15.0.0-alpha.
|
|
31
|
+
"@applicaster/applicaster-types": "15.0.0-alpha.1844658165",
|
|
32
|
+
"@applicaster/zapp-react-native-bridge": "15.0.0-alpha.1844658165",
|
|
33
|
+
"@applicaster/zapp-react-native-redux": "15.0.0-alpha.1844658165",
|
|
34
|
+
"@applicaster/zapp-react-native-utils": "15.0.0-alpha.1844658165",
|
|
35
35
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
36
|
"promise": "^8.3.0",
|
|
37
37
|
"url": "^0.11.0",
|