@applicaster/zapp-react-native-ui-components 16.0.0-alpha.6580258138 → 16.0.0-alpha.7050631823
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/CellRendererResolver/index.ts +1 -1
- package/Components/Layout/FullWidthRow.tsx +38 -0
- package/Components/MasterCell/dataAdapter.ts +15 -5
- package/Components/MasterCell/index.tsx +30 -5
- package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
- package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
- package/Components/MasterCell/utils/index.ts +85 -46
- package/Components/ModalComponent/BottomSheetModalContent.tsx +78 -22
- package/Components/PlayerContainer/PlayerContainer.tsx +7 -3
- package/Components/River/ComponentsMap/ComponentsMap.tsx +2 -2
- package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +3 -15
- package/Components/Screen/index.tsx +8 -4
- package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
- package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
- package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
- package/Contexts/CachedDimensionsContext/index.ts +17 -2
- package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
- package/Helpers/index.js +0 -39
- package/package.json +5 -5
- package/Components/Screen/utils.ts +0 -16
- package/Helpers/Analytics/index.js +0 -95
- /package/Components/BackgroundImage/{BackgroundImage.tv.android.tsx → BackgroundImage.android.tv.tsx} +0 -0
- /package/Components/BackgroundImage/{BackgroundImage.tv.ios.tsx → BackgroundImage.ios.tv.tsx} +0 -0
- /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import React, { useMemo } from "react";
|
|
2
2
|
import * as R from "ramda";
|
|
3
|
-
import
|
|
3
|
+
import memoizee from "memoizee";
|
|
4
|
+
|
|
4
5
|
import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
5
6
|
import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
|
|
6
|
-
|
|
7
|
-
import { masterCellLogger } from "../logger";
|
|
8
|
-
import { getCellState } from "../../Cell/utils";
|
|
7
|
+
import { isValidColor } from "@applicaster/zapp-react-native-utils/colorUtils";
|
|
9
8
|
import { getColorFromData } from "@applicaster/zapp-react-native-utils/cellUtils";
|
|
10
|
-
import { get } from "@applicaster/zapp-react-native-utils/utils";
|
|
11
|
-
import {
|
|
9
|
+
import { pathOr, get, isNil } from "@applicaster/zapp-react-native-utils/utils";
|
|
10
|
+
import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
|
|
12
11
|
import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
13
12
|
import { useScreenStateStore } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useScreenStateStore";
|
|
14
|
-
import
|
|
15
|
-
import
|
|
13
|
+
import { isNotEmptyString } from "@applicaster/zapp-react-native-utils/stringUtils";
|
|
14
|
+
import { masterCellLogger } from "../logger";
|
|
15
|
+
import { getCellState } from "../../Cell/utils";
|
|
16
|
+
import { isCellSelected, useBehaviorUpdate } from "./behaviorProvider";
|
|
16
17
|
import { DataProvider } from "../DefaultComponents/DataProvider";
|
|
17
18
|
|
|
18
19
|
const hasElementSpecificViewType = (viewType) => (element) => {
|
|
19
|
-
if (
|
|
20
|
+
if (isNil(element)) {
|
|
20
21
|
return false;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -28,12 +29,20 @@ const hasElementSpecificViewType = (viewType) => (element) => {
|
|
|
28
29
|
return hasElementsSpecificViewType(viewType)(element.elements);
|
|
29
30
|
};
|
|
30
31
|
|
|
32
|
+
export const hasElementsSpecificViewType = (viewType) => (elements) => {
|
|
33
|
+
if (isNilOrEmpty(elements)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return R.any(hasElementSpecificViewType(viewType))(elements);
|
|
38
|
+
};
|
|
39
|
+
|
|
31
40
|
const logWarning = (
|
|
32
41
|
colorValueFromCellStyle,
|
|
33
42
|
colorFromProp,
|
|
34
43
|
colorValueFromEntry
|
|
35
44
|
) => {
|
|
36
|
-
if (
|
|
45
|
+
if (isNil(colorValueFromEntry)) {
|
|
37
46
|
masterCellLogger.warn({
|
|
38
47
|
message: `Cannot resolve property ${colorValueFromCellStyle} from the entry.`,
|
|
39
48
|
data: {
|
|
@@ -44,15 +53,10 @@ const logWarning = (
|
|
|
44
53
|
}
|
|
45
54
|
};
|
|
46
55
|
|
|
47
|
-
export
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return R.any(hasElementSpecificViewType(viewType))(elements);
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
function resolveColorForProp(entry: any, colorFromProp: string | undefined) {
|
|
56
|
+
export function resolveColorForProp(
|
|
57
|
+
entry: any,
|
|
58
|
+
colorFromProp: string | undefined
|
|
59
|
+
) {
|
|
56
60
|
if (!colorFromProp) {
|
|
57
61
|
return undefined;
|
|
58
62
|
}
|
|
@@ -62,9 +66,7 @@ function resolveColorForProp(entry: any, colorFromProp: string | undefined) {
|
|
|
62
66
|
colorFromProp.split(".")
|
|
63
67
|
);
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (nestedEntryValue === undefined && !validateColor(color)) {
|
|
69
|
+
if (nestedEntryValue === undefined && !isValidColor(colorFromProp)) {
|
|
68
70
|
logWarning(colorFromProp, colorFromProp, nestedEntryValue);
|
|
69
71
|
|
|
70
72
|
return undefined;
|
|
@@ -86,36 +88,73 @@ function resolveColorForProp(entry: any, colorFromProp: string | undefined) {
|
|
|
86
88
|
|
|
87
89
|
const getColorKeys = memoizee((style) => {
|
|
88
90
|
const styleKeys = Object.keys(style);
|
|
89
|
-
const colorKeys = styleKeys.filter((key) => /color/i.test(key));
|
|
90
91
|
|
|
91
|
-
return
|
|
92
|
+
return styleKeys.filter((key) => /color/i.test(key));
|
|
92
93
|
});
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
/**
|
|
96
|
+
* A color style value is either a literal color (e.g. "#FFFFFF") or a
|
|
97
|
+
* data-mapping path into the entry. Data mappings always point at the entry
|
|
98
|
+
* `extensions` object (e.g. "extensions.brandColor"), so we only need to do a
|
|
99
|
+
* path lookup when the value starts with "extensions" — everything else is a
|
|
100
|
+
* literal color and can be returned as-is.
|
|
101
|
+
*
|
|
102
|
+
* This avoids the previous, very expensive approach that ran `validateColor`
|
|
103
|
+
* on every value and memoized on a full `fast-json-stable-stringify` of the
|
|
104
|
+
* entire entry.
|
|
105
|
+
*/
|
|
106
|
+
const EXTENSIONS_PREFIX = "extensions";
|
|
107
|
+
|
|
108
|
+
const isDataMappingPath = (value: string): boolean =>
|
|
109
|
+
typeof value === "string" && value.startsWith(EXTENSIONS_PREFIX);
|
|
110
|
+
|
|
111
|
+
export const resolveColor = (
|
|
112
|
+
entry,
|
|
113
|
+
style,
|
|
114
|
+
allowDynamicColorsOutsideExtensions
|
|
115
|
+
) => {
|
|
116
|
+
if (style === null || style === undefined) {
|
|
117
|
+
return style;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const colorKeys = getColorKeys(style);
|
|
121
|
+
|
|
122
|
+
if (colorKeys.length === 0) {
|
|
123
|
+
return style;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return colorKeys.reduce(
|
|
127
|
+
(acc, key) => {
|
|
128
|
+
const value = acc[key];
|
|
99
129
|
|
|
100
|
-
|
|
101
|
-
(
|
|
102
|
-
|
|
103
|
-
const colorStyle = resolveColorForProp(entry, acc[value]);
|
|
130
|
+
// 1. The Expensive Edge-Case (Only for the app with root mappings)
|
|
131
|
+
if (allowDynamicColorsOutsideExtensions && isNotEmptyString(value)) {
|
|
132
|
+
const possibleColor = resolveColorForProp(entry, value);
|
|
104
133
|
|
|
105
|
-
|
|
106
|
-
}
|
|
134
|
+
acc[key] = isValidColor(possibleColor) ? possibleColor : null;
|
|
107
135
|
|
|
108
136
|
return acc;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 2. The Fast Path (For 99% of apps)
|
|
140
|
+
if (isDataMappingPath(value)) {
|
|
141
|
+
// resolve the mapped color from the entry; fall back to null
|
|
142
|
+
// (RN ignores null style values) when the path is missing
|
|
143
|
+
const possibleColor = pathOr(null, value.split("."), entry);
|
|
144
|
+
|
|
145
|
+
acc[key] = possibleColor;
|
|
146
|
+
|
|
147
|
+
return acc;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 3. Default Case: Treat as a raw color string
|
|
151
|
+
acc[key] = value;
|
|
152
|
+
|
|
153
|
+
return acc;
|
|
116
154
|
},
|
|
117
|
-
|
|
118
|
-
);
|
|
155
|
+
{ ...style }
|
|
156
|
+
);
|
|
157
|
+
};
|
|
119
158
|
|
|
120
159
|
export function isVideoPreviewEnabled({
|
|
121
160
|
enable_video_preview = false,
|
|
@@ -238,7 +277,7 @@ const recursiveCloneElement = (focused: boolean) => (element) => {
|
|
|
238
277
|
};
|
|
239
278
|
|
|
240
279
|
export const recursiveCloneElementsWithState = (focused: boolean, children) => {
|
|
241
|
-
if (
|
|
280
|
+
if (isNilOrEmpty(children)) {
|
|
242
281
|
return undefined;
|
|
243
282
|
}
|
|
244
283
|
|
|
@@ -252,7 +291,7 @@ export const cloneChildrenWithIds = (
|
|
|
252
291
|
ids: string[],
|
|
253
292
|
children: React.ReactElement[]
|
|
254
293
|
) => {
|
|
255
|
-
if (
|
|
294
|
+
if (isNilOrEmpty(children)) {
|
|
256
295
|
return undefined;
|
|
257
296
|
}
|
|
258
297
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
|
2
|
-
import
|
|
2
|
+
import Sortable from "react-native-sortables";
|
|
3
|
+
import { StyleSheet, View, Text } from "react-native";
|
|
3
4
|
import { Button } from "./Button";
|
|
4
5
|
import { ItemIconProps } from "./Button/ItemIcon";
|
|
5
6
|
import { ItemProps } from "./Button/Item";
|
|
@@ -12,6 +13,33 @@ import type { PluginConfiguration } from "./";
|
|
|
12
13
|
import { ModalHeader as DefaultModalHeader } from "./Header";
|
|
13
14
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
14
15
|
|
|
16
|
+
const styles = StyleSheet.create({
|
|
17
|
+
alphaItem: {
|
|
18
|
+
backgroundColor: "#aaaaff",
|
|
19
|
+
borderRadius: 8,
|
|
20
|
+
height: 80,
|
|
21
|
+
flexDirection: "row",
|
|
22
|
+
alignItems: "center",
|
|
23
|
+
justifyContent: "space-between",
|
|
24
|
+
paddingHorizontal: 16,
|
|
25
|
+
},
|
|
26
|
+
alphaText: {
|
|
27
|
+
fontSize: 28,
|
|
28
|
+
},
|
|
29
|
+
handle: {
|
|
30
|
+
backgroundColor: "rgba(0,0,0,0.2)",
|
|
31
|
+
borderRadius: 5,
|
|
32
|
+
padding: 10,
|
|
33
|
+
},
|
|
34
|
+
handleText: {
|
|
35
|
+
color: "white",
|
|
36
|
+
fontWeight: "bold",
|
|
37
|
+
},
|
|
38
|
+
sortableGrid: {
|
|
39
|
+
padding: 8,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
15
43
|
type ModalComponentProps = {
|
|
16
44
|
items: any[];
|
|
17
45
|
current_selection?: any;
|
|
@@ -38,6 +66,7 @@ type ModalComponentProps = {
|
|
|
38
66
|
theme: PluginConfiguration
|
|
39
67
|
) => ItemIconProps["asset"] | null;
|
|
40
68
|
iconPlacement?: "left" | "right";
|
|
69
|
+
sortable?: boolean;
|
|
41
70
|
};
|
|
42
71
|
|
|
43
72
|
export function BottomSheetModalContent(props: ModalComponentProps) {
|
|
@@ -51,6 +80,7 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
51
80
|
title,
|
|
52
81
|
headerComponent: ModalHeader = DefaultModalHeader,
|
|
53
82
|
buttonComponent: ButtonComponent = Button,
|
|
83
|
+
sortable = true,
|
|
54
84
|
getSelectedItemIcon = ({
|
|
55
85
|
modal_bottom_sheet_item_selected_icon,
|
|
56
86
|
}: BaseThemePropertiesMobile) => modal_bottom_sheet_item_selected_icon,
|
|
@@ -91,6 +121,12 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
91
121
|
|
|
92
122
|
const bottomInset = useSafeAreaInsets().bottom;
|
|
93
123
|
|
|
124
|
+
const [sortableItems, setSortableItems] = React.useState(
|
|
125
|
+
new Array(items.length)
|
|
126
|
+
.fill(null)
|
|
127
|
+
.map((_, index) => items[index]?.name ?? "unknown")
|
|
128
|
+
);
|
|
129
|
+
|
|
94
130
|
return (
|
|
95
131
|
<View
|
|
96
132
|
style={{
|
|
@@ -104,30 +140,50 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
|
|
|
104
140
|
summary={summary}
|
|
105
141
|
title={title}
|
|
106
142
|
/>
|
|
107
|
-
|
|
108
|
-
|
|
143
|
+
|
|
144
|
+
<View
|
|
145
|
+
style={{
|
|
109
146
|
paddingBottom: paddingBottom + bottomInset,
|
|
110
147
|
}}
|
|
111
148
|
>
|
|
112
|
-
{
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
149
|
+
{sortable ? (
|
|
150
|
+
<Sortable.Grid
|
|
151
|
+
customHandle
|
|
152
|
+
columns={1}
|
|
153
|
+
rowGap={8}
|
|
154
|
+
columnGap={8}
|
|
155
|
+
data={sortableItems}
|
|
156
|
+
keyExtractor={(item) => item}
|
|
157
|
+
onDragEnd={({ data }) => {
|
|
158
|
+
console.log("Sortable new order", { data });
|
|
159
|
+
setSortableItems(data);
|
|
160
|
+
}}
|
|
161
|
+
renderItem={({ item, index }) => (
|
|
162
|
+
<View style={styles.alphaItem}>
|
|
163
|
+
<ButtonComponent
|
|
164
|
+
key={index}
|
|
165
|
+
configuration={theme}
|
|
166
|
+
selectedItem={current_selection}
|
|
167
|
+
item={items[index]}
|
|
168
|
+
onPress={handlePress}
|
|
169
|
+
label={theme[item?.label] ?? item?.label}
|
|
170
|
+
iconBaseProps={iconBaseProps}
|
|
171
|
+
itemBaseProps={itemBaseProps}
|
|
172
|
+
labelBaseProps={labelBaseProps}
|
|
173
|
+
selectedItemIcon={getSelectedItemIcon(theme)}
|
|
174
|
+
defaultItemIcon={getDefaultItemIcon(theme)}
|
|
175
|
+
iconPlacement={iconPlacement}
|
|
176
|
+
/>
|
|
177
|
+
<Sortable.Handle>
|
|
178
|
+
<View pointerEvents="none" style={styles.handle}>
|
|
179
|
+
<Text style={styles.handleText}>⋮{index}:</Text>
|
|
180
|
+
</View>
|
|
181
|
+
</Sortable.Handle>
|
|
182
|
+
</View>
|
|
183
|
+
)}
|
|
184
|
+
/>
|
|
185
|
+
) : null}
|
|
186
|
+
</View>
|
|
131
187
|
</View>
|
|
132
188
|
);
|
|
133
189
|
}
|
|
@@ -344,11 +344,15 @@ const PlayerContainerComponent = (props: Props) => {
|
|
|
344
344
|
setIsLoadingNextVideo(false);
|
|
345
345
|
|
|
346
346
|
const resumeTime = Number(item?.extensions?.resumeTime);
|
|
347
|
+
const activePlayer = playerManager.getActivePlayer();
|
|
347
348
|
|
|
348
|
-
// Сhecking that the player itself knows where to start playing, and there is no need to call seekTo after returning from the Сhromecast
|
|
349
|
+
// Сhecking that the player itself knows where to start playing, and there is no need to call seekTo after returning from the Сhromecast.
|
|
350
|
+
// Live streams and a missing/zero resume position are skipped: seeking to 0 on
|
|
351
|
+
// a live stream pulls playback off the live edge into a buffer-starvation seek loop.
|
|
349
352
|
if (
|
|
350
|
-
!
|
|
351
|
-
|
|
353
|
+
!activePlayer?.hasResumePosition() &&
|
|
354
|
+
resumeTime > 0 &&
|
|
355
|
+
!activePlayer?.isLive()
|
|
352
356
|
) {
|
|
353
357
|
player?.seekTo(resumeTime);
|
|
354
358
|
}
|
|
@@ -9,7 +9,7 @@ import { useScreenConfiguration } from "../useScreenConfiguration";
|
|
|
9
9
|
import { RefreshControl } from "../RefreshControl";
|
|
10
10
|
import { ifEmptyUseFallback } from "@applicaster/zapp-react-native-utils/cellUtils";
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
useMarkPipesDataStale,
|
|
13
13
|
useProfilerLogging,
|
|
14
14
|
} from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
15
15
|
import { useLoadingState } from "./hooks/useLoadingState";
|
|
@@ -154,7 +154,7 @@ function ComponentsMapComponent(props: Props) {
|
|
|
154
154
|
[flatListHeight]
|
|
155
155
|
);
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
useMarkPipesDataStale(riverId, riverComponents);
|
|
158
158
|
|
|
159
159
|
const refreshControl = React.useMemo(
|
|
160
160
|
() => (pullToRefreshEnabled ? <RefreshControl /> : null),
|
|
@@ -10,11 +10,7 @@ exports[`componentsMap renders renders components map correctly 1`] = `
|
|
|
10
10
|
>
|
|
11
11
|
<View
|
|
12
12
|
onLayout={[Function]}
|
|
13
|
-
style={
|
|
14
|
-
{
|
|
15
|
-
"flex": 1,
|
|
16
|
-
}
|
|
17
|
-
}
|
|
13
|
+
style={{}}
|
|
18
14
|
>
|
|
19
15
|
<RCTScrollView
|
|
20
16
|
ListFooterComponent={
|
|
@@ -167,11 +163,7 @@ exports[`componentsMap renders renders components map correctly 1`] = `
|
|
|
167
163
|
>
|
|
168
164
|
<View
|
|
169
165
|
onLayout={[Function]}
|
|
170
|
-
style={
|
|
171
|
-
{
|
|
172
|
-
"flex": 1,
|
|
173
|
-
}
|
|
174
|
-
}
|
|
166
|
+
style={{}}
|
|
175
167
|
>
|
|
176
168
|
<View />
|
|
177
169
|
</View>
|
|
@@ -183,11 +175,7 @@ exports[`componentsMap renders renders components map correctly 1`] = `
|
|
|
183
175
|
>
|
|
184
176
|
<View
|
|
185
177
|
onLayout={[Function]}
|
|
186
|
-
style={
|
|
187
|
-
{
|
|
188
|
-
"flex": 1,
|
|
189
|
-
}
|
|
190
|
-
}
|
|
178
|
+
style={{}}
|
|
191
179
|
/>
|
|
192
180
|
</View>
|
|
193
181
|
<View
|
|
@@ -18,10 +18,13 @@ import {
|
|
|
18
18
|
useScreenData,
|
|
19
19
|
} from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
20
20
|
import { getNavigationPluginModule } from "@applicaster/zapp-react-native-app/App/Layout/layoutHelpers";
|
|
21
|
+
import {
|
|
22
|
+
isValidColor,
|
|
23
|
+
isTransparentColor,
|
|
24
|
+
} from "@applicaster/zapp-react-native-utils/colorUtils";
|
|
21
25
|
|
|
22
26
|
import { RouteManager } from "../RouteManager";
|
|
23
27
|
import { useScreenConfiguration } from "../River/useScreenConfiguration";
|
|
24
|
-
import { isValidColor } from "./utils";
|
|
25
28
|
import { useWaitForValidOrientation } from "./hooks";
|
|
26
29
|
|
|
27
30
|
const screenStyles = {
|
|
@@ -82,9 +85,10 @@ export function Screen(_props: Props) {
|
|
|
82
85
|
const style = React.useMemo(
|
|
83
86
|
() => ({
|
|
84
87
|
...screenStyles,
|
|
85
|
-
backgroundColor:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
backgroundColor:
|
|
89
|
+
isValidColor(backgroundColor) && !isTransparentColor(backgroundColor)
|
|
90
|
+
? backgroundColor
|
|
91
|
+
: theme.app_background_color,
|
|
88
92
|
}),
|
|
89
93
|
[theme.app_background_color, backgroundColor]
|
|
90
94
|
);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { StyleSheet,
|
|
3
|
-
|
|
2
|
+
import { StyleSheet, SafeAreaView as RNSafeAreaView } from "react-native";
|
|
3
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
4
4
|
import { useIsRTL } from "@applicaster/zapp-react-native-utils/localizationUtils";
|
|
5
5
|
import CloseButton from "./Buttons/CloseButton";
|
|
6
6
|
import BackButton from "./Buttons/BackButton";
|
|
7
7
|
import BarTitle from "./BarTitle";
|
|
8
|
+
import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
8
9
|
|
|
9
10
|
type Props = {
|
|
10
11
|
screenStyles: {
|
|
@@ -23,6 +24,11 @@ const defaultState: NavBarState = {
|
|
|
23
24
|
title: "",
|
|
24
25
|
};
|
|
25
26
|
|
|
27
|
+
const SafeAreaViewPlatform = platformSelect({
|
|
28
|
+
ios: RNSafeAreaView,
|
|
29
|
+
android: SafeAreaView,
|
|
30
|
+
});
|
|
31
|
+
|
|
26
32
|
const BarView = ({ screenStyles, barState = defaultState }: Props) => {
|
|
27
33
|
const isRTL = useIsRTL();
|
|
28
34
|
|
|
@@ -44,14 +50,15 @@ const BarView = ({ screenStyles, barState = defaultState }: Props) => {
|
|
|
44
50
|
);
|
|
45
51
|
|
|
46
52
|
return (
|
|
47
|
-
<
|
|
53
|
+
<SafeAreaViewPlatform
|
|
54
|
+
edges={["top", "left", "right"]}
|
|
48
55
|
style={[style.view, isRTL ? style.rtlStyle : {}]}
|
|
49
|
-
testID="BarView-
|
|
56
|
+
testID="BarView-safeAreaView"
|
|
50
57
|
>
|
|
51
58
|
{backButton}
|
|
52
59
|
<BarTitle title={barState.title} screenStyles={screenStyles} />
|
|
53
60
|
{closeButton}
|
|
54
|
-
</
|
|
61
|
+
</SafeAreaViewPlatform>
|
|
55
62
|
);
|
|
56
63
|
};
|
|
57
64
|
|
|
@@ -46,7 +46,7 @@ describe("BarView", () => {
|
|
|
46
46
|
<BarView screenStyles={screenStyles} barState={barState} />
|
|
47
47
|
);
|
|
48
48
|
|
|
49
|
-
expect(getByTestId("BarView-
|
|
49
|
+
expect(getByTestId("BarView-safeAreaView").props.style).toContainEqual({
|
|
50
50
|
transform: [{ scaleX: -1 }],
|
|
51
51
|
});
|
|
52
52
|
});
|
|
@@ -58,7 +58,7 @@ describe("BarView", () => {
|
|
|
58
58
|
<BarView screenStyles={screenStyles} barState={barState} />
|
|
59
59
|
);
|
|
60
60
|
|
|
61
|
-
expect(getByTestId("BarView-
|
|
61
|
+
expect(getByTestId("BarView-safeAreaView").props.style).not.toContainEqual({
|
|
62
62
|
transform: [{ scaleX: -1 }],
|
|
63
63
|
});
|
|
64
64
|
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { renderHook, act } from "@testing-library/react-native";
|
|
2
|
+
|
|
3
|
+
type DimensionsStore = {
|
|
4
|
+
setState: (
|
|
5
|
+
partial: { dimensions: Record<string, unknown>; ids: string[] },
|
|
6
|
+
replace?: boolean
|
|
7
|
+
) => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const createdStores: DimensionsStore[] = [];
|
|
11
|
+
|
|
12
|
+
jest.mock("zustand", () => {
|
|
13
|
+
const actual = jest.requireActual<typeof import("zustand")>("zustand");
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
create: ((initializer: Parameters<typeof actual.create>[0]) => {
|
|
18
|
+
const store = actual.create(initializer);
|
|
19
|
+
createdStores.push(store);
|
|
20
|
+
|
|
21
|
+
return store;
|
|
22
|
+
}) as typeof actual.create,
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
import { useCachedDimensions } from "../index";
|
|
27
|
+
|
|
28
|
+
const resetStore = () => {
|
|
29
|
+
const store = createdStores[createdStores.length - 1];
|
|
30
|
+
|
|
31
|
+
act(() => {
|
|
32
|
+
store?.setState({ dimensions: {}, ids: [] });
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe("useCachedDimensions", () => {
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
resetStore();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("getCachedDimensions", () => {
|
|
42
|
+
it("returns default dimensions when id has no cached value", () => {
|
|
43
|
+
const { result } = renderHook(() => useCachedDimensions("component-1"));
|
|
44
|
+
|
|
45
|
+
expect(result.current.getCachedDimensions()).toEqual({
|
|
46
|
+
width: undefined,
|
|
47
|
+
height: undefined,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns cached dimensions after setCachedDimensions", () => {
|
|
52
|
+
const { result } = renderHook(() => useCachedDimensions("component-1"));
|
|
53
|
+
const dimensions = { width: 100, height: 200 };
|
|
54
|
+
|
|
55
|
+
act(() => {
|
|
56
|
+
result.current.setCachedDimensions(dimensions);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(result.current.getCachedDimensions()).toEqual(dimensions);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("setCachedDimensions", () => {
|
|
64
|
+
it("updates dimensions for the hook id", () => {
|
|
65
|
+
const { result } = renderHook(() => useCachedDimensions("component-1"));
|
|
66
|
+
|
|
67
|
+
act(() => {
|
|
68
|
+
result.current.setCachedDimensions({ width: 50, height: 75 });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(result.current.getCachedDimensions()).toEqual({
|
|
72
|
+
width: 50,
|
|
73
|
+
height: 75,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("shares cached dimensions between hook instances with the same id", () => {
|
|
78
|
+
const { result: first } = renderHook(() =>
|
|
79
|
+
useCachedDimensions("shared-id")
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const { result: second } = renderHook(() =>
|
|
83
|
+
useCachedDimensions("shared-id")
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
act(() => {
|
|
87
|
+
first.current.setCachedDimensions({ width: 10, height: 20 });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(second.current.getCachedDimensions()).toEqual({
|
|
91
|
+
width: 10,
|
|
92
|
+
height: 20,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("getLastCachedDimensions", () => {
|
|
98
|
+
it("returns undefined when no dimensions have been cached", () => {
|
|
99
|
+
const { result } = renderHook(() => useCachedDimensions("component-1"));
|
|
100
|
+
|
|
101
|
+
expect(result.current.getLastCachedDimensions()).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns the most recently cached dimensions across ids", () => {
|
|
105
|
+
const { result: first } = renderHook(() => useCachedDimensions("first"));
|
|
106
|
+
|
|
107
|
+
const { result: second } = renderHook(() =>
|
|
108
|
+
useCachedDimensions("second")
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
act(() => {
|
|
112
|
+
first.current.setCachedDimensions({ width: 100, height: 100 });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
act(() => {
|
|
116
|
+
second.current.setCachedDimensions({ width: 200, height: 200 });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(first.current.getLastCachedDimensions()).toEqual({
|
|
120
|
+
width: 200,
|
|
121
|
+
height: 200,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("returns the latest dimensions when the same id is updated multiple times", () => {
|
|
126
|
+
const { result } = renderHook(() => useCachedDimensions("component-1"));
|
|
127
|
+
|
|
128
|
+
act(() => {
|
|
129
|
+
result.current.setCachedDimensions({ width: 100, height: 100 });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
act(() => {
|
|
133
|
+
result.current.setCachedDimensions({ width: 300, height: 400 });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(result.current.getLastCachedDimensions()).toEqual({
|
|
137
|
+
width: 300,
|
|
138
|
+
height: 400,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("return value", () => {
|
|
144
|
+
it("exposes getCachedDimensions, setCachedDimensions, and getLastCachedDimensions", () => {
|
|
145
|
+
const { result } = renderHook(() => useCachedDimensions("component-1"));
|
|
146
|
+
|
|
147
|
+
expect(result.current).toEqual({
|
|
148
|
+
getCachedDimensions: expect.any(Function),
|
|
149
|
+
setCachedDimensions: expect.any(Function),
|
|
150
|
+
getLastCachedDimensions: expect.any(Function),
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|