Skip to content

Commit 051513b

Browse files
authored
React Events: consolidate logic for Press event component (#15451)
Refactor of Press and additional regression coverage. The logic for "start", "move", "end", and "cancel" events is consolidated into a single block to reduce duplication and improve consistency of the UX across input-types. Also reduces code size. The bailout logic for anchor tags is removed since we preventDefault for click by default. We can discuss scenarios where it makes sense to limit functionality around interactions on anchor tags. The logic for ignoring emulated events is simplified and improved. Pointer events can produce emulated touch (immediately after pointer) and mouse events (delayed) which is now accounted for and tested.
1 parent cdfce1a commit 051513b

File tree

2 files changed

+143
-195
lines changed

2 files changed

+143
-195
lines changed

packages/react-events/src/Press.js

Lines changed: 69 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ import type {
1313
ReactResponderDispatchEventOptions,
1414
} from 'shared/ReactTypes';
1515
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
16+
import {
17+
getEventPointerType,
18+
getEventCurrentTarget,
19+
isEventPositionWithinTouchHitTarget,
20+
} from './utils';
1621

1722
const CAPTURE_PHASE = 2;
1823

@@ -44,7 +49,6 @@ type PointerType = '' | 'mouse' | 'keyboard' | 'pen' | 'touch';
4449
type PressState = {
4550
isActivePressed: boolean,
4651
isActivePressStart: boolean,
47-
isAnchorTouched: boolean,
4852
isLongPressed: boolean,
4953
isPressed: boolean,
5054
isPressWithinResponderRegion: boolean,
@@ -59,7 +63,7 @@ type PressState = {
5963
right: number,
6064
top: number,
6165
|}>,
62-
shouldSkipMouseAfterTouch: boolean,
66+
ignoreEmulatedMouseEvents: boolean,
6367
};
6468

6569
type PressEventType =
@@ -355,23 +359,6 @@ function calculateResponderRegion(target, props) {
355359
};
356360
}
357361

358-
function getPointerType(nativeEvent: any) {
359-
const {type, pointerType} = nativeEvent;
360-
if (pointerType != null) {
361-
return pointerType;
362-
}
363-
if (type.indexOf('mouse') > -1) {
364-
return 'mouse';
365-
}
366-
if (type.indexOf('touch') > -1) {
367-
return 'touch';
368-
}
369-
if (type.indexOf('key') > -1) {
370-
return 'keyboard';
371-
}
372-
return '';
373-
}
374-
375362
function isPressWithinResponderRegion(
376363
nativeEvent: $PropertyType<ReactResponderEvent, 'nativeEvent'>,
377364
state: PressState,
@@ -406,7 +393,6 @@ const PressResponder = {
406393
didDispatchEvent: false,
407394
isActivePressed: false,
408395
isActivePressStart: false,
409-
isAnchorTouched: false,
410396
isLongPressed: false,
411397
isPressed: false,
412398
isPressWithinResponderRegion: true,
@@ -416,7 +402,7 @@ const PressResponder = {
416402
pressStartTimeout: null,
417403
pressTarget: null,
418404
responderRegion: null,
419-
shouldSkipMouseAfterTouch: false,
405+
ignoreEmulatedMouseEvents: false,
420406
};
421407
},
422408
onEvent(
@@ -431,70 +417,80 @@ const PressResponder = {
431417
if (phase === CAPTURE_PHASE) {
432418
return false;
433419
}
420+
434421
const nativeEvent: any = event.nativeEvent;
422+
const pointerType = getEventPointerType(event);
435423
const shouldStopPropagation =
436424
props.stopPropagation === undefined ? true : props.stopPropagation;
437425

438426
switch (type) {
439-
/**
440-
* Respond to pointer events and fall back to mouse.
441-
*/
427+
// START
442428
case 'pointerdown':
443-
case 'mousedown': {
444-
if (!state.isPressed && !state.shouldSkipMouseAfterTouch) {
445-
const pointerType = getPointerType(nativeEvent);
446-
state.pointerType = pointerType;
429+
case 'keydown':
430+
case 'keypress':
431+
case 'mousedown':
432+
case 'touchstart': {
433+
if (!state.isPressed) {
434+
if (type === 'pointerdown' || type === 'touchstart') {
435+
state.ignoreEmulatedMouseEvents = true;
436+
}
437+
438+
// Ignore unrelated key events
439+
if (pointerType === 'keyboard') {
440+
if (!isValidKeyPress(nativeEvent.key)) {
441+
return shouldStopPropagation;
442+
}
443+
}
447444

448-
// Ignore pressing on hit slop area with mouse
449-
if (
450-
(pointerType === 'mouse' || type === 'mousedown') &&
451-
context.isPositionWithinTouchHitTarget(
452-
target.ownerDocument,
453-
nativeEvent.x,
454-
nativeEvent.y,
455-
)
456-
) {
457-
return false;
445+
// Ignore emulated mouse events and mouse pressing on touch hit target
446+
// area
447+
if (type === 'mousedown') {
448+
if (
449+
state.ignoreEmulatedMouseEvents ||
450+
isEventPositionWithinTouchHitTarget(event, context)
451+
) {
452+
return shouldStopPropagation;
453+
}
458454
}
459455

460456
// Ignore any device buttons except left-mouse and touch/pen contact
461457
if (nativeEvent.button > 0) {
462458
return shouldStopPropagation;
463459
}
464460

461+
state.pointerType = pointerType;
465462
state.pressTarget = target;
466463
state.isPressWithinResponderRegion = true;
467464
dispatchPressStartEvents(context, props, state);
468465
context.addRootEventTypes(target.ownerDocument, rootEventTypes);
469466
return shouldStopPropagation;
467+
} else {
468+
// Prevent spacebar press from scrolling the window
469+
if (isValidKeyPress(nativeEvent.key) && nativeEvent.key === ' ') {
470+
nativeEvent.preventDefault();
471+
return shouldStopPropagation;
472+
}
470473
}
471-
return false;
474+
return shouldStopPropagation;
472475
}
476+
477+
// MOVE
473478
case 'pointermove':
474479
case 'mousemove':
475480
case 'touchmove': {
476481
if (state.isPressed) {
477-
if (state.shouldSkipMouseAfterTouch) {
482+
// Ignore emulated events (pointermove will dispatch touch and mouse events)
483+
// Ignore pointermove events during a keyboard press
484+
if (state.pointerType !== pointerType) {
478485
return shouldStopPropagation;
479486
}
480487

481-
const pointerType = getPointerType(nativeEvent);
482-
state.pointerType = pointerType;
483-
484488
if (state.responderRegion == null) {
485-
let currentTarget = (target: any);
486-
while (
487-
currentTarget.parentNode &&
488-
context.isTargetWithinEventComponent(currentTarget.parentNode)
489-
) {
490-
currentTarget = currentTarget.parentNode;
491-
}
492489
state.responderRegion = calculateResponderRegion(
493-
currentTarget,
490+
getEventCurrentTarget(event, context),
494491
props,
495492
);
496493
}
497-
498494
if (isPressWithinResponderRegion(nativeEvent, state)) {
499495
state.isPressWithinResponderRegion = true;
500496
if (props.onPressMove) {
@@ -510,19 +506,21 @@ const PressResponder = {
510506
}
511507
return false;
512508
}
509+
510+
// END
513511
case 'pointerup':
514-
case 'mouseup': {
512+
case 'keyup':
513+
case 'mouseup':
514+
case 'touchend': {
515515
if (state.isPressed) {
516-
if (state.shouldSkipMouseAfterTouch) {
517-
state.shouldSkipMouseAfterTouch = false;
518-
return shouldStopPropagation;
516+
// Ignore unrelated keyboard events
517+
if (pointerType === 'keyboard') {
518+
if (!isValidKeyPress(nativeEvent.key)) {
519+
return false;
520+
}
519521
}
520522

521-
const pointerType = getPointerType(nativeEvent);
522-
state.pointerType = pointerType;
523-
524523
const wasLongPressed = state.isLongPressed;
525-
526524
dispatchPressEndEvents(context, props, state);
527525

528526
if (state.pressTarget !== null && props.onPress) {
@@ -540,128 +538,25 @@ const PressResponder = {
540538
}
541539
context.removeRootEventTypes(rootEventTypes);
542540
return shouldStopPropagation;
543-
}
544-
state.isAnchorTouched = false;
545-
state.shouldSkipMouseAfterTouch = false;
546-
return false;
547-
}
548-
549-
/**
550-
* Touch event implementations are only needed for Safari, which lacks
551-
* support for pointer events.
552-
*/
553-
case 'touchstart': {
554-
if (!state.isPressed) {
555-
// We bail out of polyfilling anchor tags, given the same heuristics
556-
// explained above in regards to needing to use click events.
557-
if (isAnchorTagElement(target)) {
558-
state.isAnchorTouched = true;
559-
return shouldStopPropagation;
560-
}
561-
const pointerType = getPointerType(nativeEvent);
562-
state.pointerType = pointerType;
563-
state.pressTarget = target;
564-
state.isPressWithinResponderRegion = true;
565-
dispatchPressStartEvents(context, props, state);
566-
context.addRootEventTypes(target.ownerDocument, rootEventTypes);
567-
return shouldStopPropagation;
568-
}
569-
return false;
570-
}
571-
case 'touchend': {
572-
if (state.isAnchorTouched) {
573-
state.isAnchorTouched = false;
574-
return shouldStopPropagation;
575-
}
576-
if (state.isPressed) {
577-
const pointerType = getPointerType(nativeEvent);
578-
state.pointerType = pointerType;
579-
580-
const wasLongPressed = state.isLongPressed;
581-
582-
dispatchPressEndEvents(context, props, state);
583-
584-
if (type !== 'touchcancel' && props.onPress) {
585-
// Find if the X/Y of the end touch is still that of the original target
586-
const changedTouch = nativeEvent.changedTouches[0];
587-
const doc = (target: any).ownerDocument;
588-
const fromTarget = doc.elementFromPoint(
589-
changedTouch.screenX,
590-
changedTouch.screenY,
591-
);
592-
if (
593-
fromTarget !== null &&
594-
context.isTargetWithinEventComponent(fromTarget)
595-
) {
596-
if (
597-
!(
598-
wasLongPressed &&
599-
props.onLongPressShouldCancelPress &&
600-
props.onLongPressShouldCancelPress()
601-
)
602-
) {
603-
dispatchEvent(context, state, 'press', props.onPress);
604-
}
605-
}
606-
}
607-
state.shouldSkipMouseAfterTouch = true;
608-
context.removeRootEventTypes(rootEventTypes);
609-
return shouldStopPropagation;
610-
}
611-
return false;
612-
}
613-
614-
/**
615-
* Keyboard interaction support
616-
* TODO: determine UX for metaKey + validKeyPress interactions
617-
*/
618-
case 'keydown':
619-
case 'keypress': {
620-
if (isValidKeyPress(nativeEvent.key)) {
621-
if (state.isPressed) {
622-
// Prevent spacebar press from scrolling the window
623-
if (nativeEvent.key === ' ') {
624-
nativeEvent.preventDefault();
625-
}
626-
} else {
627-
const pointerType = getPointerType(nativeEvent);
628-
state.pointerType = pointerType;
629-
state.pressTarget = target;
630-
dispatchPressStartEvents(context, props, state);
631-
context.addRootEventTypes(target.ownerDocument, rootEventTypes);
632-
}
633-
return shouldStopPropagation;
634-
}
635-
return false;
636-
}
637-
case 'keyup': {
638-
if (state.isPressed && isValidKeyPress(nativeEvent.key)) {
639-
const wasLongPressed = state.isLongPressed;
640-
dispatchPressEndEvents(context, props, state);
641-
if (state.pressTarget !== null && props.onPress) {
642-
if (
643-
!(
644-
wasLongPressed &&
645-
props.onLongPressShouldCancelPress &&
646-
props.onLongPressShouldCancelPress()
647-
)
648-
) {
649-
dispatchEvent(context, state, 'press', props.onPress);
650-
}
651-
}
652-
context.removeRootEventTypes(rootEventTypes);
653-
return shouldStopPropagation;
541+
} else if (type === 'mouseup' && state.ignoreEmulatedMouseEvents) {
542+
state.ignoreEmulatedMouseEvents = false;
654543
}
655544
return false;
656545
}
657546

547+
// CANCEL
548+
case 'contextmenu':
658549
case 'pointercancel':
659550
case 'scroll':
660551
case 'touchcancel': {
661552
if (state.isPressed) {
662-
state.shouldSkipMouseAfterTouch = false;
663-
dispatchPressEndEvents(context, props, state);
664-
context.removeRootEventTypes(rootEventTypes);
553+
if (type === 'contextmenu' && props.preventDefault !== false) {
554+
nativeEvent.preventDefault();
555+
} else {
556+
state.ignoreEmulatedMouseEvents = false;
557+
dispatchPressEndEvents(context, props, state);
558+
context.removeRootEventTypes(rootEventTypes);
559+
}
665560
return shouldStopPropagation;
666561
}
667562
return false;
@@ -679,20 +574,6 @@ const PressResponder = {
679574
}
680575
return false;
681576
}
682-
683-
case 'contextmenu': {
684-
if (state.isPressed) {
685-
if (props.preventDefault !== false) {
686-
nativeEvent.preventDefault();
687-
} else {
688-
state.shouldSkipMouseAfterTouch = false;
689-
dispatchPressEndEvents(context, props, state);
690-
context.removeRootEventTypes(rootEventTypes);
691-
}
692-
return shouldStopPropagation;
693-
}
694-
return false;
695-
}
696577
}
697578
return false;
698579
},

0 commit comments

Comments
 (0)