Skip to content

Commit 45473c9

Browse files
authored
React events: Press event fixes (#15386)
1. Fix hiding context menu for longpress via touch. 2. Fix scrolling of viewport for longpress via spacebar key. 3. Add tests for anchor-related behaviour and preventDefault. 4. Add a deactivation delay for forced activation 5. Add pointerType to Press events. NOTE: this currently extends pointerType to include `keyboard`. NOTE: React Native doesn't have a deactivation delay for forced activation, but this is possibly because of the async bridge meaning that the events aren't dispatched sync.
1 parent 9672cf6 commit 45473c9

File tree

3 files changed

+240
-27
lines changed

3 files changed

+240
-27
lines changed

packages/react-events/src/Hover.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,9 @@ function dispatchHoverChangeEvent(
7272
props: HoverProps,
7373
state: HoverState,
7474
): void {
75+
const bool = state.isActiveHovered;
7576
const listener = () => {
76-
props.onHoverChange(state.isActiveHovered);
77+
props.onHoverChange(bool);
7778
};
7879
const syntheticEvent = createHoverEvent(
7980
'hoverchange',

packages/react-events/src/Press.js

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ type PressProps = {
3636
stopPropagation: boolean,
3737
};
3838

39+
type PointerType = '' | 'mouse' | 'keyboard' | 'pen' | 'touch';
40+
3941
type PressState = {
4042
didDispatchEvent: boolean,
4143
isActivePressed: boolean,
@@ -45,6 +47,7 @@ type PressState = {
4547
isPressed: boolean,
4648
isPressWithinResponderRegion: boolean,
4749
longPressTimeout: null | Symbol,
50+
pointerType: PointerType,
4851
pressTarget: null | Element | Document,
4952
pressEndTimeout: null | Symbol,
5053
pressStartTimeout: null | Symbol,
@@ -70,6 +73,7 @@ type PressEvent = {|
7073
listener: PressEvent => void,
7174
target: Element | Document,
7275
type: PressEventType,
76+
pointerType: PointerType,
7377
|};
7478

7579
const DEFAULT_PRESS_END_DELAY_MS = 0;
@@ -85,9 +89,10 @@ const DEFAULT_PRESS_RETENTION_OFFSET = {
8589
const targetEventTypes = [
8690
{name: 'click', passive: false},
8791
{name: 'keydown', passive: false},
92+
{name: 'keypress', passive: false},
93+
{name: 'contextmenu', passive: false},
8894
'pointerdown',
8995
'pointercancel',
90-
'contextmenu',
9196
];
9297
const rootEventTypes = [
9398
{name: 'keyup', passive: false},
@@ -110,11 +115,13 @@ function createPressEvent(
110115
type: PressEventType,
111116
target: Element | Document,
112117
listener: PressEvent => void,
118+
pointerType: PointerType,
113119
): PressEvent {
114120
return {
115121
listener,
116122
target,
117123
type,
124+
pointerType,
118125
};
119126
}
120127

@@ -125,7 +132,8 @@ function dispatchEvent(
125132
listener: (e: Object) => void,
126133
): void {
127134
const target = ((state.pressTarget: any): Element | Document);
128-
const syntheticEvent = createPressEvent(name, target, listener);
135+
const pointerType = state.pointerType;
136+
const syntheticEvent = createPressEvent(name, target, listener, pointerType);
129137
context.dispatchEvent(syntheticEvent, {
130138
discrete: true,
131139
});
@@ -137,8 +145,9 @@ function dispatchPressChangeEvent(
137145
props: PressProps,
138146
state: PressState,
139147
): void {
148+
const bool = state.isActivePressed;
140149
const listener = () => {
141-
props.onPressChange(state.isActivePressed);
150+
props.onPressChange(bool);
142151
};
143152
dispatchEvent(context, state, 'presschange', listener);
144153
}
@@ -148,8 +157,9 @@ function dispatchLongPressChangeEvent(
148157
props: PressProps,
149158
state: PressState,
150159
): void {
160+
const bool = state.isLongPressed;
151161
const listener = () => {
152-
props.onLongPressChange(state.isLongPressed);
162+
props.onLongPressChange(bool);
153163
};
154164
dispatchEvent(context, state, 'longpresschange', listener);
155165
}
@@ -251,6 +261,7 @@ function dispatchPressEndEvents(
251261
state: PressState,
252262
): void {
253263
const wasActivePressStart = state.isActivePressStart;
264+
let activationWasForced = false;
254265

255266
state.isActivePressStart = false;
256267
state.isPressed = false;
@@ -267,13 +278,17 @@ function dispatchPressEndEvents(
267278
if (state.isPressWithinResponderRegion) {
268279
// if we haven't yet activated (due to delays), activate now
269280
activate(context, props, state);
281+
activationWasForced = true;
270282
}
271283
}
272284

273285
if (state.isActivePressed) {
274286
const delayPressEnd = calculateDelayMS(
275287
props.delayPressEnd,
276-
0,
288+
// if activation and deactivation occur during the same event there's no
289+
// time for visual user feedback therefore a small delay is added before
290+
// deactivating.
291+
activationWasForced ? 10 : 0,
277292
DEFAULT_PRESS_END_DELAY_MS,
278293
);
279294
if (delayPressEnd > 0) {
@@ -338,6 +353,23 @@ function calculateResponderRegion(target, props) {
338353
};
339354
}
340355

356+
function getPointerType(nativeEvent: any) {
357+
const {type, pointerType} = nativeEvent;
358+
if (pointerType != null) {
359+
return pointerType;
360+
}
361+
if (type.indexOf('mouse') > -1) {
362+
return 'mouse';
363+
}
364+
if (type.indexOf('touch') > -1) {
365+
return 'touch';
366+
}
367+
if (type.indexOf('key') > -1) {
368+
return 'keyboard';
369+
}
370+
return '';
371+
}
372+
341373
function isPressWithinResponderRegion(
342374
nativeEvent: $PropertyType<ReactResponderEvent, 'nativeEvent'>,
343375
state: PressState,
@@ -377,6 +409,7 @@ const PressResponder = {
377409
isPressed: false,
378410
isPressWithinResponderRegion: true,
379411
longPressTimeout: null,
412+
pointerType: '',
380413
pressEndTimeout: null,
381414
pressStartTimeout: null,
382415
pressTarget: null,
@@ -403,10 +436,10 @@ const PressResponder = {
403436
!context.hasOwnership() &&
404437
!state.shouldSkipMouseAfterTouch
405438
) {
406-
if (
407-
(nativeEvent: any).pointerType === 'mouse' ||
408-
type === 'mousedown'
409-
) {
439+
const pointerType = getPointerType(nativeEvent);
440+
state.pointerType = pointerType;
441+
442+
if (pointerType === 'mouse' || type === 'mousedown') {
410443
if (
411444
// Ignore right- and middle-clicks
412445
nativeEvent.button === 1 ||
@@ -436,6 +469,9 @@ const PressResponder = {
436469
return;
437470
}
438471

472+
const pointerType = getPointerType(nativeEvent);
473+
state.pointerType = pointerType;
474+
439475
if (state.responderRegion == null) {
440476
let currentTarget = (target: any);
441477
while (
@@ -470,6 +506,9 @@ const PressResponder = {
470506
return;
471507
}
472508

509+
const pointerType = getPointerType(nativeEvent);
510+
state.pointerType = pointerType;
511+
473512
const wasLongPressed = state.isLongPressed;
474513

475514
dispatchPressEndEvents(context, props, state);
@@ -506,6 +545,8 @@ const PressResponder = {
506545
state.isAnchorTouched = true;
507546
return;
508547
}
548+
const pointerType = getPointerType(nativeEvent);
549+
state.pointerType = pointerType;
509550
state.pressTarget = target;
510551
state.isPressWithinResponderRegion = true;
511552
dispatchPressStartEvents(context, props, state);
@@ -519,6 +560,9 @@ const PressResponder = {
519560
return;
520561
}
521562
if (state.isPressed) {
563+
const pointerType = getPointerType(nativeEvent);
564+
state.pointerType = pointerType;
565+
522566
const wasLongPressed = state.isLongPressed;
523567

524568
dispatchPressEndEvents(context, props, state);
@@ -556,20 +600,24 @@ const PressResponder = {
556600
* Keyboard interaction support
557601
* TODO: determine UX for metaKey + validKeyPress interactions
558602
*/
559-
case 'keydown': {
603+
case 'keydown':
604+
case 'keypress': {
560605
if (
561-
!state.isPressed &&
562-
!state.isLongPressed &&
563606
!context.hasOwnership() &&
564607
isValidKeyPress((nativeEvent: any).key)
565608
) {
566-
// Prevent spacebar press from scrolling the window
567-
if ((nativeEvent: any).key === ' ') {
568-
(nativeEvent: any).preventDefault();
609+
if (state.isPressed) {
610+
// Prevent spacebar press from scrolling the window
611+
if ((nativeEvent: any).key === ' ') {
612+
(nativeEvent: any).preventDefault();
613+
}
614+
} else {
615+
const pointerType = getPointerType(nativeEvent);
616+
state.pointerType = pointerType;
617+
state.pressTarget = target;
618+
dispatchPressStartEvents(context, props, state);
619+
context.addRootEventTypes(target.ownerDocument, rootEventTypes);
569620
}
570-
state.pressTarget = target;
571-
dispatchPressStartEvents(context, props, state);
572-
context.addRootEventTypes(target.ownerDocument, rootEventTypes);
573621
}
574622
break;
575623
}
@@ -593,7 +641,6 @@ const PressResponder = {
593641
break;
594642
}
595643

596-
case 'contextmenu':
597644
case 'pointercancel':
598645
case 'scroll':
599646
case 'touchcancel': {
@@ -608,14 +655,29 @@ const PressResponder = {
608655
case 'click': {
609656
if (isAnchorTagElement(target)) {
610657
const {ctrlKey, metaKey, shiftKey} = ((nativeEvent: any): MouseEvent);
658+
// Check "open in new window/tab" and "open context menu" key modifiers
611659
const preventDefault = props.preventDefault;
612-
// Check "open in new window/tab" key modifiers
613-
if (preventDefault !== false && !shiftKey && !ctrlKey && !metaKey) {
660+
if (preventDefault !== false && !shiftKey && !metaKey && !ctrlKey) {
614661
(nativeEvent: any).preventDefault();
615662
}
616663
}
664+
break;
665+
}
666+
667+
case 'contextmenu': {
668+
if (state.isPressed) {
669+
if (props.preventDefault !== false) {
670+
(nativeEvent: any).preventDefault();
671+
} else {
672+
state.shouldSkipMouseAfterTouch = false;
673+
dispatchPressEndEvents(context, props, state);
674+
context.removeRootEventTypes(rootEventTypes);
675+
}
676+
}
677+
break;
617678
}
618679
}
680+
619681
if (state.didDispatchEvent) {
620682
const shouldStopPropagation =
621683
props.stopPropagation === undefined ? true : props.stopPropagation;

0 commit comments

Comments
 (0)