From 381c5e1255566611386d16f94ac8d2f0444b65c8 Mon Sep 17 00:00:00 2001 From: jameswilddev Date: Fri, 26 Jul 2024 15:44:14 +0100 Subject: [PATCH] Workaround for React Native incorrectly measuring objects on Android. --- react-native/hooks/useMeasure/index.tsx | 13 ++- react-native/hooks/useMeasure/unit.tsx | 113 ++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 2 deletions(-) diff --git a/react-native/hooks/useMeasure/index.tsx b/react-native/hooks/useMeasure/index.tsx index c03e172..71ebd7c 100644 --- a/react-native/hooks/useMeasure/index.tsx +++ b/react-native/hooks/useMeasure/index.tsx @@ -19,6 +19,15 @@ export function useMeasure ( const element = React.useRef(null) const queuedLayout = React.useRef(false) + const wrapped = (x: undefined | number, y: undefined | number, width: undefined | number, height: undefined | number, pageX: undefined | number, pageY: undefined | number): void => { + // According to types/documentation, these are never undefined. In + // practice, however, they have been observed to be undefined multiple + // times. + if (x !== undefined && y !== undefined && width !== undefined && height !== undefined && pageX !== undefined && pageY !== undefined) { + onMeasure(x, y, width, height, pageX, pageY) + } + } + return [ (_element) => { element.current = _element @@ -26,14 +35,14 @@ export function useMeasure ( if (queuedLayout.current) { queuedLayout.current = false - _element?.measure(onMeasure) + _element?.measure(wrapped) } }, () => { if (element.current === null) { queuedLayout.current = true } else { - element.current.measure(onMeasure) + element.current.measure(wrapped) } } ] diff --git a/react-native/hooks/useMeasure/unit.tsx b/react-native/hooks/useMeasure/unit.tsx index 9ad39d9..b7db93f 100644 --- a/react-native/hooks/useMeasure/unit.tsx +++ b/react-native/hooks/useMeasure/unit.tsx @@ -168,3 +168,116 @@ test('executes the callback once when the layout is computed, the ref is given a renderer.unmount() }) + +for (const discardedUpdateScenario of [ + { + name: 'when x is undefined', + x: undefined, + y: 40, + width: 640, + height: 320, + pageX: 18, + pageY: 72 + }, + { + name: 'when y is undefined', + x: 20, + y: undefined, + width: 640, + height: 320, + pageX: 18, + pageY: 72 + }, + { + name: 'when width is undefined', + x: 20, + y: 40, + width: undefined, + height: 320, + pageX: 18, + pageY: 72 + }, + { + name: 'when height is undefined', + x: 20, + y: 40, + width: 640, + height: undefined, + pageX: 18, + pageY: 72 + }, + { + name: 'when pageX is undefined', + x: 20, + y: 40, + width: 640, + height: 320, + pageX: undefined, + pageY: 72 + }, + { + name: 'when pageY is undefined', + x: 20, + y: 40, + width: 640, + height: 320, + pageX: 18, + pageY: undefined + } +]) { + describe(discardedUpdateScenario.name, () => { + test('executes the callback once when the ref is given, the layout is computed and measurement completes', () => { + const onMeasure = jest.fn() + let ref: React.RefCallback + const measure = jest.fn() + let onLayout: (event: LayoutChangeEvent) => void + const Component: React.FunctionComponent = () => { + const [_ref, _onLayout] = useMeasure(onMeasure) + ref = _ref + onLayout = _onLayout + return + } + const renderer = TestRenderer.create() + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ref!({ measure } as unknown as View) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + onLayout!({} as unknown as LayoutChangeEvent) + + measure.mock.calls[0][0](discardedUpdateScenario.x, discardedUpdateScenario.y, discardedUpdateScenario.width, discardedUpdateScenario.height, discardedUpdateScenario.pageX, discardedUpdateScenario.pageY) + + expect(onMeasure).not.toHaveBeenCalled() + expect(measure).toHaveBeenCalledTimes(1) + + renderer.unmount() + }) + + test('executes the callback once when the layout is computed, the ref is given and measurement completes', () => { + const onMeasure = jest.fn() + let ref: React.RefCallback + const measure = jest.fn() + let onLayout: (event: LayoutChangeEvent) => void + const Component: React.FunctionComponent = () => { + const [_ref, _onLayout] = useMeasure(onMeasure) + ref = _ref + onLayout = _onLayout + return + } + const renderer = TestRenderer.create() + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + onLayout!({} as unknown as LayoutChangeEvent) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ref!({ measure } as unknown as View) + + measure.mock.calls[0][0](discardedUpdateScenario.x, discardedUpdateScenario.y, discardedUpdateScenario.width, discardedUpdateScenario.height, discardedUpdateScenario.pageX, discardedUpdateScenario.pageY) + + expect(onMeasure).not.toHaveBeenCalled() + expect(measure).toHaveBeenCalledTimes(1) + + renderer.unmount() + }) + }) +}