diff --git a/packages/react-events/src/dom/Press.js b/packages/react-events/src/dom/Press.js index dcffeaa7e1..31fd03e971 100644 --- a/packages/react-events/src/dom/Press.js +++ b/packages/react-events/src/dom/Press.js @@ -685,14 +685,35 @@ const PressResponder: ReactDOMEventResponder = { const isKeyboardEvent = pointerType === 'keyboard'; const isMouseEvent = pointerType === 'mouse'; + // Ignore emulated mouse events + if (type === 'mousedown' && state.ignoreEmulatedMouseEvents) { + return; + } + + state.shouldPreventClick = false; if (isPointerEvent || isTouchEvent) { state.ignoreEmulatedMouseEvents = true; - } else if (type === 'mousedown' && state.ignoreEmulatedMouseEvents) { - // Ignore emulated mouse events - return; } else if (isKeyboardEvent) { // Ignore unrelated key events - if (!isValidKeyboardEvent(nativeEvent)) { + if (isValidKeyboardEvent(nativeEvent)) { + const { + altKey, + ctrlKey, + metaKey, + shiftKey, + } = (nativeEvent: MouseEvent); + if (nativeEvent.key === ' ') { + nativeEvent.preventDefault(); + } else if ( + props.preventDefault !== false && + !shiftKey && + !metaKey && + !ctrlKey && + !altKey + ) { + state.shouldPreventClick = true; + } + } else { return; } } @@ -920,7 +941,6 @@ const PressResponder: ReactDOMEventResponder = { } // Determine whether to call preventDefault on subsequent native events. - state.shouldPreventClick = false; if ( context.isTargetWithinEventComponent(target) && context.isTargetWithinHostComponent(target, 'a', true) diff --git a/packages/react-events/src/dom/__tests__/Press-test.internal.js b/packages/react-events/src/dom/__tests__/Press-test.internal.js index 91020fa96f..eec3505087 100644 --- a/packages/react-events/src/dom/__tests__/Press-test.internal.js +++ b/packages/react-events/src/dom/__tests__/Press-test.internal.js @@ -46,11 +46,7 @@ function createTouchEvent(type, id, data) { } const createKeyboardEvent = (type, data) => { - return new KeyboardEvent(type, { - bubbles: true, - cancelable: true, - ...data, - }); + return createEvent(type, data); }; function init() { @@ -216,13 +212,20 @@ describe('Event responder: Press', () => { }); it('is called once after "keydown" events for Spacebar', () => { - ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: ' '})); + const preventDefault = jest.fn(); + ref.current.dispatchEvent( + createKeyboardEvent('keydown', {key: ' ', preventDefault}), + ); + expect(preventDefault).toBeCalled(); ref.current.dispatchEvent(createKeyboardEvent('keypress', {key: ' '})); ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: ' '})); ref.current.dispatchEvent(createKeyboardEvent('keypress', {key: ' '})); expect(onPressStart).toHaveBeenCalledTimes(1); expect(onPressStart).toHaveBeenCalledWith( - expect.objectContaining({pointerType: 'keyboard', type: 'pressstart'}), + expect.objectContaining({ + pointerType: 'keyboard', + type: 'pressstart', + }), ); }); @@ -411,7 +414,6 @@ describe('Event responder: Press', () => { target: ref.current, }), ); - ref.current.dispatchEvent(createEvent('mousedown')); ref.current.dispatchEvent( createEvent('pointerup', {pointerType: 'touch'}), ); @@ -420,7 +422,9 @@ describe('Event responder: Press', () => { target: ref.current, }), ); + ref.current.dispatchEvent(createEvent('mousedown')); ref.current.dispatchEvent(createEvent('mouseup')); + ref.current.dispatchEvent(createEvent('click')); expect(onPressEnd).toHaveBeenCalledTimes(1); expect(onPressEnd).toHaveBeenCalledWith( expect.objectContaining({pointerType: 'touch', type: 'pressend'}), @@ -2419,7 +2423,7 @@ describe('Event responder: Press', () => { }); describe('link components', () => { - it('prevents native behaviour by default', () => { + it('prevents native behaviour for pointer events by default', () => { const onPress = jest.fn(); const preventDefault = jest.fn(); const ref = React.createRef(); @@ -2444,6 +2448,26 @@ describe('Event responder: Press', () => { ); }); + it('prevents native behaviour for keyboard events by default', () => { + const onPress = jest.fn(); + const preventDefault = jest.fn(); + const ref = React.createRef(); + const element = ( + + + + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createEvent('keydown', {key: 'Enter'})); + ref.current.dispatchEvent(createEvent('click', {preventDefault})); + ref.current.dispatchEvent(createEvent('keyup', {key: 'Enter'})); + expect(preventDefault).toBeCalled(); + expect(onPress).toHaveBeenCalledWith( + expect.objectContaining({defaultPrevented: true}), + ); + }); + it('deeply prevents native behaviour by default', () => { const onPress = jest.fn(); const preventDefault = jest.fn(); @@ -2527,7 +2551,7 @@ describe('Event responder: Press', () => { }); }); - it('uses native behaviour if preventDefault is false', () => { + it('uses native behaviour for pointer events if preventDefault is false', () => { const onPress = jest.fn(); const preventDefault = jest.fn(); const ref = React.createRef(); @@ -2552,6 +2576,26 @@ describe('Event responder: Press', () => { ); }); + it('uses native behaviour for keyboard events if preventDefault is false', () => { + const onPress = jest.fn(); + const preventDefault = jest.fn(); + const ref = React.createRef(); + const element = ( + + + + ); + ReactDOM.render(element, container); + + ref.current.dispatchEvent(createEvent('keydown', {key: 'Enter'})); + ref.current.dispatchEvent(createEvent('click', {preventDefault})); + ref.current.dispatchEvent(createEvent('keyup', {key: 'Enter'})); + expect(preventDefault).not.toBeCalled(); + expect(onPress).toHaveBeenCalledWith( + expect.objectContaining({defaultPrevented: false}), + ); + }); + it('warns when preventDefault is used in an event hook', () => { const onPress = jest.fn(); const preventDefault = jest.fn();