Skip to content

Commit cc1e249

Browse files
authored
fix: check for pointerEvents in both props and styles (#1799)
1 parent 5cd1602 commit cc1e249

File tree

2 files changed

+131
-12
lines changed

2 files changed

+131
-12
lines changed

src/__tests__/fire-event.test.tsx

Lines changed: 124 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ test('should not fire on disabled Pressable', () => {
210210
expect(handlePress).not.toHaveBeenCalled();
211211
});
212212

213-
test('should not fire inside View with pointerEvents="none"', () => {
213+
test('should not fire inside View with pointerEvents="none" in props', () => {
214214
const onPress = jest.fn();
215215
render(
216216
<View pointerEvents="none">
@@ -225,7 +225,37 @@ test('should not fire inside View with pointerEvents="none"', () => {
225225
expect(onPress).not.toHaveBeenCalled();
226226
});
227227

228-
test('should not fire inside View with pointerEvents="box-only"', () => {
228+
test('should not fire inside View with pointerEvents="none" in styles', () => {
229+
const onPress = jest.fn();
230+
render(
231+
<View style={{ pointerEvents: 'none' }}>
232+
<Pressable onPress={onPress}>
233+
<Text>Trigger</Text>
234+
</Pressable>
235+
</View>,
236+
);
237+
238+
fireEvent.press(screen.getByText('Trigger'));
239+
fireEvent(screen.getByText('Trigger'), 'onPress');
240+
expect(onPress).not.toHaveBeenCalled();
241+
});
242+
243+
test('should not fire inside View with pointerEvents="none" in styles array', () => {
244+
const onPress = jest.fn();
245+
render(
246+
<View style={[{ pointerEvents: 'none' }]}>
247+
<Pressable onPress={onPress}>
248+
<Text>Trigger</Text>
249+
</Pressable>
250+
</View>,
251+
);
252+
253+
fireEvent.press(screen.getByText('Trigger'));
254+
fireEvent(screen.getByText('Trigger'), 'onPress');
255+
expect(onPress).not.toHaveBeenCalled();
256+
});
257+
258+
test('should not fire inside View with pointerEvents="box-only" in props', () => {
229259
const onPress = jest.fn();
230260
render(
231261
<View pointerEvents="box-only">
@@ -240,7 +270,22 @@ test('should not fire inside View with pointerEvents="box-only"', () => {
240270
expect(onPress).not.toHaveBeenCalled();
241271
});
242272

243-
test('should fire inside View with pointerEvents="box-none"', () => {
273+
test('should not fire inside View with pointerEvents="box-only" in styles', () => {
274+
const onPress = jest.fn();
275+
render(
276+
<View style={{ pointerEvents: 'box-only' }}>
277+
<Pressable onPress={onPress}>
278+
<Text>Trigger</Text>
279+
</Pressable>
280+
</View>,
281+
);
282+
283+
fireEvent.press(screen.getByText('Trigger'));
284+
fireEvent(screen.getByText('Trigger'), 'onPress');
285+
expect(onPress).not.toHaveBeenCalled();
286+
});
287+
288+
test('should fire inside View with pointerEvents="box-none" in props', () => {
244289
const onPress = jest.fn();
245290
render(
246291
<View pointerEvents="box-none">
@@ -255,7 +300,22 @@ test('should fire inside View with pointerEvents="box-none"', () => {
255300
expect(onPress).toHaveBeenCalledTimes(2);
256301
});
257302

258-
test('should fire inside View with pointerEvents="auto"', () => {
303+
test('should fire inside View with pointerEvents="box-none" in styles', () => {
304+
const onPress = jest.fn();
305+
render(
306+
<View style={{ pointerEvents: 'box-none' }}>
307+
<Pressable onPress={onPress}>
308+
<Text>Trigger</Text>
309+
</Pressable>
310+
</View>,
311+
);
312+
313+
fireEvent.press(screen.getByText('Trigger'));
314+
fireEvent(screen.getByText('Trigger'), 'onPress');
315+
expect(onPress).toHaveBeenCalledTimes(2);
316+
});
317+
318+
test('should fire inside View with pointerEvents="auto" in props', () => {
259319
const onPress = jest.fn();
260320
render(
261321
<View pointerEvents="auto">
@@ -270,7 +330,22 @@ test('should fire inside View with pointerEvents="auto"', () => {
270330
expect(onPress).toHaveBeenCalledTimes(2);
271331
});
272332

273-
test('should not fire deeply inside View with pointerEvents="box-only"', () => {
333+
test('should fire inside View with pointerEvents="auto" in styles', () => {
334+
const onPress = jest.fn();
335+
render(
336+
<View style={{ pointerEvents: 'auto' }}>
337+
<Pressable onPress={onPress}>
338+
<Text>Trigger</Text>
339+
</Pressable>
340+
</View>,
341+
);
342+
343+
fireEvent.press(screen.getByText('Trigger'));
344+
fireEvent(screen.getByText('Trigger'), 'onPress');
345+
expect(onPress).toHaveBeenCalledTimes(2);
346+
});
347+
348+
test('should not fire deeply inside View with pointerEvents="box-only" in props', () => {
274349
const onPress = jest.fn();
275350
render(
276351
<View pointerEvents="box-only">
@@ -287,32 +362,73 @@ test('should not fire deeply inside View with pointerEvents="box-only"', () => {
287362
expect(onPress).not.toHaveBeenCalled();
288363
});
289364

290-
test('should fire non-pointer events inside View with pointerEvents="box-none"', () => {
365+
test('should not fire deeply inside View with pointerEvents="box-only" in styles', () => {
366+
const onPress = jest.fn();
367+
render(
368+
<View style={{ pointerEvents: 'box-only' }}>
369+
<View>
370+
<Pressable onPress={onPress}>
371+
<Text>Trigger</Text>
372+
</Pressable>
373+
</View>
374+
</View>,
375+
);
376+
377+
fireEvent.press(screen.getByText('Trigger'));
378+
fireEvent(screen.getByText('Trigger'), 'onPress');
379+
expect(onPress).not.toHaveBeenCalled();
380+
});
381+
382+
test('should fire non-pointer events inside View with pointerEvents="box-none" in props', () => {
291383
const onTouchStart = jest.fn();
292384
render(<View testID="view" pointerEvents="box-none" onTouchStart={onTouchStart} />);
293385

294386
fireEvent(screen.getByTestId('view'), 'touchStart');
295387
expect(onTouchStart).toHaveBeenCalled();
296388
});
297389

298-
test('should fire non-touch events inside View with pointerEvents="box-none"', () => {
390+
test('should fire non-pointer events inside View with pointerEvents="box-none" in styles', () => {
391+
const onTouchStart = jest.fn();
392+
render(<View testID="view" style={{ pointerEvents: 'box-none' }} onTouchStart={onTouchStart} />);
393+
394+
fireEvent(screen.getByTestId('view'), 'touchStart');
395+
expect(onTouchStart).toHaveBeenCalled();
396+
});
397+
398+
test('should fire non-touch events inside View with pointerEvents="box-none" in props', () => {
299399
const onLayout = jest.fn();
300400
render(<View testID="view" pointerEvents="box-none" onLayout={onLayout} />);
301401

302402
fireEvent(screen.getByTestId('view'), 'layout');
303403
expect(onLayout).toHaveBeenCalled();
304404
});
305405

406+
test('should fire non-touch events inside View with pointerEvents="box-none" in styles', () => {
407+
const onLayout = jest.fn();
408+
render(<View testID="view" style={{ pointerEvents: 'box-none' }} onLayout={onLayout} />);
409+
410+
fireEvent(screen.getByTestId('view'), 'layout');
411+
expect(onLayout).toHaveBeenCalled();
412+
});
413+
306414
// This test if pointerEvents="box-only" on composite `Pressable` is blocking
307415
// the 'press' event on host View rendered by pressable.
308-
test('should fire on Pressable with pointerEvents="box-only', () => {
416+
test('should fire on Pressable with pointerEvents="box-only" in props', () => {
309417
const onPress = jest.fn();
310418
render(<Pressable testID="pressable" pointerEvents="box-only" onPress={onPress} />);
311419

312420
fireEvent.press(screen.getByTestId('pressable'));
313421
expect(onPress).toHaveBeenCalled();
314422
});
315423

424+
test('should fire on Pressable with pointerEvents="box-only" in styles', () => {
425+
const onPress = jest.fn();
426+
render(<Pressable testID="pressable" style={{ pointerEvents: 'box-only' }} onPress={onPress} />);
427+
428+
fireEvent.press(screen.getByTestId('pressable'));
429+
expect(onPress).toHaveBeenCalled();
430+
});
431+
316432
test('should pass event up on disabled TouchableOpacity', () => {
317433
const handleInnerPress = jest.fn();
318434
const handleOuterPress = jest.fn();

src/helpers/pointer-events.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { StyleSheet } from 'react-native';
12
import type { ReactTestInstance } from 'react-test-renderer';
23

34
import { getHostParent } from './component-tree';
@@ -10,11 +11,13 @@ import { getHostParent } from './component-tree';
1011
* 'box-only': The view can be the target of touch events but its subviews cannot be
1112
* see the official react native doc https://reactnative.dev/docs/view#pointerevents */
1213
export const isPointerEventEnabled = (element: ReactTestInstance, isParent?: boolean): boolean => {
13-
const parentCondition = isParent
14-
? element?.props.pointerEvents === 'box-only'
15-
: element?.props.pointerEvents === 'box-none';
14+
// Check both props.pointerEvents and props.style.pointerEvents
15+
const pointerEvents =
16+
element?.props.pointerEvents ?? StyleSheet.flatten(element?.props.style)?.pointerEvents;
1617

17-
if (element?.props.pointerEvents === 'none' || parentCondition) {
18+
const parentCondition = isParent ? pointerEvents === 'box-only' : pointerEvents === 'box-none';
19+
20+
if (pointerEvents === 'none' || parentCondition) {
1821
return false;
1922
}
2023

0 commit comments

Comments
 (0)