diff --git a/packages/react-dom/src/shared/ReactDOMTypes.js b/packages/react-dom/src/shared/ReactDOMTypes.js
index 77930f789d..18f2cb7dc6 100644
--- a/packages/react-dom/src/shared/ReactDOMTypes.js
+++ b/packages/react-dom/src/shared/ReactDOMTypes.js
@@ -16,7 +16,7 @@ import type {
} from 'shared/ReactTypes';
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
-type AnyNativeEvent = Event | KeyboardEvent | MouseEvent | TouchEvent;
+type AnyNativeEvent = Event | KeyboardEvent | MouseEvent | Touch;
export type PointerType =
| ''
diff --git a/packages/react-interactions/events/src/dom/PressLegacy.js b/packages/react-interactions/events/src/dom/PressLegacy.js
index f5ea903a37..0b3756cb48 100644
--- a/packages/react-interactions/events/src/dom/PressLegacy.js
+++ b/packages/react-interactions/events/src/dom/PressLegacy.js
@@ -64,6 +64,7 @@ type PressState = {
|}>,
ignoreEmulatedMouseEvents: boolean,
activePointerId: null | number,
+ shouldPreventClick: boolean,
touchEvent: null | Touch,
...
};
@@ -198,6 +199,7 @@ function createPressEvent(
x: clientX,
y: clientY,
preventDefault() {
+ state.shouldPreventClick = true;
if (nativeEvent) {
pressEvent.defaultPrevented = true;
nativeEvent.preventDefault();
@@ -227,7 +229,8 @@ function dispatchEvent(
const target = ((state.pressTarget: any): Element | Document);
const pointerType = state.pointerType;
const defaultPrevented =
- event != null && event.nativeEvent.defaultPrevented === true;
+ (event != null && event.nativeEvent.defaultPrevented === true) ||
+ (name === 'press' && state.shouldPreventClick);
const touchEvent = state.touchEvent;
const syntheticEvent = createPressEvent(
context,
@@ -526,6 +529,7 @@ const pressResponderImpl = {
responderRegionOnDeactivation: null,
ignoreEmulatedMouseEvents: false,
activePointerId: null,
+ shouldPreventClick: false,
touchEvent: null,
};
},
@@ -563,6 +567,7 @@ const pressResponderImpl = {
return;
}
+ state.shouldPreventClick = false;
if (isTouchEvent) {
state.ignoreEmulatedMouseEvents = true;
} else if (isKeyboardEvent) {
@@ -582,6 +587,7 @@ const pressResponderImpl = {
!altKey
) {
nativeEvent.preventDefault();
+ state.shouldPreventClick = true;
}
} else {
return;
@@ -639,6 +645,9 @@ const pressResponderImpl = {
}
case 'click': {
+ if (state.shouldPreventClick) {
+ nativeEvent.preventDefault();
+ }
const onPress = props.onPress;
if (isFunction(onPress) && isScreenReaderVirtualClick(nativeEvent)) {
@@ -742,6 +751,7 @@ const pressResponderImpl = {
case 'touchend': {
if (isPressed) {
const buttons = state.buttons;
+ let isKeyboardEvent = false;
let touchEvent;
if (
type === 'pointerup' &&
@@ -760,13 +770,79 @@ const pressResponderImpl = {
if (!isValidKeyboardEvent(nativeEvent)) {
return;
}
+ isKeyboardEvent = true;
removeRootEventTypes(context, state);
} else if (buttons === 4) {
// Remove the root events here as no 'click' event is dispatched when this 'button' is pressed.
removeRootEventTypes(context, state);
}
+ // Determine whether to call preventDefault on subsequent native events.
+ if (
+ target !== null &&
+ context.isTargetWithinResponder(target) &&
+ context.isTargetWithinHostComponent(target, 'a')
+ ) {
+ const {
+ altKey,
+ ctrlKey,
+ metaKey,
+ shiftKey,
+ } = (nativeEvent: MouseEvent);
+ // Check "open in new window/tab" and "open context menu" key modifiers
+ const preventDefault = props.preventDefault;
+
+ if (
+ preventDefault !== false &&
+ !shiftKey &&
+ !metaKey &&
+ !ctrlKey &&
+ !altKey
+ ) {
+ state.shouldPreventClick = true;
+ }
+ }
+
+ const pressTarget = state.pressTarget;
dispatchPressEndEvents(event, context, props, state);
+ const onPress = props.onPress;
+
+ if (pressTarget !== null && isFunction(onPress)) {
+ if (
+ !isKeyboardEvent &&
+ pressTarget !== null &&
+ target !== null &&
+ !targetIsDocument(pressTarget)
+ ) {
+ if (
+ pointerType === 'mouse' &&
+ context.isTargetWithinNode(target, pressTarget)
+ ) {
+ state.isPressWithinResponderRegion = true;
+ } else {
+ // If the event target isn't within the press target, check if we're still
+ // within the responder region. The region may have changed if the
+ // element's layout was modified after activation.
+ updateIsPressWithinResponderRegion(
+ touchEvent || nativeEvent,
+ context,
+ props,
+ state,
+ );
+ }
+ }
+
+ if (state.isPressWithinResponderRegion && buttons !== 4) {
+ dispatchEvent(
+ event,
+ onPress,
+ context,
+ state,
+ 'press',
+ DiscreteEvent,
+ );
+ }
+ }
state.touchEvent = null;
} else if (type === 'mouseup') {
state.ignoreEmulatedMouseEvents = false;
@@ -779,12 +855,6 @@ const pressResponderImpl = {
if (previousPointerType !== 'keyboard') {
removeRootEventTypes(context, state);
}
-
- const pressTarget = state.pressTarget;
- const onPress = props.onPress;
- if (pressTarget !== null && isFunction(onPress)) {
- dispatchEvent(event, onPress, context, state, 'press', DiscreteEvent);
- }
break;
}
diff --git a/packages/react-interactions/events/src/dom/__tests__/PressLegacy-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/PressLegacy-test.internal.js
index ffa33378e1..abb9518890 100644
--- a/packages/react-interactions/events/src/dom/__tests__/PressLegacy-test.internal.js
+++ b/packages/react-interactions/events/src/dom/__tests__/PressLegacy-test.internal.js
@@ -469,12 +469,15 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
});
// @gate experimental
- it('is called after valid "click" event', () => {
+ it('is called after valid "keyup" event', () => {
componentInit();
const target = createEventTarget(ref.current);
- target.pointerdown();
- target.pointerup();
+ target.keydown({key: 'Enter'});
+ target.keyup({key: 'Enter'});
expect(onPress).toHaveBeenCalledTimes(1);
+ expect(onPress).toHaveBeenCalledWith(
+ expect.objectContaining({pointerType: 'keyboard', type: 'press'}),
+ );
});
// @gate experimental
@@ -801,6 +804,40 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
});
});
+ describe('beyond bounds of hit rect', () => {
+ /** ┌──────────────────┐
+ * │ ┌────────────┐ │
+ * │ │ VisualRect │ │
+ * │ └────────────┘ │
+ * │ HitRect │
+ * └──────────────────┘
+ * X <= Move to X and release
+ */
+ // @gate experimental
+ it('"onPress" is not called on release', () => {
+ componentInit();
+ const target = createEventTarget(ref.current);
+ const targetContainer = createEventTarget(container);
+ target.setBoundingClientRect(rectMock);
+ target.pointerdown({pointerType});
+ target.pointermove({pointerType, ...coordinatesInside});
+ if (pointerType === 'mouse') {
+ // TODO: use setPointerCapture so this is only true for fallback mouse events.
+ targetContainer.pointermove({pointerType, ...coordinatesOutside});
+ targetContainer.pointerup({pointerType, ...coordinatesOutside});
+ } else {
+ target.pointermove({pointerType, ...coordinatesOutside});
+ target.pointerup({pointerType, ...coordinatesOutside});
+ }
+ expect(events.filter(removePressMoveStrings)).toEqual([
+ 'onPressStart',
+ 'onPressChange',
+ 'onPressEnd',
+ 'onPressChange',
+ ]);
+ });
+ });
+
// @gate experimental
it('"onPress" is called on re-entry to hit rect', () => {
componentInit();
@@ -889,8 +926,8 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
'pointerdown',
'inner: onPressEnd',
'inner: onPressChange',
- 'pointerup',
'inner: onPress',
+ 'pointerup',
]);
});
}
@@ -986,6 +1023,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
// @gate experimental
it('prevents native behavior by default', () => {
const onPress = jest.fn();
+ const preventDefault = jest.fn();
const ref = React.createRef();
const Component = () => {
@@ -996,8 +1034,79 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const target = createEventTarget(ref.current);
target.pointerdown();
- target.pointerup();
- expect(onPress).toBeCalled();
+ target.pointerup({preventDefault});
+ expect(preventDefault).toBeCalled();
+ expect(onPress).toHaveBeenCalledWith(
+ expect.objectContaining({defaultPrevented: true}),
+ );
+ });
+
+ // @gate experimental
+ it('prevents native behaviour for keyboard events by default', () => {
+ const onPress = jest.fn();
+ const preventDefault = jest.fn();
+ const ref = React.createRef();
+
+ const Component = () => {
+ const listener = usePress({onPress});
+ return ;
+ };
+ ReactDOM.render(, container);
+
+ const target = createEventTarget(ref.current);
+ target.keydown({key: 'Enter', preventDefault});
+ target.keyup({key: 'Enter'});
+ expect(preventDefault).toBeCalled();
+ expect(onPress).toHaveBeenCalledWith(
+ expect.objectContaining({defaultPrevented: true}),
+ );
+ });
+
+ // @gate experimental
+ it('deeply prevents native behaviour by default', () => {
+ const onPress = jest.fn();
+ const preventDefault = jest.fn();
+ const buttonRef = React.createRef();
+
+ const Component = () => {
+ const listener = usePress({onPress});
+ return (
+
+
+
+ );
+ };
+ ReactDOM.render(, container);
+
+ const target = createEventTarget(buttonRef.current);
+ target.pointerdown();
+ target.pointerup({preventDefault});
+ expect(preventDefault).toBeCalled();
+ });
+
+ // @gate experimental
+ it('prevents native behaviour by default with nested elements', () => {
+ const onPress = jest.fn();
+ const preventDefault = jest.fn();
+ const ref = React.createRef();
+
+ const Component = () => {
+ const listener = usePress({onPress});
+ return (
+
+
+
+ );
+ };
+ ReactDOM.render(, container);
+
+ const target = createEventTarget(ref.current);
+ target.pointerdown();
+ target.pointerup({preventDefault});
+ expect(preventDefault).toBeCalled();
+ expect(onPress).toHaveBeenCalledWith(
+ expect.objectContaining({defaultPrevented: true}),
+ );
});
// @gate experimental