@applicaster/zapp-react-native-ui-components 12.0.0-alpha.5643066061 → 12.0.0-alpha.5786305091
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.
|
@@ -1,17 +1,43 @@
|
|
|
1
|
-
|
|
1
|
+
import React, { useEffect, useCallback, ReactNode, useReducer } from "react";
|
|
2
|
+
import { View, StyleSheet, LayoutChangeEvent } from "react-native";
|
|
3
|
+
import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const { log_debug } = createLogger({
|
|
6
|
+
subsystem: "MeasurementsPortal",
|
|
7
|
+
category: "UIComponents",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
type LayoutDimensions = {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
};
|
|
7
14
|
|
|
8
|
-
type
|
|
15
|
+
type MeasurementResult = {
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
index?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type MeasurementsPortalProps = {
|
|
9
22
|
Component: React.ElementType;
|
|
10
|
-
onLayout: (LayoutChangeEvent) => void;
|
|
11
|
-
componentProps?: Record<
|
|
23
|
+
onLayout: (event: LayoutChangeEvent) => void;
|
|
24
|
+
componentProps?: Record<string, any>;
|
|
25
|
+
onLoadFinished?: () => void;
|
|
12
26
|
};
|
|
13
27
|
|
|
14
|
-
|
|
28
|
+
type MeasurementPortalContextValue = {
|
|
29
|
+
measureComponent: (
|
|
30
|
+
comp: React.ElementType,
|
|
31
|
+
props: Record<string, any>
|
|
32
|
+
) => Promise<MeasurementResult>;
|
|
33
|
+
measuringInProgress: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type MeasurementsPortalContextProviderProps = {
|
|
37
|
+
children: ReactNode;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const isError = (possibleError: any): boolean => possibleError instanceof Error;
|
|
15
41
|
|
|
16
42
|
const styles = StyleSheet.create({
|
|
17
43
|
container: {
|
|
@@ -25,100 +51,245 @@ const styles = StyleSheet.create({
|
|
|
25
51
|
wrapper: {},
|
|
26
52
|
});
|
|
27
53
|
|
|
28
|
-
|
|
54
|
+
type State = {
|
|
55
|
+
component: React.ElementType;
|
|
56
|
+
componentProps: Record<string, any> | null;
|
|
57
|
+
measureComponentCallback: ((result: MeasurementResult) => void) | null;
|
|
58
|
+
onLoadFinishedIsCalled: boolean;
|
|
59
|
+
measuringInProgress: boolean;
|
|
60
|
+
layout: LayoutDimensions;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type Action =
|
|
64
|
+
| { type: "SET_COMPONENT"; payload: React.ElementType }
|
|
65
|
+
| { type: "SET_COMPONENT_PROPS"; payload: Record<string, any> | null }
|
|
66
|
+
| {
|
|
67
|
+
type: "SET_MEASURE_CALLBACK";
|
|
68
|
+
payload: ((result: MeasurementResult) => void) | null;
|
|
69
|
+
}
|
|
70
|
+
| { type: "SET_ON_LOAD_FINISHED"; payload: boolean }
|
|
71
|
+
| { type: "SET_MEASURING_PROGRESS"; payload: boolean }
|
|
72
|
+
| { type: "SET_LAYOUT"; payload: LayoutDimensions };
|
|
73
|
+
|
|
74
|
+
const MEASUREMENT_ACTIONS = {
|
|
75
|
+
setComponent: "SET_COMPONENT",
|
|
76
|
+
setComponentProps: "SET_COMPONENT_PROPS",
|
|
77
|
+
setMeasureCallback: "SET_MEASURE_CALLBACK",
|
|
78
|
+
setOnLoadFinished: "SET_ON_LOAD_FINISHED",
|
|
79
|
+
setMeasuringProgress: "SET_MEASURING_PROGRESS",
|
|
80
|
+
setLayout: "SET_LAYOUT",
|
|
81
|
+
} as const;
|
|
82
|
+
|
|
83
|
+
const reducer = (state: State, action: Action): State => {
|
|
84
|
+
switch (action.type) {
|
|
85
|
+
case MEASUREMENT_ACTIONS.setComponent:
|
|
86
|
+
return { ...state, component: action.payload };
|
|
87
|
+
case MEASUREMENT_ACTIONS.setComponentProps:
|
|
88
|
+
return { ...state, componentProps: action.payload };
|
|
89
|
+
case MEASUREMENT_ACTIONS.setMeasureCallback:
|
|
90
|
+
return { ...state, measureComponentCallback: action.payload };
|
|
91
|
+
case MEASUREMENT_ACTIONS.setOnLoadFinished:
|
|
92
|
+
return { ...state, onLoadFinishedIsCalled: action.payload };
|
|
93
|
+
case MEASUREMENT_ACTIONS.setMeasuringProgress:
|
|
94
|
+
return { ...state, measuringInProgress: action.payload };
|
|
95
|
+
case MEASUREMENT_ACTIONS.setLayout:
|
|
96
|
+
return { ...state, layout: action.payload };
|
|
97
|
+
default:
|
|
98
|
+
return state;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const initialState: State = {
|
|
103
|
+
component: null,
|
|
104
|
+
componentProps: null,
|
|
105
|
+
measureComponentCallback: null,
|
|
106
|
+
onLoadFinishedIsCalled: false,
|
|
107
|
+
measuringInProgress: false,
|
|
108
|
+
layout: { width: 0, height: 0 },
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Handles layout changes for the MeasurementsPortal component
|
|
113
|
+
*/
|
|
114
|
+
const onLayout =
|
|
115
|
+
(dispatch) =>
|
|
116
|
+
({ nativeEvent: { layout: newLayout } }: LayoutChangeEvent) => {
|
|
117
|
+
log_debug("Rendered measurement layout", { newLayout });
|
|
118
|
+
dispatch({ type: MEASUREMENT_ACTIONS.setLayout, payload: newLayout });
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* MeasurementsPortal is a component that is used to measure the size of a component
|
|
123
|
+
* It is not rendered to the main tree, but it actually uses a component and its props to measure the size of the component.
|
|
124
|
+
* The <Component> comes from the measureComponent method of the MeasurementPortalContext.
|
|
125
|
+
*/
|
|
126
|
+
const MeasurementsPortal = ({
|
|
127
|
+
Component,
|
|
128
|
+
onLayout,
|
|
129
|
+
componentProps,
|
|
130
|
+
}: MeasurementsPortalProps) => {
|
|
131
|
+
if (!Component) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
29
135
|
return (
|
|
30
136
|
<View style={styles.container} testID="MeasurementsPortal">
|
|
31
|
-
<View style={styles.wrapper} onLayout={onLayout}
|
|
137
|
+
<View style={styles.wrapper} onLayout={onLayout}>
|
|
32
138
|
<Component {...componentProps} />
|
|
33
139
|
</View>
|
|
34
140
|
</View>
|
|
35
141
|
);
|
|
36
142
|
};
|
|
37
143
|
|
|
38
|
-
const MeasurementPortalContext =
|
|
144
|
+
const MeasurementPortalContext =
|
|
145
|
+
React.createContext<MeasurementPortalContextValue | null>(null);
|
|
39
146
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
147
|
+
/**
|
|
148
|
+
* The MeasurementsPortalContextProvider provides a measureComponent method, and a measuringInProgress flag
|
|
149
|
+
* With these we can pre-render a component and measure its size before rendering it to the main tree to avoid layout shifts
|
|
150
|
+
*/
|
|
151
|
+
const MeasurementsPortalContextProvider = ({
|
|
152
|
+
children,
|
|
153
|
+
}: MeasurementsPortalContextProviderProps) => {
|
|
154
|
+
const [state, dispatch] = useReducer(reducer, initialState);
|
|
43
155
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
const onLoadFinishedIsCalled = React.useRef(null);
|
|
156
|
+
const onLoadFinishedRef =
|
|
157
|
+
React.useRef<MeasurementsPortalProps["onLoadFinished"]>(null);
|
|
47
158
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Finalize resolves the promise created by measureComponent and finalizes the measurement process
|
|
161
|
+
* It resolves with the measurement result (width/height/index) to the measureComponentCallback
|
|
162
|
+
*/
|
|
163
|
+
const finalize = useCallback(() => {
|
|
164
|
+
if (state.measureComponentCallback) {
|
|
165
|
+
log_debug("Finalize measurement process", {
|
|
166
|
+
layout: state.layout,
|
|
167
|
+
componentProps: state.componentProps,
|
|
168
|
+
});
|
|
51
169
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
170
|
+
state.measureComponentCallback({
|
|
171
|
+
width: state.layout.width,
|
|
172
|
+
height: state.layout.height,
|
|
173
|
+
index: state.componentProps?.index,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
55
176
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
177
|
+
dispatch({ type: MEASUREMENT_ACTIONS.setMeasureCallback, payload: null });
|
|
178
|
+
|
|
179
|
+
dispatch({
|
|
180
|
+
type: MEASUREMENT_ACTIONS.setMeasuringProgress,
|
|
181
|
+
payload: false,
|
|
61
182
|
});
|
|
62
183
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
onLoadFinishedIsCalled.current = false;
|
|
66
|
-
};
|
|
184
|
+
dispatch({ type: MEASUREMENT_ACTIONS.setOnLoadFinished, payload: false });
|
|
185
|
+
}, [state.layout, state.componentProps, state.measureComponentCallback]);
|
|
67
186
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
} else {
|
|
75
|
-
onLoadFinishedIsCalled.current = true;
|
|
76
|
-
props.onLoadFinished(...args);
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
componentProps.current = {
|
|
81
|
-
...props,
|
|
82
|
-
onLoadFinished: handleOnLoadFinish,
|
|
83
|
-
};
|
|
84
|
-
};
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
log_debug("Measurement portal updated", {
|
|
189
|
+
measureComponentCallback: state.measureComponentCallback,
|
|
190
|
+
onLoadFinishedIsCalled: state.onLoadFinishedIsCalled,
|
|
191
|
+
layout: state.layout,
|
|
192
|
+
});
|
|
85
193
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
194
|
+
if (
|
|
195
|
+
state.measureComponentCallback &&
|
|
196
|
+
state.layout.width > 0 &&
|
|
197
|
+
state.layout.height > 0 &&
|
|
198
|
+
onLoadFinishedRef.current
|
|
199
|
+
) {
|
|
200
|
+
finalize();
|
|
201
|
+
}
|
|
202
|
+
}, [state.measureComponentCallback, state.layout, finalize]);
|
|
90
203
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
204
|
+
/**
|
|
205
|
+
* Utility function that takes, a component and its props, measures the rendered size of the component
|
|
206
|
+
* and creates a promise that does not get resolved with the measurement result (width/height/index)
|
|
207
|
+
* until the onLoadFinished callback is called
|
|
208
|
+
*/
|
|
209
|
+
const measureComponent = useCallback(
|
|
210
|
+
(
|
|
211
|
+
comp: React.ElementType,
|
|
212
|
+
props: { onLoadFinished: () => void; [key: string]: any }
|
|
213
|
+
): Promise<MeasurementResult> => {
|
|
214
|
+
return new Promise((resolve) => {
|
|
215
|
+
log_debug("Measuring process started", { comp, props });
|
|
95
216
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
217
|
+
dispatch({
|
|
218
|
+
type: MEASUREMENT_ACTIONS.setMeasureCallback,
|
|
219
|
+
payload: resolve,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
dispatch({
|
|
223
|
+
type: MEASUREMENT_ACTIONS.setMeasuringProgress,
|
|
224
|
+
payload: true,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
dispatch({
|
|
228
|
+
type: MEASUREMENT_ACTIONS.setOnLoadFinished,
|
|
229
|
+
payload: false,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
dispatch({
|
|
233
|
+
type: MEASUREMENT_ACTIONS.setLayout,
|
|
234
|
+
payload: { width: 0, height: 0 },
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
onLoadFinishedRef.current =
|
|
238
|
+
// eslint-disable-next-line react/prop-types
|
|
239
|
+
props.onLoadFinished as MeasurementsPortalProps["onLoadFinished"];
|
|
240
|
+
|
|
241
|
+
const handleOnLoadFinish = (...args: any[]) => {
|
|
242
|
+
if (args?.[0]?.error && isError(args[0].error)) {
|
|
243
|
+
log_debug(
|
|
244
|
+
"Component called onLoadFinish with an error, completing measurement process",
|
|
245
|
+
{ args }
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
finalize();
|
|
249
|
+
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
log_debug(
|
|
254
|
+
"Component called onLoadFinish, completing measurement process",
|
|
255
|
+
{ args }
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
dispatch({
|
|
259
|
+
type: MEASUREMENT_ACTIONS.setOnLoadFinished,
|
|
260
|
+
payload: true,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
onLoadFinishedRef.current?.apply(null, args);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
dispatch({
|
|
267
|
+
type: MEASUREMENT_ACTIONS.setComponentProps,
|
|
268
|
+
payload: { ...props, onLoadFinished: handleOnLoadFinish },
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
dispatch({ type: MEASUREMENT_ACTIONS.setComponent, payload: comp });
|
|
105
272
|
});
|
|
106
|
-
}
|
|
107
|
-
|
|
273
|
+
},
|
|
274
|
+
[]
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const contextValue = React.useMemo(
|
|
278
|
+
() => ({
|
|
279
|
+
measureComponent,
|
|
280
|
+
measuringInProgress: state.measuringInProgress,
|
|
281
|
+
}),
|
|
282
|
+
[measureComponent, state.measuringInProgress]
|
|
283
|
+
);
|
|
108
284
|
|
|
109
285
|
return (
|
|
110
286
|
<>
|
|
111
287
|
<MeasurementsPortal
|
|
112
|
-
Component={
|
|
113
|
-
componentProps={componentProps
|
|
114
|
-
onLayout={onLayout}
|
|
288
|
+
Component={state.component}
|
|
289
|
+
componentProps={state.componentProps}
|
|
290
|
+
onLayout={onLayout(dispatch)}
|
|
115
291
|
/>
|
|
116
|
-
<MeasurementPortalContext.Provider
|
|
117
|
-
value={{
|
|
118
|
-
measureComponent,
|
|
119
|
-
measuringInProgress,
|
|
120
|
-
}}
|
|
121
|
-
>
|
|
292
|
+
<MeasurementPortalContext.Provider value={contextValue}>
|
|
122
293
|
{children}
|
|
123
294
|
</MeasurementPortalContext.Provider>
|
|
124
295
|
</>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-ui-components",
|
|
3
|
-
"version": "12.0.0-alpha.
|
|
3
|
+
"version": "12.0.0-alpha.5786305091",
|
|
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",
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"redux-mock-store": "^1.5.3"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@applicaster/applicaster-types": "12.0.0-alpha.
|
|
39
|
-
"@applicaster/zapp-react-native-bridge": "12.0.0-alpha.
|
|
40
|
-
"@applicaster/zapp-react-native-redux": "12.0.0-alpha.
|
|
41
|
-
"@applicaster/zapp-react-native-utils": "12.0.0-alpha.
|
|
38
|
+
"@applicaster/applicaster-types": "12.0.0-alpha.5786305091",
|
|
39
|
+
"@applicaster/zapp-react-native-bridge": "12.0.0-alpha.5786305091",
|
|
40
|
+
"@applicaster/zapp-react-native-redux": "12.0.0-alpha.5786305091",
|
|
41
|
+
"@applicaster/zapp-react-native-utils": "12.0.0-alpha.5786305091",
|
|
42
42
|
"promise": "^8.3.0",
|
|
43
43
|
"react-router-native": "^5.1.2",
|
|
44
44
|
"url": "^0.11.0",
|