Skip to content

fix: press event order #1696

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Oct 30, 2024
6 changes: 6 additions & 0 deletions experiments-app/src/experiments.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AccessibilityScreen } from './screens/Accessibility';
import { PressEvents } from './screens/PressEvents';
import { TextInputEventPropagation } from './screens/TextInputEventPropagation';
import { TextInputEvents } from './screens/TextInputEvents';
import { ScrollViewEvents } from './screens/ScrollViewEvents';
Expand All @@ -13,6 +14,11 @@ export const experiments = [
title: 'Accessibility',
component: AccessibilityScreen,
},
{
key: 'PressEvents',
title: 'Press Events',
component: PressEvents,
},
{
key: 'TextInputEvents',
title: 'TextInput Events',
Expand Down
82 changes: 82 additions & 0 deletions experiments-app/src/screens/PressEvents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as React from 'react';
import {
StyleSheet,
SafeAreaView,
Text,
TextInput,
View,
Pressable,
TouchableOpacity,
} from 'react-native';
import { nativeEventLogger, logEvent } from '../utils/helpers';

export function PressEvents() {
const [value, setValue] = React.useState('');

const handleChangeText = (value: string) => {
setValue(value);
logEvent('changeText', value);
};

return (
<SafeAreaView style={styles.container}>
<View style={styles.wrapper}>
<TextInput
style={styles.textInput}
value={value}
onPress={nativeEventLogger('press')}
onPressIn={nativeEventLogger('pressIn')}
onPressOut={nativeEventLogger('pressOut')}
/>
</View>
<View style={styles.wrapper}>
<Text
onPress={nativeEventLogger('press')}
onLongPress={nativeEventLogger('longPress')}
onPressIn={nativeEventLogger('pressIn')}
onPressOut={nativeEventLogger('pressOut')}
>
Text
</Text>
</View>
<View style={styles.wrapper}>
<Pressable
onPress={nativeEventLogger('press')}
onLongPress={nativeEventLogger('longPress')}
onPressIn={nativeEventLogger('pressIn')}
onPressOut={nativeEventLogger('pressOut')}
>
<Text>Pressable</Text>
</Pressable>
</View>
<View style={styles.wrapper}>
<TouchableOpacity
onPress={nativeEventLogger('press')}
onLongPress={nativeEventLogger('longPress')}
onPressIn={nativeEventLogger('pressIn')}
onPressOut={nativeEventLogger('pressOut')}
>
<Text>Pressable</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
wrapper: {
padding: 20,
backgroundColor: 'yellow',
},
textInput: {
backgroundColor: 'white',
margin: 20,
padding: 8,
fontSize: 18,
borderWidth: 1,
borderColor: 'grey',
},
});
5 changes: 4 additions & 1 deletion experiments-app/src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { NativeSyntheticEvent } from 'react-native/types';

let lastEventTimeStamp: number | null = null;

export function nativeEventLogger(name: string) {
return (event: NativeSyntheticEvent<unknown>) => {
logEvent(name, event?.nativeEvent);
Expand All @@ -14,5 +16,6 @@ export function customEventLogger(name: string) {

export function logEvent(name: string, ...args: unknown[]) {
// eslint-disable-next-line no-console
console.log(`Event: ${name}`, ...args);
console.log(`[${Date.now() - (lastEventTimeStamp ?? Date.now())}ms] Event: ${name}`, ...args);
lastEventTimeStamp = Date.now();
}
15 changes: 12 additions & 3 deletions src/__tests__/render.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-console */
import * as React from 'react';
import { Pressable, Text, TextInput, View } from 'react-native';
import { getConfig, resetToDefaults } from '../config';
import { configure, getConfig, resetToDefaults } from '../config';
import { fireEvent, render, RenderAPI, screen } from '..';

const PLACEHOLDER_FRESHNESS = 'Add custom freshness';
Expand Down Expand Up @@ -247,7 +247,16 @@ test('supports legacy rendering', () => {
expect(screen.root).toBeDefined();
});

test('supports concurrent rendering', () => {
// Enable concurrent rendering globally
configure({ concurrentRoot: true });

test('globally enable concurrent rendering', () => {
render(<View testID="test" />);
expect(screen.root).toBeOnTheScreen();
});

// Enable concurrent rendering locally
test('locally enable concurrent rendering', () => {
render(<View testID="test" />, { concurrentRoot: true });
expect(screen.root).toBeDefined();
expect(screen.root).toBeOnTheScreen();
});
8 changes: 4 additions & 4 deletions src/user-event/press/__tests__/press.real-timers.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ describe('userEvent.press with real timers', () => {
);
await userEvent.press(screen.getByText('press me'));

expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']);
expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']);
});

test('does not trigger on disabled Text', async () => {
Expand Down Expand Up @@ -240,7 +240,7 @@ describe('userEvent.press with real timers', () => {
expect(events).toEqual([]);
});

test('works on TetInput', async () => {
test('works on TextInput', async () => {
const { events, logEvent } = createEventLogger();

render(
Expand All @@ -255,7 +255,7 @@ describe('userEvent.press with real timers', () => {
expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut']);
});

test('does not call onPressIn and onPressOut on non editable TetInput', async () => {
test('does not call onPressIn and onPressOut on non editable TextInput', async () => {
const { events, logEvent } = createEventLogger();

render(
Expand All @@ -270,7 +270,7 @@ describe('userEvent.press with real timers', () => {
expect(events).toEqual([]);
});

test('does not call onPressIn and onPressOut on TetInput with pointer events disabled', async () => {
test('does not call onPressIn and onPressOut on TextInput with pointer events disabled', async () => {
const { events, logEvent } = createEventLogger();

render(
Expand Down
2 changes: 1 addition & 1 deletion src/user-event/press/__tests__/press.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ describe('userEvent.press with fake timers', () => {
);

await userEvent.press(screen.getByText('press me'));
expect(getEventsNames(events)).toEqual(['pressIn', 'press', 'pressOut']);
expect(getEventsNames(events)).toEqual(['pressIn', 'pressOut', 'press']);
});

test('press works on Button', async () => {
Expand Down
14 changes: 11 additions & 3 deletions src/user-event/press/press.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,19 @@ async function emitTextPressEvents(
await wait(config);
dispatchEvent(element, 'pressIn', EventBuilder.Common.touch());

// Emit either `press` or `longPress`.
dispatchEvent(element, options.type, EventBuilder.Common.touch());

await wait(config, options.duration);

// Long press events are emitted before `pressOut`.
if (options.type === 'longPress') {
dispatchEvent(element, 'longPress', EventBuilder.Common.touch());
}

dispatchEvent(element, 'pressOut', EventBuilder.Common.touch());

// Regular press events are emitted after `pressOut`.
if (options.type === 'press') {
dispatchEvent(element, 'press', EventBuilder.Common.touch());
}
}

/**
Expand Down