Skip to content

Commit fd870e6

Browse files
authored
[react-ui/events] Tap responder API changes (#16827)
This patch limits the `onTap*` callbacks to the primary pointer button. Auxiliary button and modified primary button interactions call `onAuxiliaryTap`, cancel any active tap, and preserve the native behavior.
1 parent 4ddcb8e commit fd870e6

File tree

7 files changed

+254
-143
lines changed

7 files changed

+254
-143
lines changed

packages/react-ui/events/src/dom/Press.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ type PressEventType =
3434

3535
type PressEvent = {|
3636
altKey: boolean,
37-
buttons: null | 0 | 1 | 4,
37+
buttons: null | 1 | 4,
3838
ctrlKey: boolean,
3939
defaultPrevented: boolean,
4040
key: null | string,
@@ -53,7 +53,7 @@ type PressEvent = {|
5353
function createGestureState(e: any, type: PressEventType): PressEvent {
5454
return {
5555
altKey: e.altKey,
56-
buttons: e.buttons,
56+
buttons: e.type === 'tap:auxiliary' ? 4 : 1,
5757
ctrlKey: e.ctrlKey,
5858
defaultPrevented: e.defaultPrevented,
5959
key: e.key,
@@ -103,6 +103,19 @@ export function usePress(props: PressProps) {
103103
const tap = useTap({
104104
disabled: disabled || active === 'keyboard',
105105
preventDefault,
106+
onAuxiliaryTap(e) {
107+
if (onPressStart != null) {
108+
onPressStart(createGestureState(e, 'pressstart'));
109+
}
110+
if (onPressEnd != null) {
111+
onPressEnd(createGestureState(e, 'pressend'));
112+
}
113+
// Here we rely on Tap only calling 'onAuxiliaryTap' with modifiers when
114+
// the primary button is pressed
115+
if (onPress != null && (e.metaKey || e.shiftKey)) {
116+
onPress(createGestureState(e, 'press'));
117+
}
118+
},
106119
onTapStart(e) {
107120
if (active == null) {
108121
updateActive('tap');
@@ -124,7 +137,7 @@ export function usePress(props: PressProps) {
124137
if (onPressEnd != null) {
125138
onPressEnd(createGestureState(e, 'pressend'));
126139
}
127-
if (onPress != null && e.buttons !== 4) {
140+
if (onPress != null) {
128141
onPress(createGestureState(e, 'press'));
129142
}
130143
updateActive(null);

packages/react-ui/events/src/dom/Tap.js

Lines changed: 93 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,18 @@ import type {ReactEventResponderListener} from 'shared/ReactTypes';
1717
import React from 'react';
1818
import {
1919
buttonsEnum,
20-
hasPointerEvents,
21-
isMac,
2220
dispatchDiscreteEvent,
2321
dispatchUserBlockingEvent,
2422
getTouchById,
2523
hasModifierKey,
24+
hasPointerEvents,
2625
} from './shared';
2726

2827
type TapProps = $ReadOnly<{|
2928
disabled?: boolean,
3029
maximumDistance?: number,
3130
preventDefault?: boolean,
31+
onAuxiliaryTap?: (e: TapEvent) => void,
3232
onTapCancel?: (e: TapEvent) => void,
3333
onTapChange?: boolean => void,
3434
onTapEnd?: (e: TapEvent) => void,
@@ -38,7 +38,6 @@ type TapProps = $ReadOnly<{|
3838

3939
type TapGestureState = {|
4040
altKey: boolean,
41-
buttons: 0 | 1 | 4,
4241
ctrlKey: boolean,
4342
height: number,
4443
metaKey: boolean,
@@ -67,13 +66,15 @@ type TapState = {|
6766
ignoreEmulatedEvents: boolean,
6867
initialPosition: {|x: number, y: number|},
6968
isActive: boolean,
69+
isAuxiliaryActive: boolean,
7070
pointerType: PointerType,
7171
responderTarget: null | Element,
7272
rootEvents: null | Array<string>,
73-
shouldPreventClick: boolean,
73+
shouldPreventDefault: boolean,
7474
|};
7575

7676
type TapEventType =
77+
| 'tap:auxiliary'
7778
| 'tap:cancel'
7879
| 'tap:change'
7980
| 'tap:end'
@@ -125,14 +126,14 @@ function createInitialState(): TapState {
125126
buttons: 0,
126127
ignoreEmulatedEvents: false,
127128
isActive: false,
129+
isAuxiliaryActive: false,
128130
initialPosition: {x: 0, y: 0},
129131
pointerType: '',
130132
responderTarget: null,
131133
rootEvents: null,
132-
shouldPreventClick: true,
134+
shouldPreventDefault: true,
133135
gestureState: {
134136
altKey: false,
135-
buttons: 0,
136137
ctrlKey: false,
137138
height: 1,
138139
metaKey: false,
@@ -187,7 +188,6 @@ function createPointerEventGestureState(
187188

188189
return {
189190
altKey,
190-
buttons: state.buttons,
191191
ctrlKey,
192192
height,
193193
metaKey,
@@ -249,7 +249,6 @@ function createFallbackGestureState(
249249

250250
return {
251251
altKey,
252-
buttons: state.buttons != null ? state.buttons : 1,
253252
ctrlKey,
254253
height: !isCancelType && radiusY != null ? radiusY * 2 : 1,
255254
metaKey,
@@ -351,19 +350,23 @@ function isActivePointer(
351350
}
352351
}
353352

353+
function isAuxiliary(buttons: number, nativeEvent: any): boolean {
354+
return (
355+
// middle-click
356+
buttons === buttonsEnum.auxiliary ||
357+
// open-in-new-tab
358+
(buttons === buttonsEnum.primary && nativeEvent.metaKey) ||
359+
// open-in-new-window
360+
(buttons === buttonsEnum.primary && nativeEvent.shiftKey)
361+
);
362+
}
363+
354364
function shouldActivate(event: ReactDOMResponderEvent): boolean {
355365
const nativeEvent: any = event.nativeEvent;
356366
const pointerType = event.pointerType;
357367
const buttons = nativeEvent.buttons;
358-
const isContextMenu = pointerType === 'mouse' && nativeEvent.ctrlKey && isMac;
359-
const isValidButton =
360-
buttons === buttonsEnum.primary || buttons === buttonsEnum.middle;
361-
362-
if (pointerType === 'touch' || (isValidButton && !isContextMenu)) {
363-
return true;
364-
} else {
365-
return false;
366-
}
368+
const isValidButton = buttons === buttonsEnum.primary;
369+
return pointerType === 'touch' || (isValidButton && !hasModifierKey(event));
367370
}
368371

369372
/**
@@ -418,7 +421,7 @@ function dispatchEnd(
418421
const onTapEnd = props.onTapEnd;
419422
dispatchChange(context, props, state);
420423
if (onTapEnd != null) {
421-
const defaultPrevented = state.shouldPreventClick === true;
424+
const defaultPrevented = state.shouldPreventDefault === true;
422425
const payload = context.objectAssign({}, state.gestureState, {
423426
defaultPrevented,
424427
type,
@@ -441,6 +444,22 @@ function dispatchCancel(
441444
}
442445
}
443446

447+
function dispatchAuxiliaryTap(
448+
context: ReactDOMResponderContext,
449+
props: TapProps,
450+
state: TapState,
451+
): void {
452+
const type = 'tap:auxiliary';
453+
const onAuxiliaryTap = props.onAuxiliaryTap;
454+
if (onAuxiliaryTap != null) {
455+
const payload = context.objectAssign({}, state.gestureState, {
456+
defaultPrevented: false,
457+
type,
458+
});
459+
dispatchDiscreteEvent(context, payload, onAuxiliaryTap);
460+
}
461+
}
462+
444463
/**
445464
* Responder implementation
446465
*/
@@ -493,26 +512,41 @@ const responderImpl = {
493512
}
494513
}
495514

496-
if (!state.isActive && shouldActivate(event)) {
497-
state.isActive = true;
498-
state.buttons = nativeEvent.buttons;
499-
state.pointerType = event.pointerType;
500-
state.responderTarget = context.getResponderNode();
501-
state.shouldPreventClick = props.preventDefault !== false;
502-
503-
const gestureState = createGestureState(context, props, state, event);
504-
state.gestureState = gestureState;
505-
state.initialPosition.x = gestureState.x;
506-
state.initialPosition.y = gestureState.y;
507-
508-
dispatchStart(context, props, state);
509-
addRootEventTypes(rootEventTypes, context, state);
510-
511-
if (!hasPointerEvents) {
512-
if (eventType === 'touchstart') {
513-
state.ignoreEmulatedEvents = true;
515+
if (!state.isActive) {
516+
const activate = shouldActivate(event);
517+
const activateAuxiliary = isAuxiliary(
518+
nativeEvent.buttons,
519+
nativeEvent,
520+
);
521+
522+
if (activate || activateAuxiliary) {
523+
state.buttons = nativeEvent.buttons;
524+
state.pointerType = event.pointerType;
525+
state.responderTarget = context.getResponderNode();
526+
addRootEventTypes(rootEventTypes, context, state);
527+
if (!hasPointerEvents) {
528+
if (eventType === 'touchstart') {
529+
state.ignoreEmulatedEvents = true;
530+
}
514531
}
515532
}
533+
534+
if (activate) {
535+
const gestureState = createGestureState(
536+
context,
537+
props,
538+
state,
539+
event,
540+
);
541+
state.isActive = true;
542+
state.shouldPreventDefault = props.preventDefault !== false;
543+
state.gestureState = gestureState;
544+
state.initialPosition.x = gestureState.x;
545+
state.initialPosition.y = gestureState.y;
546+
dispatchStart(context, props, state);
547+
} else if (activateAuxiliary) {
548+
state.isAuxiliaryActive = true;
549+
}
516550
}
517551
break;
518552
}
@@ -575,24 +609,30 @@ const responderImpl = {
575609
case 'mouseup':
576610
case 'touchend': {
577611
if (state.isActive && isActivePointer(event, state)) {
578-
if (state.buttons === buttonsEnum.middle) {
579-
// Remove the root events here as no 'click' event is dispatched
580-
// when this 'button' is pressed.
581-
removeRootEventTypes(context, state);
582-
}
583-
584612
state.gestureState = createGestureState(context, props, state, event);
585-
586613
state.isActive = false;
587-
if (context.isTargetWithinResponder(hitTarget)) {
588-
// Determine whether to call preventDefault on subsequent native events.
589-
if (hasModifierKey(event)) {
590-
state.shouldPreventClick = false;
591-
}
592-
dispatchEnd(context, props, state);
593-
} else {
614+
if (isAuxiliary(state.buttons, nativeEvent)) {
615+
dispatchCancel(context, props, state);
616+
dispatchAuxiliaryTap(context, props, state);
617+
// Remove the root events here as no 'click' event is dispatched
618+
removeRootEventTypes(context, state);
619+
} else if (
620+
!context.isTargetWithinResponder(hitTarget) ||
621+
hasModifierKey(event)
622+
) {
594623
dispatchCancel(context, props, state);
624+
} else {
625+
dispatchEnd(context, props, state);
595626
}
627+
} else if (
628+
state.isAuxiliaryActive &&
629+
isAuxiliary(state.buttons, nativeEvent)
630+
) {
631+
state.isAuxiliaryActive = false;
632+
state.gestureState = createGestureState(context, props, state, event);
633+
dispatchAuxiliaryTap(context, props, state);
634+
// Remove the root events here as no 'click' event is dispatched
635+
removeRootEventTypes(context, state);
596636
}
597637

598638
if (!hasPointerEvents) {
@@ -612,6 +652,7 @@ const responderImpl = {
612652
state.gestureState = createGestureState(context, props, state, event);
613653
state.isActive = false;
614654
dispatchCancel(context, props, state);
655+
removeRootEventTypes(context, state);
615656
}
616657
break;
617658
}
@@ -630,12 +671,13 @@ const responderImpl = {
630671
state.gestureState = createGestureState(context, props, state, event);
631672
state.isActive = false;
632673
dispatchCancel(context, props, state);
674+
removeRootEventTypes(context, state);
633675
}
634676
break;
635677
}
636678

637679
case 'click': {
638-
if (state.shouldPreventClick) {
680+
if (state.shouldPreventDefault) {
639681
nativeEvent.preventDefault();
640682
}
641683
removeRootEventTypes(context, state);

packages/react-ui/events/src/dom/__tests__/Press-test.internal.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,13 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
125125

126126
it('is called after middle-button pointer down', () => {
127127
const target = createEventTarget(ref.current);
128-
target.pointerdown({buttons: buttonsType.middle, pointerType: 'mouse'});
128+
const pointerType = 'mouse';
129+
target.pointerdown({buttons: buttonsType.auxiliary, pointerType});
130+
target.pointerup({pointerType});
129131
expect(onPressStart).toHaveBeenCalledTimes(1);
130132
expect(onPressStart).toHaveBeenCalledWith(
131133
expect.objectContaining({
132-
buttons: buttonsType.middle,
134+
buttons: buttonsType.auxiliary,
133135
pointerType: 'mouse',
134136
type: 'pressstart',
135137
}),
@@ -209,12 +211,15 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
209211

210212
it('is called after middle-button pointer up', () => {
211213
const target = createEventTarget(ref.current);
212-
target.pointerdown({buttons: buttonsType.middle, pointerType: 'mouse'});
214+
target.pointerdown({
215+
buttons: buttonsType.auxiliary,
216+
pointerType: 'mouse',
217+
});
213218
target.pointerup({pointerType: 'mouse'});
214219
expect(onPressEnd).toHaveBeenCalledTimes(1);
215220
expect(onPressEnd).toHaveBeenCalledWith(
216221
expect.objectContaining({
217-
buttons: buttonsType.middle,
222+
buttons: buttonsType.auxiliary,
218223
pointerType: 'mouse',
219224
type: 'pressend',
220225
}),
@@ -350,7 +355,10 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {
350355

351356
it('is not called after middle-button press', () => {
352357
const target = createEventTarget(ref.current);
353-
target.pointerdown({buttons: buttonsType.middle, pointerType: 'mouse'});
358+
target.pointerdown({
359+
buttons: buttonsType.auxiliary,
360+
pointerType: 'mouse',
361+
});
354362
target.pointerup({pointerType: 'mouse'});
355363
expect(onPress).not.toHaveBeenCalled();
356364
});

0 commit comments

Comments
 (0)