From 9c432b5da9821ed2f12e1beb3975b464197062a6 Mon Sep 17 00:00:00 2001 From: Hassan Khan Date: Sat, 19 Jul 2025 14:01:46 +0100 Subject: [PATCH] fix: check for `pointerEvents` in both props and styles ### Summary This allows users to test components that use `pointerEvents` in styles instead of props. Required for https://github.com/react-navigation/react-navigation/pull/12693 ### Test plan Related tests were copied and modified so`pointerEvents` is specified in `style` instead of props. --- src/__tests__/fire-event.test.tsx | 132 ++++++++++++++++++++++++++++-- src/helpers/pointer-events.ts | 11 ++- 2 files changed, 131 insertions(+), 12 deletions(-) diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx index cdada565..c8e2701f 100644 --- a/src/__tests__/fire-event.test.tsx +++ b/src/__tests__/fire-event.test.tsx @@ -210,7 +210,7 @@ test('should not fire on disabled Pressable', () => { expect(handlePress).not.toHaveBeenCalled(); }); -test('should not fire inside View with pointerEvents="none"', () => { +test('should not fire inside View with pointerEvents="none" in props', () => { const onPress = jest.fn(); render( @@ -225,7 +225,37 @@ test('should not fire inside View with pointerEvents="none"', () => { expect(onPress).not.toHaveBeenCalled(); }); -test('should not fire inside View with pointerEvents="box-only"', () => { +test('should not fire inside View with pointerEvents="none" in styles', () => { + const onPress = jest.fn(); + render( + + + Trigger + + , + ); + + fireEvent.press(screen.getByText('Trigger')); + fireEvent(screen.getByText('Trigger'), 'onPress'); + expect(onPress).not.toHaveBeenCalled(); +}); + +test('should not fire inside View with pointerEvents="none" in styles array', () => { + const onPress = jest.fn(); + render( + + + Trigger + + , + ); + + fireEvent.press(screen.getByText('Trigger')); + fireEvent(screen.getByText('Trigger'), 'onPress'); + expect(onPress).not.toHaveBeenCalled(); +}); + +test('should not fire inside View with pointerEvents="box-only" in props', () => { const onPress = jest.fn(); render( @@ -240,7 +270,22 @@ test('should not fire inside View with pointerEvents="box-only"', () => { expect(onPress).not.toHaveBeenCalled(); }); -test('should fire inside View with pointerEvents="box-none"', () => { +test('should not fire inside View with pointerEvents="box-only" in styles', () => { + const onPress = jest.fn(); + render( + + + Trigger + + , + ); + + fireEvent.press(screen.getByText('Trigger')); + fireEvent(screen.getByText('Trigger'), 'onPress'); + expect(onPress).not.toHaveBeenCalled(); +}); + +test('should fire inside View with pointerEvents="box-none" in props', () => { const onPress = jest.fn(); render( @@ -255,7 +300,22 @@ test('should fire inside View with pointerEvents="box-none"', () => { expect(onPress).toHaveBeenCalledTimes(2); }); -test('should fire inside View with pointerEvents="auto"', () => { +test('should fire inside View with pointerEvents="box-none" in styles', () => { + const onPress = jest.fn(); + render( + + + Trigger + + , + ); + + fireEvent.press(screen.getByText('Trigger')); + fireEvent(screen.getByText('Trigger'), 'onPress'); + expect(onPress).toHaveBeenCalledTimes(2); +}); + +test('should fire inside View with pointerEvents="auto" in props', () => { const onPress = jest.fn(); render( @@ -270,7 +330,22 @@ test('should fire inside View with pointerEvents="auto"', () => { expect(onPress).toHaveBeenCalledTimes(2); }); -test('should not fire deeply inside View with pointerEvents="box-only"', () => { +test('should fire inside View with pointerEvents="auto" in styles', () => { + const onPress = jest.fn(); + render( + + + Trigger + + , + ); + + fireEvent.press(screen.getByText('Trigger')); + fireEvent(screen.getByText('Trigger'), 'onPress'); + expect(onPress).toHaveBeenCalledTimes(2); +}); + +test('should not fire deeply inside View with pointerEvents="box-only" in props', () => { const onPress = jest.fn(); render( @@ -287,7 +362,24 @@ test('should not fire deeply inside View with pointerEvents="box-only"', () => { expect(onPress).not.toHaveBeenCalled(); }); -test('should fire non-pointer events inside View with pointerEvents="box-none"', () => { +test('should not fire deeply inside View with pointerEvents="box-only" in styles', () => { + const onPress = jest.fn(); + render( + + + + Trigger + + + , + ); + + fireEvent.press(screen.getByText('Trigger')); + fireEvent(screen.getByText('Trigger'), 'onPress'); + expect(onPress).not.toHaveBeenCalled(); +}); + +test('should fire non-pointer events inside View with pointerEvents="box-none" in props', () => { const onTouchStart = jest.fn(); render(); @@ -295,7 +387,15 @@ test('should fire non-pointer events inside View with pointerEvents="box-none"', expect(onTouchStart).toHaveBeenCalled(); }); -test('should fire non-touch events inside View with pointerEvents="box-none"', () => { +test('should fire non-pointer events inside View with pointerEvents="box-none" in styles', () => { + const onTouchStart = jest.fn(); + render(); + + fireEvent(screen.getByTestId('view'), 'touchStart'); + expect(onTouchStart).toHaveBeenCalled(); +}); + +test('should fire non-touch events inside View with pointerEvents="box-none" in props', () => { const onLayout = jest.fn(); render(); @@ -303,9 +403,17 @@ test('should fire non-touch events inside View with pointerEvents="box-none"', ( expect(onLayout).toHaveBeenCalled(); }); +test('should fire non-touch events inside View with pointerEvents="box-none" in styles', () => { + const onLayout = jest.fn(); + render(); + + fireEvent(screen.getByTestId('view'), 'layout'); + expect(onLayout).toHaveBeenCalled(); +}); + // This test if pointerEvents="box-only" on composite `Pressable` is blocking // the 'press' event on host View rendered by pressable. -test('should fire on Pressable with pointerEvents="box-only', () => { +test('should fire on Pressable with pointerEvents="box-only" in props', () => { const onPress = jest.fn(); render(); @@ -313,6 +421,14 @@ test('should fire on Pressable with pointerEvents="box-only', () => { expect(onPress).toHaveBeenCalled(); }); +test('should fire on Pressable with pointerEvents="box-only" in styles', () => { + const onPress = jest.fn(); + render(); + + fireEvent.press(screen.getByTestId('pressable')); + expect(onPress).toHaveBeenCalled(); +}); + test('should pass event up on disabled TouchableOpacity', () => { const handleInnerPress = jest.fn(); const handleOuterPress = jest.fn(); diff --git a/src/helpers/pointer-events.ts b/src/helpers/pointer-events.ts index 2e72ff8a..5992669c 100644 --- a/src/helpers/pointer-events.ts +++ b/src/helpers/pointer-events.ts @@ -1,3 +1,4 @@ +import { StyleSheet } from 'react-native'; import type { ReactTestInstance } from 'react-test-renderer'; import { getHostParent } from './component-tree'; @@ -10,11 +11,13 @@ import { getHostParent } from './component-tree'; * 'box-only': The view can be the target of touch events but its subviews cannot be * see the official react native doc https://reactnative.dev/docs/view#pointerevents */ export const isPointerEventEnabled = (element: ReactTestInstance, isParent?: boolean): boolean => { - const parentCondition = isParent - ? element?.props.pointerEvents === 'box-only' - : element?.props.pointerEvents === 'box-none'; + // Check both props.pointerEvents and props.style.pointerEvents + const pointerEvents = + element?.props.pointerEvents ?? StyleSheet.flatten(element?.props.style)?.pointerEvents; - if (element?.props.pointerEvents === 'none' || parentCondition) { + const parentCondition = isParent ? pointerEvents === 'box-only' : pointerEvents === 'box-none'; + + if (pointerEvents === 'none' || parentCondition) { return false; }