Pressable click fix (#18625)

* Update press-legacy to use native click events

* update tests for pressable change

* fix formatting issue

* Address comments. Bring back some tests, remove others. Cleanup

* Fix flow and lint errors

* formatting fix missed by yarn lint
This commit is contained in:
Naman Goel 2020-04-20 15:13:17 -07:00 committed by GitHub
parent 4f59a149b8
commit 5f7b175b35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 14 additions and 193 deletions

View File

@ -16,7 +16,7 @@ import type {
} from 'shared/ReactTypes'; } from 'shared/ReactTypes';
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
type AnyNativeEvent = Event | KeyboardEvent | MouseEvent | Touch; type AnyNativeEvent = Event | KeyboardEvent | MouseEvent | TouchEvent;
export type PointerType = export type PointerType =
| '' | ''

View File

@ -64,7 +64,6 @@ type PressState = {
|}>, |}>,
ignoreEmulatedMouseEvents: boolean, ignoreEmulatedMouseEvents: boolean,
activePointerId: null | number, activePointerId: null | number,
shouldPreventClick: boolean,
touchEvent: null | Touch, touchEvent: null | Touch,
... ...
}; };
@ -199,7 +198,6 @@ function createPressEvent(
x: clientX, x: clientX,
y: clientY, y: clientY,
preventDefault() { preventDefault() {
state.shouldPreventClick = true;
if (nativeEvent) { if (nativeEvent) {
pressEvent.defaultPrevented = true; pressEvent.defaultPrevented = true;
nativeEvent.preventDefault(); nativeEvent.preventDefault();
@ -229,8 +227,7 @@ function dispatchEvent(
const target = ((state.pressTarget: any): Element | Document); const target = ((state.pressTarget: any): Element | Document);
const pointerType = state.pointerType; const pointerType = state.pointerType;
const defaultPrevented = const defaultPrevented =
(event != null && event.nativeEvent.defaultPrevented === true) || event != null && event.nativeEvent.defaultPrevented === true;
(name === 'press' && state.shouldPreventClick);
const touchEvent = state.touchEvent; const touchEvent = state.touchEvent;
const syntheticEvent = createPressEvent( const syntheticEvent = createPressEvent(
context, context,
@ -529,7 +526,6 @@ const pressResponderImpl = {
responderRegionOnDeactivation: null, responderRegionOnDeactivation: null,
ignoreEmulatedMouseEvents: false, ignoreEmulatedMouseEvents: false,
activePointerId: null, activePointerId: null,
shouldPreventClick: false,
touchEvent: null, touchEvent: null,
}; };
}, },
@ -567,7 +563,6 @@ const pressResponderImpl = {
return; return;
} }
state.shouldPreventClick = false;
if (isTouchEvent) { if (isTouchEvent) {
state.ignoreEmulatedMouseEvents = true; state.ignoreEmulatedMouseEvents = true;
} else if (isKeyboardEvent) { } else if (isKeyboardEvent) {
@ -587,7 +582,6 @@ const pressResponderImpl = {
!altKey !altKey
) { ) {
nativeEvent.preventDefault(); nativeEvent.preventDefault();
state.shouldPreventClick = true;
} }
} else { } else {
return; return;
@ -645,9 +639,6 @@ const pressResponderImpl = {
} }
case 'click': { case 'click': {
if (state.shouldPreventClick) {
nativeEvent.preventDefault();
}
const onPress = props.onPress; const onPress = props.onPress;
if (isFunction(onPress) && isScreenReaderVirtualClick(nativeEvent)) { if (isFunction(onPress) && isScreenReaderVirtualClick(nativeEvent)) {
@ -751,7 +742,6 @@ const pressResponderImpl = {
case 'touchend': { case 'touchend': {
if (isPressed) { if (isPressed) {
const buttons = state.buttons; const buttons = state.buttons;
let isKeyboardEvent = false;
let touchEvent; let touchEvent;
if ( if (
type === 'pointerup' && type === 'pointerup' &&
@ -770,79 +760,13 @@ const pressResponderImpl = {
if (!isValidKeyboardEvent(nativeEvent)) { if (!isValidKeyboardEvent(nativeEvent)) {
return; return;
} }
isKeyboardEvent = true;
removeRootEventTypes(context, state); removeRootEventTypes(context, state);
} else if (buttons === 4) { } else if (buttons === 4) {
// Remove the root events here as no 'click' event is dispatched when this 'button' is pressed. // Remove the root events here as no 'click' event is dispatched when this 'button' is pressed.
removeRootEventTypes(context, state); 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); 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; state.touchEvent = null;
} else if (type === 'mouseup') { } else if (type === 'mouseup') {
state.ignoreEmulatedMouseEvents = false; state.ignoreEmulatedMouseEvents = false;
@ -855,6 +779,12 @@ const pressResponderImpl = {
if (previousPointerType !== 'keyboard') { if (previousPointerType !== 'keyboard') {
removeRootEventTypes(context, state); removeRootEventTypes(context, state);
} }
const pressTarget = state.pressTarget;
const onPress = props.onPress;
if (pressTarget !== null && isFunction(onPress)) {
dispatchEvent(event, onPress, context, state, 'press', DiscreteEvent);
}
break; break;
} }

View File

@ -469,15 +469,12 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
}); });
// @gate experimental // @gate experimental
it('is called after valid "keyup" event', () => { it('is called after valid "click" event', () => {
componentInit(); componentInit();
const target = createEventTarget(ref.current); const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'}); target.pointerdown();
target.keyup({key: 'Enter'}); target.pointerup();
expect(onPress).toHaveBeenCalledTimes(1); expect(onPress).toHaveBeenCalledTimes(1);
expect(onPress).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'keyboard', type: 'press'}),
);
}); });
// @gate experimental // @gate experimental
@ -804,40 +801,6 @@ 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 // @gate experimental
it('"onPress" is called on re-entry to hit rect', () => { it('"onPress" is called on re-entry to hit rect', () => {
componentInit(); componentInit();
@ -926,8 +889,8 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
'pointerdown', 'pointerdown',
'inner: onPressEnd', 'inner: onPressEnd',
'inner: onPressChange', 'inner: onPressChange',
'inner: onPress',
'pointerup', 'pointerup',
'inner: onPress',
]); ]);
}); });
} }
@ -1023,7 +986,6 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
// @gate experimental // @gate experimental
it('prevents native behavior by default', () => { it('prevents native behavior by default', () => {
const onPress = jest.fn(); const onPress = jest.fn();
const preventDefault = jest.fn();
const ref = React.createRef(); const ref = React.createRef();
const Component = () => { const Component = () => {
@ -1034,79 +996,8 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const target = createEventTarget(ref.current); const target = createEventTarget(ref.current);
target.pointerdown(); target.pointerdown();
target.pointerup({preventDefault}); target.pointerup();
expect(preventDefault).toBeCalled(); expect(onPress).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 // @gate experimental