Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,12 @@ type TextInputBaseProps = Readonly<{
*/
onKeyPress?: ?(e: TextInputKeyPressEvent) => unknown,

/**
* Called when a touch is held for 500ms. The input is focused then, as on a
* press, and the release that follows does not call `onPress`.
*/
onLongPress?: ?(event: GestureResponderEvent) => unknown,

/**
* Called when a single tap gesture is detected.
*/
Expand Down
43 changes: 26 additions & 17 deletions packages/react-native/Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -468,36 +468,45 @@ function InternalTextInput(props: TextInputProps): React.Node {
const {
editable,
hitSlop,
onLongPress,
onPress,
onPressIn,
onPressOut,
rejectResponderTermination,
} = props;

const config = useMemo(
() => ({
const config = useMemo(() => {
const focusIfEditable = () => {
if (editable !== false && inputRef.current != null) {
inputRef.current.focus();
}
};
return {
cancelable: Platform.OS === 'ios' ? !rejectResponderTermination : null,
hitSlop,
onLongPress:
onLongPress == null
? null
: (event: GestureResponderEvent) => {
onLongPress(event);
focusIfEditable();
},
onPress: (event: GestureResponderEvent) => {
onPress?.(event);
if (editable !== false) {
if (inputRef.current != null) {
inputRef.current.focus();
}
}
focusIfEditable();
},
onPressIn,
onPressOut,
}),
[
editable,
hitSlop,
onPress,
onPressIn,
onPressOut,
rejectResponderTermination,
],
);
};
}, [
editable,
hitSlop,
onLongPress,
onPress,
onPressIn,
onPressOut,
rejectResponderTermination,
]);

// Hide caret during test runs due to a flashing caret
// makes screenshot tests flakey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,30 @@ import * as React from 'react';
import {createRef, useEffect, useLayoutEffect, useRef} from 'react';
import {TextInput} from 'react-native';

function touchStart(element: HostInstance): void {
Fantom.dispatchNativeEvent(
element,
'onTouchStart',
{
touches: [{identifier: 0, pageX: 5, pageY: 5, timestamp: 0}],
changedTouches: [{identifier: 0, pageX: 5, pageY: 5, timestamp: 0}],
},
{category: Fantom.NativeEventCategory.Discrete},
);
}

function touchEnd(element: HostInstance): void {
Fantom.dispatchNativeEvent(
element,
'onTouchEnd',
{
touches: [],
changedTouches: [{identifier: 0, pageX: 5, pageY: 5, timestamp: 200}],
},
{category: Fantom.NativeEventCategory.Discrete},
);
}

describe('<TextInput>', () => {
describe('props', () => {
describe('selection', () => {
Expand Down Expand Up @@ -255,6 +279,141 @@ describe('<TextInput>', () => {
});
});

describe('onLongPress', () => {
let timers: Fantom.TimerMock;

beforeEach(() => {
timers = Fantom.installTimerMock();
});

afterEach(() => {
timers.uninstall();
});

it('is called once a touch is held past the long press delay', () => {
const root = Fantom.createRoot();
const nodeRef = createRef<HostInstance>();
const onLongPress = jest.fn();

Fantom.runTask(() => {
root.render(
<TextInput
onLongPress={onLongPress}
ref={nodeRef}
style={{height: 100, width: 100}}
/>,
);
});

const element = nullthrows(nodeRef.current);
touchStart(element);
timers.advanceTimersByTime(499);
expect(onLongPress).toHaveBeenCalledTimes(0);

timers.advanceTimersByTime(1);
expect(onLongPress).toHaveBeenCalledTimes(1);

touchEnd(element);
timers.runAllTimers();
});

it('focuses the input as a press does, and the release that follows calls no onPress', () => {
const root = Fantom.createRoot();
const nodeRef = createRef<HostInstance>();
const onPress = jest.fn();

Fantom.runTask(() => {
root.render(
<TextInput
nativeID="text-input"
onLongPress={() => {}}
onPress={onPress}
ref={nodeRef}
style={{height: 100, width: 100}}
/>,
);
});

root.takeMountingManagerLogs();

const element = nullthrows(nodeRef.current);
touchStart(element);
timers.advanceTimersByTime(500);

expect(root.takeMountingManagerLogs()).toEqual([
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}',
]);

touchEnd(element);
timers.runAllTimers();

expect(onPress).toHaveBeenCalledTimes(0);
});

it('does not change a press released before the delay, which calls onPress and focuses', () => {
const root = Fantom.createRoot();
const nodeRef = createRef<HostInstance>();
const onLongPress = jest.fn();
const onPress = jest.fn();

Fantom.runTask(() => {
root.render(
<TextInput
nativeID="text-input"
onLongPress={onLongPress}
onPress={onPress}
ref={nodeRef}
style={{height: 100, width: 100}}
/>,
);
});

root.takeMountingManagerLogs();

const element = nullthrows(nodeRef.current);
touchStart(element);
timers.advanceTimersByTime(100);
touchEnd(element);
timers.runAllTimers();

expect(onLongPress).toHaveBeenCalledTimes(0);
expect(onPress).toHaveBeenCalledTimes(1);
expect(root.takeMountingManagerLogs()).toEqual([
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}',
]);
});

it('when not set, leaves a held press calling onPress and focusing', () => {
const root = Fantom.createRoot();
const nodeRef = createRef<HostInstance>();
const onPress = jest.fn();

Fantom.runTask(() => {
root.render(
<TextInput
nativeID="text-input"
onPress={onPress}
ref={nodeRef}
style={{height: 100, width: 100}}
/>,
);
});

root.takeMountingManagerLogs();

const element = nullthrows(nodeRef.current);
touchStart(element);
timers.advanceTimersByTime(500);
touchEnd(element);
timers.runAllTimers();

expect(onPress).toHaveBeenCalledTimes(1);
expect(root.takeMountingManagerLogs()).toEqual([
'Command {type: "AndroidTextInput", nativeID: "text-input", name: "focus"}',
]);
});
});

describe('id and nativeID', () => {
it(`has 'id' propagated as 'nativeID' to the mounting layer`, () => {
const root = Fantom.createRoot();
Expand Down
7 changes: 4 additions & 3 deletions packages/react-native/ReactNativeApi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated SignedSource<<b0c8c41e65758fde472ddf1c5b106a27>>
* @generated SignedSource<<1b73a40ce1edc05ec461e82434a82f45>>
*
* This file was generated by scripts/js-api/build-types/index.js.
*/
Expand Down Expand Up @@ -5048,6 +5048,7 @@ declare type TextInputBaseProps = {
readonly onEndEditing?: (e: TextInputEndEditingEvent) => unknown
readonly onFocus?: (e: TextInputFocusEvent) => unknown
readonly onKeyPress?: (e: TextInputKeyPressEvent) => unknown
readonly onLongPress?: (event: GestureResponderEvent) => unknown
readonly onPress?: (event: GestureResponderEvent) => unknown
readonly onPressIn?: (event: GestureResponderEvent) => unknown
readonly onPressOut?: (event: GestureResponderEvent) => unknown
Expand Down Expand Up @@ -6022,7 +6023,7 @@ export {
TaskProvider, // 266dedf2
Text, // 6f8269c1
TextContentType, // 239b3ecc
TextInput, // cf6467dc
TextInput, // 33a0ccfa
TextInputAndroidProps, // 9ebbc103
TextInputBlurEvent, // b4c7a58c
TextInputChangeEvent, // 92363e10
Expand All @@ -6032,7 +6033,7 @@ export {
TextInputIOSProps, // fb3c9327
TextInputInstance, // 53fe7b73
TextInputKeyPressEvent, // cbbc322a
TextInputProps, // bb087c92
TextInputProps, // 3ea65957
TextInputSelectionChangeEvent, // 4a8f10c2
TextInputSubmitEditingEvent, // e811fdbd
TextInstance, // 7c7e2069
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,13 @@ export interface TextInputProps
*/
onEndEditing?: ((e: TextInputEndEditingEvent) => void) | undefined;

/**
* Called when a touch is held for 500ms. The input is focused then, as on a
* press, and the release that follows does not call `onPress`.
*/
onLongPress?:
((e: NativeSyntheticEvent<NativeTouchEvent>) => void) | undefined;

/**
* Called when a single tap gesture is detected.
*/
Expand Down
Loading