Repository: facebook/react
Pull request: Revert "Pressable click fix"

Reverts facebook/react#18625

When clicking on navigation items, we don't seem to be calling `preventDefault`, resulting in full page reloads internally.

Changed files:
filename: packages/react-dom/src/shared/ReactDOMTypes.js
patch: @@ -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 =
   | ''

filename: packages/react-interactions/events/src/dom/PressLegacy.js
patch: @@ -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;
       }
 

filename: packages/react-interactions/events/src/dom/__tests__/PressLegacy-test.internal.js
patch: @@ -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 <a href="#" ref={ref} DEPRECATED_flareListeners={listener} />;
+      };
+      ReactDOM.render(<Component />, 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 (
+          <a href="#">
+            <button ref={buttonRef} DEPRECATED_flareListeners={listener} />
+          </a>
+        );
+      };
+      ReactDOM.render(<Component />, 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 (
+          <a href="#" DEPRECATED_flareListeners={listener}>
+            <div ref={ref} />
+          </a>
+        );
+      };
+      ReactDOM.render(<Component />, container);
+
+      const target = createEventTarget(ref.current);
+      target.pointerdown();
+      target.pointerup({preventDefault});
+      expect(preventDefault).toBeCalled();
+      expect(onPress).toHaveBeenCalledWith(
+        expect.objectContaining({defaultPrevented: true}),
+      );
     });
 
     // @gate experimental