@applicaster/zapp-react-native-ui-components 16.0.0-alpha.5365234026 → 16.0.0-alpha.5402127570
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/PressableView.tsx +27 -7
- package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +0 -1
- package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +32 -21
- package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
- package/Components/MasterCell/hooks/index.ts +2 -0
- package/Components/MasterCell/hooks/usePressedState.ts +4 -0
- package/Components/River/ComponentsMap/ComponentsMap.tsx +103 -186
- package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +22 -1
- package/package.json +5 -5
- package/Components/Layout/FullWidthRow.tsx +0 -38
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import {
|
|
1
|
+
import React, { useCallback } from "react";
|
|
2
|
+
import { Pressable } from "react-native";
|
|
3
|
+
import { PressedStateContext } from "../contexts/PressedStateContext";
|
|
3
4
|
|
|
4
5
|
type Props = {
|
|
5
6
|
children?: React.ReactNode;
|
|
6
7
|
style?: Record<string, unknown>;
|
|
8
|
+
pressedStyle?: Record<string, unknown>;
|
|
7
9
|
testID?: string;
|
|
8
10
|
accessibilityLabel?: string;
|
|
9
11
|
accessibilityHint?: string;
|
|
10
12
|
onPress?: () => void;
|
|
13
|
+
testOnly_pressed?: boolean;
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
export function PressableView({
|
|
@@ -16,19 +19,36 @@ export function PressableView({
|
|
|
16
19
|
testID,
|
|
17
20
|
accessibilityLabel,
|
|
18
21
|
accessibilityHint,
|
|
22
|
+
pressedStyle,
|
|
19
23
|
onPress,
|
|
24
|
+
testOnly_pressed = false,
|
|
20
25
|
}: Props) {
|
|
21
26
|
return (
|
|
22
|
-
<
|
|
23
|
-
|
|
27
|
+
<Pressable
|
|
28
|
+
testOnly_pressed={testOnly_pressed}
|
|
24
29
|
onPress={onPress}
|
|
25
30
|
testID={testID}
|
|
26
31
|
accessibilityLabel={accessibilityLabel}
|
|
27
32
|
accessibilityHint={accessibilityHint}
|
|
28
33
|
accessible={!!(testID || accessibilityLabel)}
|
|
29
|
-
style={
|
|
34
|
+
style={useCallback(
|
|
35
|
+
({ pressed }) => [
|
|
36
|
+
style,
|
|
37
|
+
pressed && {
|
|
38
|
+
opacity: 1,
|
|
39
|
+
},
|
|
40
|
+
pressed && pressedStyle,
|
|
41
|
+
],
|
|
42
|
+
[style, pressedStyle]
|
|
43
|
+
)}
|
|
30
44
|
>
|
|
31
|
-
{
|
|
32
|
-
|
|
45
|
+
{({ pressed }) => {
|
|
46
|
+
return (
|
|
47
|
+
<PressedStateContext.Provider value={pressed}>
|
|
48
|
+
{children}
|
|
49
|
+
</PressedStateContext.Provider>
|
|
50
|
+
);
|
|
51
|
+
}}
|
|
52
|
+
</Pressable>
|
|
33
53
|
);
|
|
34
54
|
}
|
|
@@ -83,6 +83,10 @@ export const useTextLabel = ({ label, entry }): string => {
|
|
|
83
83
|
return prepareHebrewText(label, isRTL);
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* use `otherProps.state` to set focused or default styles (focusedStyles or normalStyles) to the text component
|
|
88
|
+
* for mobile buttons state using usePressedState() and passing isPressed to `state`
|
|
89
|
+
* */
|
|
86
90
|
export const withFocusedStyles = ({ style, otherProps }) => {
|
|
87
91
|
const state = R.path(["state"], otherProps);
|
|
88
92
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useCallback } from "react";
|
|
2
2
|
import { PressableView } from "../../PressableView";
|
|
3
|
+
import { usePressedState } from "../../../hooks";
|
|
3
4
|
|
|
4
5
|
import { ActionButtonController } from "../../ActionButtonsCore/components";
|
|
5
6
|
|
|
@@ -20,7 +21,137 @@ const isValidElement = (
|
|
|
20
21
|
): child is React.ReactElement<ChildElementProps> =>
|
|
21
22
|
React.isValidElement(child);
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
type ActionButtonContentProps = {
|
|
25
|
+
style: any;
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
action: any;
|
|
28
|
+
focusedStyles: any;
|
|
29
|
+
entry: any;
|
|
30
|
+
props: Record<string, any>;
|
|
31
|
+
actionContext: any;
|
|
32
|
+
actionState: any;
|
|
33
|
+
isActive: boolean;
|
|
34
|
+
onPress: () => any;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const ActionButtonContent = ({
|
|
38
|
+
children,
|
|
39
|
+
action,
|
|
40
|
+
entry,
|
|
41
|
+
actionContext,
|
|
42
|
+
actionState,
|
|
43
|
+
isActive,
|
|
44
|
+
}: ActionButtonContentProps) => {
|
|
45
|
+
const isPressed = usePressedState();
|
|
46
|
+
|
|
47
|
+
const supportsEntryState =
|
|
48
|
+
typeof actionContext?.initialEntryState === "function";
|
|
49
|
+
|
|
50
|
+
const shouldRenderAsset = Boolean(
|
|
51
|
+
supportsEntryState ? actionState?.mobileButtonAssets : false
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const shouldRenderLabel = Boolean(
|
|
55
|
+
supportsEntryState && resolveLabelText(actionState?.label)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const _cloneChildrenWithState = useCallback(
|
|
59
|
+
(nodes?: React.ReactNode): React.ReactNode => {
|
|
60
|
+
return React.Children.map(nodes, (child) => {
|
|
61
|
+
if (!isValidElement(child)) {
|
|
62
|
+
return child;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const role = child.props.mobileActionRole;
|
|
66
|
+
|
|
67
|
+
const nextChildren = _cloneChildrenWithState(child.props.children);
|
|
68
|
+
|
|
69
|
+
// Prevents of the asset rendering when the action doesn't provide asset for current entry state
|
|
70
|
+
if (role === "asset" && !shouldRenderAsset) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Prevents of the label rendering when the action doesn't provide label for current entry state.
|
|
75
|
+
// Also prevents of the label container when it doesn't have children to avoid unnecessary empty space.
|
|
76
|
+
if (role === "label" && !shouldRenderLabel) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Prevents of the label container rendering when it doesn't have children to avoid unnecessary empty space.
|
|
81
|
+
if (
|
|
82
|
+
role === "label_container" &&
|
|
83
|
+
React.Children.count(nextChildren) === 0
|
|
84
|
+
) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const nextProps: Partial<ChildElementProps> = {};
|
|
89
|
+
|
|
90
|
+
// Inject asset or uri to Asset component with role asset
|
|
91
|
+
if (role === "asset") {
|
|
92
|
+
const resolvedAsset = selectByAssetFlavour(
|
|
93
|
+
actionState,
|
|
94
|
+
action.flavour,
|
|
95
|
+
isActive
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (typeof resolvedAsset === "function") {
|
|
99
|
+
const AssetComponent = resolvedAsset as CellActionAssetComponent;
|
|
100
|
+
|
|
101
|
+
nextProps.asset = (
|
|
102
|
+
<AssetComponent
|
|
103
|
+
flavour={action.flavour || "flavour_1"}
|
|
104
|
+
width={action.width}
|
|
105
|
+
height={action.height}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
} else {
|
|
109
|
+
nextProps.uri = resolvedAsset;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Inject state and entry to Text component with role label
|
|
114
|
+
// Use isPressed state for mobile buttons to set focused state on mobile
|
|
115
|
+
if (role === "label") {
|
|
116
|
+
nextProps.state = isActive || isPressed ? "focused" : "default";
|
|
117
|
+
nextProps.entry = entry;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (nextChildren !== child.props.children) {
|
|
121
|
+
nextProps.children = nextChildren;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return React.cloneElement(child, nextProps);
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
[
|
|
128
|
+
shouldRenderAsset,
|
|
129
|
+
shouldRenderLabel,
|
|
130
|
+
actionState,
|
|
131
|
+
action.flavour,
|
|
132
|
+
action.width,
|
|
133
|
+
action.height,
|
|
134
|
+
isActive,
|
|
135
|
+
isPressed,
|
|
136
|
+
entry,
|
|
137
|
+
]
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (!shouldRenderAsset && !shouldRenderLabel) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return _cloneChildrenWithState(children);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const ActionButton = ({
|
|
148
|
+
style,
|
|
149
|
+
children,
|
|
150
|
+
action,
|
|
151
|
+
focusedStyles,
|
|
152
|
+
entry,
|
|
153
|
+
...props
|
|
154
|
+
}) => {
|
|
24
155
|
return (
|
|
25
156
|
<ActionButtonController
|
|
26
157
|
action={action}
|
|
@@ -31,105 +162,28 @@ export const ActionButton = ({ style, children, action, entry, ...props }) => {
|
|
|
31
162
|
);
|
|
32
163
|
}}
|
|
33
164
|
>
|
|
34
|
-
{(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return child;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const role = child.props.mobileActionRole;
|
|
55
|
-
|
|
56
|
-
const nextChildren = _cloneChildrenWithState(child.props.children);
|
|
57
|
-
|
|
58
|
-
// Prevents of the asset rendering when the action doesn't provide asset for current entry state
|
|
59
|
-
if (role === "asset" && !shouldRenderAsset) {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Prevents of the label rendering when the action doesn't provide label for current entry state.
|
|
64
|
-
// Also prevents of the label container when it doesn't have children to avoid unnecessary empty space.
|
|
65
|
-
if (role === "label" && !shouldRenderLabel) {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Prevents of the label container rendering when it doesn't have children to avoid unnecessary empty space.
|
|
70
|
-
if (
|
|
71
|
-
role === "label_container" &&
|
|
72
|
-
React.Children.count(nextChildren) === 0
|
|
73
|
-
) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const nextProps: Partial<ChildElementProps> = {};
|
|
78
|
-
|
|
79
|
-
// Inject asset or uri to Asset component with role asset
|
|
80
|
-
if (role === "asset") {
|
|
81
|
-
const resolvedAsset = selectByAssetFlavour(
|
|
82
|
-
actionState,
|
|
83
|
-
action.flavour,
|
|
84
|
-
isActive
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
if (typeof resolvedAsset === "function") {
|
|
88
|
-
const AssetComponent =
|
|
89
|
-
resolvedAsset as CellActionAssetComponent;
|
|
90
|
-
|
|
91
|
-
nextProps.asset = (
|
|
92
|
-
<AssetComponent
|
|
93
|
-
flavour={action.flavour || "flavour_1"}
|
|
94
|
-
width={action.width}
|
|
95
|
-
height={action.height}
|
|
96
|
-
/>
|
|
97
|
-
);
|
|
98
|
-
} else {
|
|
99
|
-
nextProps.uri = resolvedAsset;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Inject state and entry to Text component with role label
|
|
104
|
-
if (role === "label") {
|
|
105
|
-
nextProps.state = isActive ? "focused" : "default";
|
|
106
|
-
nextProps.entry = entry;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (nextChildren !== child.props.children) {
|
|
110
|
-
nextProps.children = nextChildren;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return React.cloneElement(child, nextProps);
|
|
114
|
-
});
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
if (!shouldRenderAsset && !shouldRenderLabel) {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return (
|
|
122
|
-
<PressableView
|
|
123
|
-
testID={props.testID || `${entry?.id}`}
|
|
124
|
-
style={isActive ? { ...style, ...props.focusedStyles } : style}
|
|
125
|
-
onPress={onPress}
|
|
126
|
-
accessibilityLabel={props.accessibilityLabel || `${entry?.id}`}
|
|
127
|
-
accessibilityHint={props.accessibilityHint}
|
|
165
|
+
{(renderProps) => (
|
|
166
|
+
<PressableView
|
|
167
|
+
testOnly_pressed={props.testOnly_pressed}
|
|
168
|
+
testID={props.testID || `${entry?.id}`}
|
|
169
|
+
style={style}
|
|
170
|
+
pressedStyle={focusedStyles}
|
|
171
|
+
onPress={renderProps.onPress}
|
|
172
|
+
accessibilityLabel={props.accessibilityLabel || `${entry?.id}`}
|
|
173
|
+
accessibilityHint={props.accessibilityHint}
|
|
174
|
+
>
|
|
175
|
+
<ActionButtonContent
|
|
176
|
+
style={style}
|
|
177
|
+
action={action}
|
|
178
|
+
focusedStyles={focusedStyles}
|
|
179
|
+
entry={entry}
|
|
180
|
+
props={props}
|
|
181
|
+
{...renderProps}
|
|
128
182
|
>
|
|
129
|
-
{
|
|
130
|
-
</
|
|
131
|
-
|
|
132
|
-
}
|
|
183
|
+
{children}
|
|
184
|
+
</ActionButtonContent>
|
|
185
|
+
</PressableView>
|
|
186
|
+
)}
|
|
133
187
|
</ActionButtonController>
|
|
134
188
|
);
|
|
135
189
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Text as RNText,
|
|
2
|
+
import { Pressable, Text as RNText, View } from "react-native";
|
|
3
3
|
import { render, fireEvent } from "@testing-library/react-native";
|
|
4
4
|
import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
|
|
5
5
|
|
|
@@ -8,6 +8,11 @@ import { elementMapper } from "../../../../elementMapper";
|
|
|
8
8
|
import { defaultComponents } from "../../../index";
|
|
9
9
|
import { MobileActionButtons } from "..";
|
|
10
10
|
import { AssetComponent } from "../AssetComponent";
|
|
11
|
+
import { usePressedState } from "../../../../hooks/usePressedState";
|
|
12
|
+
|
|
13
|
+
jest.mock("../../../../hooks/usePressedState", () => ({
|
|
14
|
+
usePressedState: jest.fn(),
|
|
15
|
+
}));
|
|
11
16
|
|
|
12
17
|
jest.mock("@applicaster/zapp-react-native-utils/reactHooks/actions", () => ({
|
|
13
18
|
useActions: jest.fn(),
|
|
@@ -136,6 +141,7 @@ const baseNode = {
|
|
|
136
141
|
borderColor: "rgba(4,4,4,1)",
|
|
137
142
|
},
|
|
138
143
|
testID: "mobile-action-button",
|
|
144
|
+
testOnly_pressed: false,
|
|
139
145
|
},
|
|
140
146
|
elements: [
|
|
141
147
|
{
|
|
@@ -190,14 +196,20 @@ const baseNode = {
|
|
|
190
196
|
describe("MobileActionButton", () => {
|
|
191
197
|
beforeEach(() => {
|
|
192
198
|
jest.clearAllMocks();
|
|
199
|
+
(usePressedState as jest.Mock).mockReturnValue(false);
|
|
193
200
|
});
|
|
194
201
|
|
|
195
|
-
const renderNode = (node = baseNode) =>
|
|
196
|
-
|
|
202
|
+
const renderNode = ({ node = baseNode, pressed = false } = {}) => {
|
|
203
|
+
if (pressed) {
|
|
204
|
+
node.props.testOnly_pressed = true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return render(
|
|
197
208
|
<React.Fragment>
|
|
198
209
|
{elementMapper(defaultComponents)(node as never)}
|
|
199
210
|
</React.Fragment>
|
|
200
211
|
);
|
|
212
|
+
};
|
|
201
213
|
|
|
202
214
|
it("renders nested image and text children through elementMapper", () => {
|
|
203
215
|
mockUseActions.mockReturnValue(buildActionContext());
|
|
@@ -210,12 +222,13 @@ describe("MobileActionButton", () => {
|
|
|
210
222
|
});
|
|
211
223
|
|
|
212
224
|
it("applies focused styles when action entry state is active", () => {
|
|
225
|
+
(usePressedState as jest.Mock).mockReturnValue(true);
|
|
213
226
|
mockUseActions.mockReturnValue(buildActionContext({ active: true }));
|
|
214
227
|
|
|
215
|
-
const { getByTestId, getByText } = renderNode();
|
|
228
|
+
const { getByTestId, getByText } = renderNode({ pressed: true });
|
|
216
229
|
|
|
217
230
|
expect(
|
|
218
|
-
getByTestId("mobile-action-button").props.style.backgroundColor
|
|
231
|
+
getByTestId("mobile-action-button").props.style[2].backgroundColor
|
|
219
232
|
).toBe("rgba(3,3,3,1)");
|
|
220
233
|
|
|
221
234
|
expect(getByText("Play").props.style).toEqual(
|
|
@@ -253,6 +266,8 @@ describe("MobileActionButton", () => {
|
|
|
253
266
|
})
|
|
254
267
|
);
|
|
255
268
|
|
|
269
|
+
node.props.testOnly_pressed = false;
|
|
270
|
+
|
|
256
271
|
const { UNSAFE_getAllByType, UNSAFE_getByType, getByTestId, getByText } =
|
|
257
272
|
render(
|
|
258
273
|
<React.Fragment>
|
|
@@ -260,14 +275,14 @@ describe("MobileActionButton", () => {
|
|
|
260
275
|
</React.Fragment>
|
|
261
276
|
);
|
|
262
277
|
|
|
263
|
-
const pressable = UNSAFE_getByType(
|
|
278
|
+
const pressable = UNSAFE_getByType(Pressable);
|
|
264
279
|
const label = getByText("Play");
|
|
265
280
|
const asset = getByTestId("mobile_action_button_1-asset");
|
|
266
281
|
|
|
267
282
|
expect(pressable.props.testID).toBe("mobile_action_button_1");
|
|
268
283
|
expect(pressable.props.accessibilityLabel).toBe("entry-1");
|
|
269
284
|
|
|
270
|
-
expect(pressable.props.style).toMatchObject({
|
|
285
|
+
expect(pressable.props.style({ pressed: false })[0]).toMatchObject({
|
|
271
286
|
backgroundColor: "rgba(1,1,1,1)",
|
|
272
287
|
borderColor: "rgba(2,2,2,1)",
|
|
273
288
|
borderWidth: 1,
|
|
@@ -278,15 +293,11 @@ describe("MobileActionButton", () => {
|
|
|
278
293
|
height: 24,
|
|
279
294
|
});
|
|
280
295
|
|
|
281
|
-
expect(label.props.style).
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
lineHeight: 24,
|
|
287
|
-
}),
|
|
288
|
-
])
|
|
289
|
-
);
|
|
296
|
+
expect(label.props.style[0]).toMatchObject({
|
|
297
|
+
color: "rgba(10,10,10,1)",
|
|
298
|
+
fontSize: 15,
|
|
299
|
+
lineHeight: 24,
|
|
300
|
+
});
|
|
290
301
|
|
|
291
302
|
expect(UNSAFE_getAllByType(View).length).toBeGreaterThan(0);
|
|
292
303
|
expect(UNSAFE_getAllByType(RNText).length).toBeGreaterThan(0);
|
|
@@ -324,7 +335,7 @@ describe("MobileActionButton", () => {
|
|
|
324
335
|
buildActionContext({ mobileButtonAssets: MockAssetComponent })
|
|
325
336
|
);
|
|
326
337
|
|
|
327
|
-
const { getByTestId } = renderNode(nodeWithFlavour1);
|
|
338
|
+
const { getByTestId } = renderNode({ node: nodeWithFlavour1 });
|
|
328
339
|
|
|
329
340
|
expect(MockAssetComponent).toHaveBeenCalled();
|
|
330
341
|
expect(getByTestId("mobile-action-button-asset")).toBeTruthy();
|
|
@@ -339,7 +350,7 @@ describe("MobileActionButton", () => {
|
|
|
339
350
|
buildActionContext({ mobileButtonAssets: MockAssetComponent })
|
|
340
351
|
);
|
|
341
352
|
|
|
342
|
-
renderNode(nodeWithFlavour1);
|
|
353
|
+
renderNode({ node: nodeWithFlavour1 });
|
|
343
354
|
|
|
344
355
|
expect(MockAssetComponent).toHaveBeenCalledWith(
|
|
345
356
|
expect.objectContaining({ flavour: "flavour_1" }),
|
|
@@ -356,7 +367,7 @@ describe("MobileActionButton", () => {
|
|
|
356
367
|
buildActionContext({ mobileButtonAssets: MockAssetComponent })
|
|
357
368
|
);
|
|
358
369
|
|
|
359
|
-
renderNode(nodeWithFlavour2);
|
|
370
|
+
renderNode({ node: nodeWithFlavour2 });
|
|
360
371
|
|
|
361
372
|
expect(MockAssetComponent).toHaveBeenCalledWith(
|
|
362
373
|
expect.objectContaining({ flavour: "flavour_2" }),
|
|
@@ -369,7 +380,7 @@ describe("MobileActionButton", () => {
|
|
|
369
380
|
buildActionContext({ mobileButtonAssets: undefined })
|
|
370
381
|
);
|
|
371
382
|
|
|
372
|
-
const { queryByTestId } = renderNode(nodeWithFlavour1);
|
|
383
|
+
const { queryByTestId } = renderNode({ node: nodeWithFlavour1 });
|
|
373
384
|
|
|
374
385
|
expect(queryByTestId("mobile-action-button-asset")).toBeNull();
|
|
375
386
|
});
|
|
@@ -385,7 +396,7 @@ describe("MobileActionButton", () => {
|
|
|
385
396
|
})
|
|
386
397
|
);
|
|
387
398
|
|
|
388
|
-
const { getByTestId } = renderNode(nodeWithFlavour1);
|
|
399
|
+
const { getByTestId } = renderNode({ node: nodeWithFlavour1 });
|
|
389
400
|
|
|
390
401
|
expect(getByTestId("mobile-action-button-asset")).toBeTruthy();
|
|
391
402
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import * as R from "ramda";
|
|
3
|
-
import { FlatList,
|
|
3
|
+
import { FlatList, StyleSheet, View } from "react-native";
|
|
4
4
|
import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
|
|
5
5
|
import { RiverItem } from "../RiverItem";
|
|
6
6
|
import { RiverFooter } from "../RiverFooter";
|
|
@@ -36,16 +36,18 @@ type Props = {
|
|
|
36
36
|
groupId?: string;
|
|
37
37
|
isScreenWrappedInContainer?: boolean;
|
|
38
38
|
riverComponents: ZappUIComponent[];
|
|
39
|
-
scrollViewExtraProps?:
|
|
39
|
+
scrollViewExtraProps?: {};
|
|
40
40
|
riverId?: string;
|
|
41
|
-
getStaticComponentFeed:
|
|
42
|
-
component: ZappUIComponent;
|
|
43
|
-
index: number;
|
|
44
|
-
}) => ZappFeed | Promise<ZappFeed>;
|
|
41
|
+
getStaticComponentFeed: any;
|
|
45
42
|
stickyHeaderIndices?: number[];
|
|
46
|
-
useScrollView?: boolean;
|
|
47
43
|
};
|
|
48
44
|
|
|
45
|
+
const styles = StyleSheet.create({
|
|
46
|
+
container: {
|
|
47
|
+
flex: 1,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
49
51
|
const getNearestValue = (value, optionA, optionB) =>
|
|
50
52
|
Math.abs(value - optionA) < Math.abs(value - optionB) ? optionA : optionB;
|
|
51
53
|
|
|
@@ -62,14 +64,11 @@ function ComponentsMapComponent(props: Props) {
|
|
|
62
64
|
riverId,
|
|
63
65
|
getStaticComponentFeed,
|
|
64
66
|
stickyHeaderIndices,
|
|
65
|
-
useScrollView,
|
|
66
67
|
} = props;
|
|
67
68
|
|
|
68
69
|
const flatListRef = React.useRef<FlatList | null>(null);
|
|
69
|
-
const scrollViewRef = React.useRef<ScrollView | null>(null);
|
|
70
70
|
const flatListWrapperRef = React.useRef<View | null>(null);
|
|
71
71
|
const hasUserScrolledRef = React.useRef(false);
|
|
72
|
-
const scrollEndReachedRef = React.useRef(false);
|
|
73
72
|
const screenConfig = useScreenConfiguration(riverId);
|
|
74
73
|
const screenData = useScreenData(riverId);
|
|
75
74
|
const pullToRefreshEnabled = screenData?.rules?.pull_to_refresh_enabled;
|
|
@@ -88,7 +87,7 @@ function ComponentsMapComponent(props: Props) {
|
|
|
88
87
|
|
|
89
88
|
const onLoadDone = React.useCallback(() => {
|
|
90
89
|
logTimestamp(riverId?.toString());
|
|
91
|
-
}, [logTimestamp
|
|
90
|
+
}, [logTimestamp]);
|
|
92
91
|
|
|
93
92
|
const { loadingState, onLoadFinished, onLoadFailed, shouldShowLoadingError } =
|
|
94
93
|
useLoadingState(riverComponents.length, onLoadDone);
|
|
@@ -117,17 +116,7 @@ function ComponentsMapComponent(props: Props) {
|
|
|
117
116
|
</ScreenLoadingMeasurementsListItemWrapper>
|
|
118
117
|
);
|
|
119
118
|
},
|
|
120
|
-
[
|
|
121
|
-
feed,
|
|
122
|
-
getStaticComponentFeed,
|
|
123
|
-
groupId,
|
|
124
|
-
isScreenWrappedInContainer,
|
|
125
|
-
loadingState,
|
|
126
|
-
onLoadFailed,
|
|
127
|
-
onLoadFinished,
|
|
128
|
-
riverComponents.length,
|
|
129
|
-
riverId,
|
|
130
|
-
]
|
|
119
|
+
[feed, getStaticComponentFeed, onLoadFailed, onLoadFinished]
|
|
131
120
|
);
|
|
132
121
|
|
|
133
122
|
const screenStyle = React.useMemo(
|
|
@@ -149,13 +138,7 @@ function ComponentsMapComponent(props: Props) {
|
|
|
149
138
|
R.prop("screen_padding_right")(theme)
|
|
150
139
|
),
|
|
151
140
|
}),
|
|
152
|
-
[
|
|
153
|
-
screenConfig.paddingTop,
|
|
154
|
-
screenConfig.paddingBottom,
|
|
155
|
-
screenConfig.paddingLeft,
|
|
156
|
-
screenConfig.paddingRight,
|
|
157
|
-
theme,
|
|
158
|
-
]
|
|
141
|
+
[riverId]
|
|
159
142
|
);
|
|
160
143
|
|
|
161
144
|
const handleOnLayout = React.useCallback(
|
|
@@ -190,50 +173,55 @@ function ComponentsMapComponent(props: Props) {
|
|
|
190
173
|
canMomentum.current = true;
|
|
191
174
|
}, []);
|
|
192
175
|
|
|
193
|
-
/*
|
|
194
|
-
const
|
|
195
|
-
(
|
|
196
|
-
|
|
176
|
+
/* onMomentumScrollEnd: Handling snap to expanded/collapsed state ( for navbar ) */
|
|
177
|
+
const _onMomentumScrollEnd = React.useCallback(({ nativeEvent }) => {
|
|
178
|
+
if (!canMomentum.current) return;
|
|
179
|
+
canMomentum.current = false;
|
|
197
180
|
|
|
198
|
-
|
|
199
|
-
if (scrollState === 0 || scrollState === -headerHeight) return;
|
|
200
|
-
if (!flatListRef.current) return;
|
|
181
|
+
const { height: headerHeight, scrollState } = navBarStore.getState();
|
|
201
182
|
|
|
202
|
-
|
|
183
|
+
const offsetY = Math.max(0, nativeEvent.contentOffset.y);
|
|
203
184
|
|
|
204
|
-
|
|
185
|
+
if (
|
|
186
|
+
!(scrollState === 0 || scrollState === -headerHeight) &&
|
|
187
|
+
flatListRef.current
|
|
188
|
+
) {
|
|
189
|
+
const offset =
|
|
205
190
|
getNearestValue(Math.round(scrollState), -headerHeight, 0) ===
|
|
206
|
-
-headerHeight
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
191
|
+
-headerHeight
|
|
192
|
+
? Math.round(offsetY + scrollState + headerHeight)
|
|
193
|
+
: Math.round(offsetY + scrollState);
|
|
194
|
+
|
|
195
|
+
flatListRef.current.scrollToOffset({
|
|
196
|
+
animated: true,
|
|
197
|
+
offset,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}, []);
|
|
211
201
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
);
|
|
202
|
+
/* onScrollEnd: Handling snap to expanded/collapsed state ( for navbar ) */
|
|
203
|
+
const _onScrollEndDrag = React.useCallback(({ nativeEvent }) => {
|
|
204
|
+
const { height: headerHeight, scrollState } = navBarStore.getState();
|
|
216
205
|
|
|
217
|
-
|
|
218
|
-
const _onMomentumScrollEnd = React.useCallback(
|
|
219
|
-
({ nativeEvent }) => {
|
|
220
|
-
if (!canMomentum.current) return;
|
|
221
|
-
canMomentum.current = false;
|
|
222
|
-
snapToHeaderState(nativeEvent.contentOffset.y);
|
|
223
|
-
},
|
|
224
|
-
[snapToHeaderState]
|
|
225
|
-
);
|
|
206
|
+
const offsetY = Math.max(0, nativeEvent.contentOffset.y);
|
|
226
207
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
208
|
+
if (
|
|
209
|
+
!(scrollState === 0 || scrollState === -headerHeight) &&
|
|
210
|
+
flatListRef.current &&
|
|
211
|
+
nativeEvent.velocity.y === 0
|
|
212
|
+
) {
|
|
213
|
+
const offset =
|
|
214
|
+
getNearestValue(Math.round(scrollState), -headerHeight, 0) ===
|
|
215
|
+
-headerHeight
|
|
216
|
+
? Math.round(offsetY + scrollState + headerHeight)
|
|
217
|
+
: Math.round(offsetY + scrollState);
|
|
218
|
+
|
|
219
|
+
flatListRef.current.scrollToOffset({
|
|
220
|
+
animated: true,
|
|
221
|
+
offset,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}, []);
|
|
237
225
|
|
|
238
226
|
const onScroll = React.useCallback((event) => {
|
|
239
227
|
hasUserScrolledRef.current = true;
|
|
@@ -257,42 +245,8 @@ function ComponentsMapComponent(props: Props) {
|
|
|
257
245
|
}
|
|
258
246
|
}, []);
|
|
259
247
|
|
|
260
|
-
const onScrollWithEndReached = React.useCallback(
|
|
261
|
-
(event) => {
|
|
262
|
-
onScroll(event);
|
|
263
|
-
|
|
264
|
-
if (!hasUserScrolledRef.current) return;
|
|
265
|
-
if (isScreenWrappedInContainer) return;
|
|
266
|
-
|
|
267
|
-
const {
|
|
268
|
-
nativeEvent: {
|
|
269
|
-
contentOffset: { y },
|
|
270
|
-
layoutMeasurement: { height },
|
|
271
|
-
contentSize: { height: contentHeight },
|
|
272
|
-
},
|
|
273
|
-
} = event;
|
|
274
|
-
|
|
275
|
-
const distanceFromEnd = contentHeight - y - height;
|
|
276
|
-
|
|
277
|
-
if (distanceFromEnd <= height * 0.5) {
|
|
278
|
-
if (!scrollEndReachedRef.current) {
|
|
279
|
-
scrollEndReachedRef.current = true;
|
|
280
|
-
emitScrollEndReached();
|
|
281
|
-
}
|
|
282
|
-
} else {
|
|
283
|
-
scrollEndReachedRef.current = false;
|
|
284
|
-
}
|
|
285
|
-
},
|
|
286
|
-
[onScroll, isScreenWrappedInContainer]
|
|
287
|
-
);
|
|
288
|
-
|
|
289
248
|
const contentContainerStyle = React.useMemo(
|
|
290
|
-
() =>
|
|
291
|
-
isScreenWrappedInContainer
|
|
292
|
-
? {
|
|
293
|
-
flexGrow: 1,
|
|
294
|
-
}
|
|
295
|
-
: screenStyle,
|
|
249
|
+
() => (isScreenWrappedInContainer ? {} : screenStyle),
|
|
296
250
|
[isScreenWrappedInContainer, screenStyle]
|
|
297
251
|
);
|
|
298
252
|
|
|
@@ -303,98 +257,61 @@ function ComponentsMapComponent(props: Props) {
|
|
|
303
257
|
// TODO: support both "isScreenWrappedInContainer" and hasScreenPicker().
|
|
304
258
|
// The Screen Picker in Mobile is completly different than the TV
|
|
305
259
|
// so the various offsets / margins in TV do not apply here.
|
|
306
|
-
// Fix for WebView rerender crashes on Android API 28+
|
|
307
|
-
// https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468
|
|
308
|
-
const sharedScrollProps = {
|
|
309
|
-
overScrollMode: isAndroid ? ("never" as const) : ("auto" as const),
|
|
310
|
-
scrollIndicatorInsets,
|
|
311
|
-
onLayout: handleOnLayout,
|
|
312
|
-
contentContainerStyle,
|
|
313
|
-
refreshControl,
|
|
314
|
-
onScrollBeginDrag,
|
|
315
|
-
onMomentumScrollEnd: _onMomentumScrollEnd,
|
|
316
|
-
onScrollEndDrag: _onScrollEndDrag,
|
|
317
|
-
scrollEventThrottle: 16,
|
|
318
|
-
...scrollViewExtraProps,
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
const listContent = useScrollView ? (
|
|
322
|
-
<ScrollView
|
|
323
|
-
testID="components-map-scroll-view"
|
|
324
|
-
// ViewportTracker clones this child and calls its `ref` as a function
|
|
325
|
-
// (childrenRef.current.ref(node)), so a callback ref is required here
|
|
326
|
-
// — an object ref would throw "ref is not a function".
|
|
327
|
-
ref={(ref) => {
|
|
328
|
-
scrollViewRef.current = ref;
|
|
329
|
-
}}
|
|
330
|
-
onScroll={onScrollWithEndReached}
|
|
331
|
-
{...sharedScrollProps}
|
|
332
|
-
>
|
|
333
|
-
{riverComponents.map((item, index) => (
|
|
334
|
-
<React.Fragment key={keyExtractor(item)}>
|
|
335
|
-
{renderRiverItem({ item, index })}
|
|
336
|
-
</React.Fragment>
|
|
337
|
-
))}
|
|
338
|
-
<RiverFooter
|
|
339
|
-
flatListHeight={flatListHeight}
|
|
340
|
-
loadingState={loadingState}
|
|
341
|
-
/>
|
|
342
|
-
</ScrollView>
|
|
343
|
-
) : (
|
|
344
|
-
<FlatList
|
|
345
|
-
testID="components-map-flat-list"
|
|
346
|
-
// ViewportTracker clones this child and calls its `ref` as a function
|
|
347
|
-
// (childrenRef.current.ref(node)), so a callback ref is required here
|
|
348
|
-
// — an object ref would throw "ref is not a function".
|
|
349
|
-
ref={(ref) => {
|
|
350
|
-
flatListRef.current = ref;
|
|
351
|
-
}}
|
|
352
|
-
extraData={feed}
|
|
353
|
-
stickyHeaderIndices={stickyHeaderIndices}
|
|
354
|
-
removeClippedSubviews={isAndroid}
|
|
355
|
-
initialNumToRender={3}
|
|
356
|
-
maxToRenderPerBatch={10}
|
|
357
|
-
windowSize={12}
|
|
358
|
-
keyExtractor={keyExtractor}
|
|
359
|
-
renderItem={renderRiverItem}
|
|
360
|
-
data={riverComponents}
|
|
361
|
-
ListFooterComponent={
|
|
362
|
-
<RiverFooter
|
|
363
|
-
flatListHeight={flatListHeight}
|
|
364
|
-
loadingState={loadingState}
|
|
365
|
-
/>
|
|
366
|
-
}
|
|
367
|
-
onScroll={onScroll}
|
|
368
|
-
onEndReached={
|
|
369
|
-
/* TODO: end-reached detection is inconsistent between the two paths:
|
|
370
|
-
the ScrollView branch uses manual pixel-offset math (onScrollWithEndReached)
|
|
371
|
-
with a scrollEndReachedRef debounce, while this FlatList relies on the
|
|
372
|
-
native onEndReached. Consider unifying the threshold/debounce behavior. */
|
|
373
|
-
/* When wrapped in a parent ScrollView (e.g. tabs),
|
|
374
|
-
this FlatList doesn't scroll so onEndReached can fire repeatedly;
|
|
375
|
-
skip it here and let the parent ScrollView emit scroll-end instead. */
|
|
376
|
-
isScreenWrappedInContainer
|
|
377
|
-
? undefined
|
|
378
|
-
: () => {
|
|
379
|
-
if (!hasUserScrolledRef.current) return;
|
|
380
|
-
emitScrollEndReached();
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
{...sharedScrollProps}
|
|
384
|
-
/>
|
|
385
|
-
);
|
|
386
|
-
|
|
387
260
|
return (
|
|
388
|
-
|
|
389
|
-
// for measuring the list's position/height; it intentionally has no style.
|
|
390
|
-
<View ref={flatListWrapperRef}>
|
|
261
|
+
<View style={styles.container} ref={flatListWrapperRef}>
|
|
391
262
|
<ComponentsMapHeightContext.Provider value={flatListHeight}>
|
|
392
263
|
<ComponentsMapRefContext.Provider value={flatListWrapperRef}>
|
|
393
264
|
<ScreenLoadingMeasurements
|
|
394
265
|
riverId={riverId}
|
|
395
266
|
numberOfComponents={riverComponents.length}
|
|
396
267
|
>
|
|
397
|
-
<ViewportTracker>
|
|
268
|
+
<ViewportTracker>
|
|
269
|
+
<FlatList
|
|
270
|
+
testID="components-map-flat-list"
|
|
271
|
+
ref={(ref) => {
|
|
272
|
+
flatListRef.current = ref;
|
|
273
|
+
}}
|
|
274
|
+
// Fix for WebView rerender crashes on Android API 28+
|
|
275
|
+
// https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-964035468
|
|
276
|
+
overScrollMode={isAndroid ? "never" : "auto"}
|
|
277
|
+
scrollIndicatorInsets={scrollIndicatorInsets}
|
|
278
|
+
extraData={feed}
|
|
279
|
+
stickyHeaderIndices={stickyHeaderIndices}
|
|
280
|
+
removeClippedSubviews={isAndroid}
|
|
281
|
+
onLayout={handleOnLayout}
|
|
282
|
+
initialNumToRender={3}
|
|
283
|
+
maxToRenderPerBatch={10}
|
|
284
|
+
windowSize={12}
|
|
285
|
+
keyExtractor={keyExtractor}
|
|
286
|
+
renderItem={renderRiverItem}
|
|
287
|
+
data={riverComponents}
|
|
288
|
+
contentContainerStyle={contentContainerStyle}
|
|
289
|
+
ListFooterComponent={
|
|
290
|
+
<RiverFooter
|
|
291
|
+
flatListHeight={flatListHeight}
|
|
292
|
+
loadingState={loadingState}
|
|
293
|
+
/>
|
|
294
|
+
}
|
|
295
|
+
refreshControl={refreshControl}
|
|
296
|
+
onScrollBeginDrag={onScrollBeginDrag}
|
|
297
|
+
onScroll={onScroll}
|
|
298
|
+
onMomentumScrollEnd={_onMomentumScrollEnd}
|
|
299
|
+
onScrollEndDrag={_onScrollEndDrag}
|
|
300
|
+
scrollEventThrottle={16}
|
|
301
|
+
{...scrollViewExtraProps}
|
|
302
|
+
onEndReached={
|
|
303
|
+
/* When wrapped in a parent ScrollView (e.g. tabs),
|
|
304
|
+
this FlatList doesn't scroll so onEndReached can fire repeatedly;
|
|
305
|
+
skip it here and let the parent ScrollView emit scroll-end instead. */
|
|
306
|
+
isScreenWrappedInContainer
|
|
307
|
+
? undefined
|
|
308
|
+
: () => {
|
|
309
|
+
if (!hasUserScrolledRef.current) return;
|
|
310
|
+
emitScrollEndReached();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/>
|
|
314
|
+
</ViewportTracker>
|
|
398
315
|
</ScreenLoadingMeasurements>
|
|
399
316
|
</ComponentsMapRefContext.Provider>
|
|
400
317
|
</ComponentsMapHeightContext.Provider>
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
2
|
|
|
3
3
|
exports[`componentsMap renders renders components map correctly 1`] = `
|
|
4
|
-
<View
|
|
4
|
+
<View
|
|
5
|
+
style={
|
|
6
|
+
{
|
|
7
|
+
"flex": 1,
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
>
|
|
5
11
|
<View
|
|
6
12
|
onLayout={[Function]}
|
|
13
|
+
style={
|
|
14
|
+
{
|
|
15
|
+
"flex": 1,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
7
18
|
>
|
|
8
19
|
<RCTScrollView
|
|
9
20
|
ListFooterComponent={
|
|
@@ -156,6 +167,11 @@ exports[`componentsMap renders renders components map correctly 1`] = `
|
|
|
156
167
|
>
|
|
157
168
|
<View
|
|
158
169
|
onLayout={[Function]}
|
|
170
|
+
style={
|
|
171
|
+
{
|
|
172
|
+
"flex": 1,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
159
175
|
>
|
|
160
176
|
<View />
|
|
161
177
|
</View>
|
|
@@ -167,6 +183,11 @@ exports[`componentsMap renders renders components map correctly 1`] = `
|
|
|
167
183
|
>
|
|
168
184
|
<View
|
|
169
185
|
onLayout={[Function]}
|
|
186
|
+
style={
|
|
187
|
+
{
|
|
188
|
+
"flex": 1,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
170
191
|
/>
|
|
171
192
|
</View>
|
|
172
193
|
<View
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-ui-components",
|
|
3
|
-
"version": "16.0.0-alpha.
|
|
3
|
+
"version": "16.0.0-alpha.5402127570",
|
|
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": "16.0.0-alpha.
|
|
32
|
-
"@applicaster/zapp-react-native-bridge": "16.0.0-alpha.
|
|
33
|
-
"@applicaster/zapp-react-native-redux": "16.0.0-alpha.
|
|
34
|
-
"@applicaster/zapp-react-native-utils": "16.0.0-alpha.
|
|
31
|
+
"@applicaster/applicaster-types": "16.0.0-alpha.5402127570",
|
|
32
|
+
"@applicaster/zapp-react-native-bridge": "16.0.0-alpha.5402127570",
|
|
33
|
+
"@applicaster/zapp-react-native-redux": "16.0.0-alpha.5402127570",
|
|
34
|
+
"@applicaster/zapp-react-native-utils": "16.0.0-alpha.5402127570",
|
|
35
35
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
36
|
"promise": "^8.3.0",
|
|
37
37
|
"url": "^0.11.0",
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import { StyleSheet, View } from "react-native";
|
|
3
|
-
|
|
4
|
-
const styles = StyleSheet.create({
|
|
5
|
-
row: { flexDirection: "row" },
|
|
6
|
-
basis: {
|
|
7
|
-
maxWidth: "100%",
|
|
8
|
-
flexBasis: "100%",
|
|
9
|
-
},
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
type Props = {
|
|
13
|
-
children: React.ReactNode;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Constrains its child to exactly the available parent width.
|
|
18
|
-
*
|
|
19
|
-
* UI components such as Hero and List render their items inside a parent
|
|
20
|
-
* `ScrollView` (e.g. when wrapped by tabs). A child placed directly in a
|
|
21
|
-
* scroll container can size itself to its own content rather than to the
|
|
22
|
-
* viewport, which causes items to be measured at a stale width and resize
|
|
23
|
-
* unexpectedly after layout changes such as device rotation.
|
|
24
|
-
*
|
|
25
|
-
* Wrapping the content in a `flexDirection: "row"` parent with a
|
|
26
|
-
* `flexBasis: "100%"` / `maxWidth: "100%"` child forces the child to
|
|
27
|
-
* re-derive its width from the parent on every layout pass instead of
|
|
28
|
-
* caching a measured pixel width, keeping it full-width and stable.
|
|
29
|
-
*
|
|
30
|
-
* @param children - The content to render at full parent width.
|
|
31
|
-
*/
|
|
32
|
-
export function FullWidthRow({ children }: Props) {
|
|
33
|
-
return (
|
|
34
|
-
<View style={styles.row}>
|
|
35
|
-
<View style={styles.basis}>{children}</View>
|
|
36
|
-
</View>
|
|
37
|
-
);
|
|
38
|
-
}
|