@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
- /* eslint react/prop-types: off */
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
- import React from "react";
4
- import { View, StyleSheet } from "react-native";
5
- import * as R from "ramda";
6
- import { v4 } from "uuid";
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 Props = {
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<any, any>;
23
+ onLayout: (event: LayoutChangeEvent) => void;
24
+ componentProps?: Record<string, any>;
25
+ onLoadFinished?: () => void;
12
26
  };
13
27
 
14
- const isError = (possibleError): boolean => possibleError instanceof Error;
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
- const MeasurementsPortal = ({ Component, onLayout, componentProps }: Props) => {
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} key={v4()}>
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 = React.createContext(null);
144
+ const MeasurementPortalContext =
145
+ React.createContext<MeasurementPortalContextValue | null>(null);
39
146
 
40
- const MeasurementsPortalContextProvider = ({ children }) => {
41
- const Component = React.useRef(View);
42
- const [measuringInProgress, setMeasuringInProgress] = React.useState(false);
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 measureComponentCallback = React.useRef(null);
45
- const componentProps = React.useRef(null);
46
- const onLoadFinishedIsCalled = React.useRef(null);
156
+ const onLoadFinishedRef =
157
+ React.useRef<MeasurementsPortalProps["onLoadFinished"]>(null);
47
158
 
48
- const setComponent = (comp) => {
49
- Component.current = comp;
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
- const setMeasureComponentCallback = (cb) => {
53
- measureComponentCallback.current = cb;
54
- };
170
+ state.measureComponentCallback({
171
+ width: state.layout.width,
172
+ height: state.layout.height,
173
+ index: state.componentProps?.index,
174
+ });
175
+ }
55
176
 
56
- const finalize = ({ width, height }) => {
57
- measureComponentCallback?.current?.({
58
- width,
59
- height,
60
- index: componentProps.current.index,
177
+ dispatch({ type: MEASUREMENT_ACTIONS.setMeasureCallback, payload: null });
178
+
179
+ dispatch({
180
+ type: MEASUREMENT_ACTIONS.setMeasuringProgress,
181
+ payload: false,
61
182
  });
62
183
 
63
- setMeasureComponentCallback(null);
64
- setMeasuringInProgress(false);
65
- onLoadFinishedIsCalled.current = false;
66
- };
184
+ dispatch({ type: MEASUREMENT_ACTIONS.setOnLoadFinished, payload: false });
185
+ }, [state.layout, state.componentProps, state.measureComponentCallback]);
67
186
 
68
- const setComponentProps = (props) => {
69
- const handleOnLoadFinish = (...args) => {
70
- const error = R.path([0, "error"], args);
71
-
72
- if (isError(error)) {
73
- finalize({ width: 0, height: 0 });
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
- const measureComponent = React.useCallback(async (comp, props) => {
87
- return new Promise((resolve) => {
88
- setMeasureComponentCallback(resolve);
89
- setMeasuringInProgress(true);
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
- setComponentProps(props);
92
- setComponent(comp);
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
- const onLayout = ({
97
- nativeEvent: {
98
- layout: { width, height },
99
- },
100
- }) => {
101
- if (measureComponentCallback.current && onLoadFinishedIsCalled.current) {
102
- finalize({
103
- width,
104
- height,
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={Component.current}
113
- componentProps={componentProps.current}
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.5643066061",
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.5643066061",
39
- "@applicaster/zapp-react-native-bridge": "12.0.0-alpha.5643066061",
40
- "@applicaster/zapp-react-native-redux": "12.0.0-alpha.5643066061",
41
- "@applicaster/zapp-react-native-utils": "12.0.0-alpha.5643066061",
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",