Skip to content

Commit a45c71c

Browse files
authored
feat: Support onClick as an alias for onPress (#7891)
* feat: Support onClick as an alias for onPress * Update docs description * Disable the React 16 table tests for now * fix * ugh * jest is silly * Omit onClick from Spectrum components
1 parent 4ad04f0 commit a45c71c

File tree

20 files changed

+282
-167
lines changed

20 files changed

+282
-167
lines changed

packages/@react-aria/button/src/useButton.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ export function useButton(props: AriaButtonOptions<ElementType>, ref: RefObject<
5757
preventFocusOnPress,
5858
// @ts-ignore - undocumented
5959
allowFocusWhenDisabled,
60-
// @ts-ignore
61-
onClick: deprecatedOnClick,
60+
onClick,
6261
href,
6362
target,
6463
rel,
@@ -88,6 +87,7 @@ export function useButton(props: AriaButtonOptions<ElementType>, ref: RefObject<
8887
onPressChange,
8988
onPress,
9089
onPressUp,
90+
onClick,
9191
isDisabled,
9292
preventFocusOnPress,
9393
ref
@@ -106,13 +106,7 @@ export function useButton(props: AriaButtonOptions<ElementType>, ref: RefObject<
106106
'aria-expanded': props['aria-expanded'],
107107
'aria-controls': props['aria-controls'],
108108
'aria-pressed': props['aria-pressed'],
109-
'aria-current': props['aria-current'],
110-
onClick: (e) => {
111-
if (deprecatedOnClick) {
112-
deprecatedOnClick(e);
113-
console.warn('onClick is deprecated, please use onPress');
114-
}
115-
}
109+
'aria-current': props['aria-current']
116110
})
117111
};
118112
}

packages/@react-aria/dnd/src/useDrop.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
*/
1212

1313
import {AriaButtonProps} from '@react-types/button';
14-
import {DragEvent, HTMLAttributes, useRef, useState} from 'react';
14+
import {DOMAttributes, DropActivateEvent, DropEnterEvent, DropEvent, DropExitEvent, DropMoveEvent, DropOperation, FocusableElement, DragTypes as IDragTypes, RefObject} from '@react-types/shared';
15+
import {DragEvent, useRef, useState} from 'react';
1516
import * as DragManager from './DragManager';
1617
import {DragTypes, globalAllowedDropOperations, globalDndState, readFromDataTransfer, setGlobalDnDState, setGlobalDropEffect} from './utils';
1718
import {DROP_EFFECT_TO_DROP_OPERATION, DROP_OPERATION, DROP_OPERATION_ALLOWED, DROP_OPERATION_TO_DROP_EFFECT} from './constants';
18-
import {DropActivateEvent, DropEnterEvent, DropEvent, DropExitEvent, DropMoveEvent, DropOperation, FocusableElement, DragTypes as IDragTypes, RefObject} from '@react-types/shared';
1919
import {isIPad, isMac, useEffectEvent, useLayoutEffect} from '@react-aria/utils';
2020
import {useVirtualDrop} from './useVirtualDrop';
2121

@@ -56,7 +56,7 @@ export interface DropOptions {
5656

5757
export interface DropResult {
5858
/** Props for the droppable element. */
59-
dropProps: HTMLAttributes<HTMLElement>,
59+
dropProps: DOMAttributes,
6060
/** Whether the drop target is currently focused or hovered. */
6161
isDropTarget: boolean,
6262
/** Props for the explicit drop button affordance, if any. */

packages/@react-aria/interactions/src/useFocusWithin.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
// NOTICE file in the root directory of this source tree.
1616
// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions
1717

18+
import {createSyntheticEvent, setEventTarget, useSyntheticBlurEvent} from './utils';
1819
import {DOMAttributes} from '@react-types/shared';
1920
import {FocusEvent, useCallback, useRef} from 'react';
2021
import {getActiveElement, getEventTarget, getOwnerDocument, nodeContains, useGlobalListeners} from '@react-aria/utils';
21-
import {SyntheticFocusEvent, useSyntheticBlurEvent} from './utils';
2222

2323
export interface FocusWithinProps {
2424
/** Whether the focus within events should be disabled. */
@@ -104,9 +104,9 @@ export function useFocusWithin(props: FocusWithinProps): FocusWithinResult {
104104
let currentTarget = e.currentTarget;
105105
addGlobalListener(ownerDocument, 'focus', e => {
106106
if (state.current.isFocusWithin && !nodeContains(currentTarget, e.target as Element)) {
107-
let event = new SyntheticFocusEvent('blur', new ownerDocument.defaultView!.FocusEvent('blur', {relatedTarget: e.target}));
108-
event.target = currentTarget;
109-
event.currentTarget = currentTarget;
107+
let nativeEvent = new ownerDocument.defaultView!.FocusEvent('blur', {relatedTarget: e.target});
108+
setEventTarget(nativeEvent, currentTarget);
109+
let event = createSyntheticEvent<FocusEvent>(nativeEvent);
110110
onBlur(event);
111111
}
112112
}, {capture: true});

packages/@react-aria/interactions/src/usePress.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ import {
3131
useGlobalListeners,
3232
useSyncRef
3333
} from '@react-aria/utils';
34+
import {createSyntheticEvent, preventFocus, setEventTarget} from './utils';
3435
import {disableTextSelection, restoreTextSelection} from './textSelection';
3536
import {DOMAttributes, FocusableElement, PressEvent as IPressEvent, PointerType, PressEvents, RefObject} from '@react-types/shared';
3637
import {flushSync} from 'react-dom';
3738
import {PressResponderContext} from './context';
38-
import {preventFocus} from './utils';
39-
import {TouchEvent as RTouchEvent, useContext, useEffect, useMemo, useRef, useState} from 'react';
39+
import {MouseEvent as RMouseEvent, TouchEvent as RTouchEvent, useContext, useEffect, useMemo, useRef, useState} from 'react';
4040

4141
export interface PressProps extends PressEvents {
4242
/** Whether the target is in a controlled press state (e.g. an overlay it triggers is open). */
@@ -170,6 +170,7 @@ export function usePress(props: PressHookProps): PressResult {
170170
onPressStart,
171171
onPressEnd,
172172
onPressUp,
173+
onClick,
173174
isDisabled,
174175
isPressed: isPressedProp,
175176
preventFocusOnPress,
@@ -295,6 +296,23 @@ export function usePress(props: PressHookProps): PressResult {
295296
}
296297
});
297298

299+
let triggerClick = useEffectEvent((e: RMouseEvent<FocusableElement>) => {
300+
onClick?.(e);
301+
});
302+
303+
let triggerSyntheticClick = useEffectEvent((e: KeyboardEvent | TouchEvent, target: FocusableElement) => {
304+
// Some third-party libraries pass in onClick instead of onPress.
305+
// Create a fake mouse event and trigger onClick as well.
306+
// This matches the browser's native activation behavior for certain elements (e.g. button).
307+
// https://html.spec.whatwg.org/#activation
308+
// https://html.spec.whatwg.org/#fire-a-synthetic-pointer-event
309+
if (onClick) {
310+
let event = new MouseEvent('click', e);
311+
setEventTarget(event, target);
312+
onClick(createSyntheticEvent(event));
313+
}
314+
});
315+
298316
let pressProps = useMemo(() => {
299317
let state = ref.current;
300318
let pressProps: DOMAttributes = {
@@ -362,11 +380,13 @@ export function usePress(props: PressHookProps): PressResult {
362380
let stopPressStart = triggerPressStart(e, 'virtual');
363381
let stopPressUp = triggerPressUp(e, 'virtual');
364382
let stopPressEnd = triggerPressEnd(e, 'virtual');
383+
triggerClick(e);
365384
shouldStopPropagation = stopPressStart && stopPressUp && stopPressEnd;
366385
} else if (state.isPressed && state.pointerType !== 'keyboard') {
367386
let pointerType = state.pointerType || (e.nativeEvent as PointerEvent).pointerType as PointerType || 'virtual';
368387
shouldStopPropagation = triggerPressEnd(createEvent(e.currentTarget, e), pointerType, true);
369388
state.isOverTarget = false;
389+
triggerClick(e);
370390
cancel(e);
371391
}
372392

@@ -385,7 +405,11 @@ export function usePress(props: PressHookProps): PressResult {
385405
}
386406

387407
let target = getEventTarget(e);
388-
triggerPressEnd(createEvent(state.target, e), 'keyboard', nodeContains(state.target, getEventTarget(e)));
408+
let wasPressed = nodeContains(state.target, getEventTarget(e));
409+
triggerPressEnd(createEvent(state.target, e), 'keyboard', wasPressed);
410+
if (wasPressed) {
411+
triggerSyntheticClick(e, state.target);
412+
}
389413
removeAllGlobalListeners();
390414

391415
// If a link was triggered with a key other than Enter, open the URL ourselves.
@@ -723,6 +747,7 @@ export function usePress(props: PressHookProps): PressResult {
723747
if (touch && isOverTarget(touch, e.currentTarget) && state.pointerType != null) {
724748
triggerPressUp(createTouchEvent(state.target!, e), state.pointerType);
725749
shouldStopPropagation = triggerPressEnd(createTouchEvent(state.target!, e), state.pointerType);
750+
triggerSyntheticClick(e.nativeEvent, state.target!);
726751
} else if (state.isOverTarget && state.pointerType != null) {
727752
shouldStopPropagation = triggerPressEnd(createTouchEvent(state.target!, e), state.pointerType, false);
728753
}
@@ -784,7 +809,9 @@ export function usePress(props: PressHookProps): PressResult {
784809
cancelOnPointerExit,
785810
triggerPressEnd,
786811
triggerPressStart,
787-
triggerPressUp
812+
triggerPressUp,
813+
triggerClick,
814+
triggerSyntheticClick
788815
]);
789816

790817
// Remove user-select: none in case component unmounts immediately after pressStart

packages/@react-aria/interactions/src/utils.ts

Lines changed: 19 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -12,57 +12,25 @@
1212

1313
import {FocusableElement} from '@react-types/shared';
1414
import {focusWithoutScrolling, getOwnerWindow, isFocusable, useEffectEvent, useLayoutEffect} from '@react-aria/utils';
15-
import {FocusEvent as ReactFocusEvent, useCallback, useRef} from 'react';
16-
17-
export class SyntheticFocusEvent<Target = Element> implements ReactFocusEvent<Target> {
18-
nativeEvent: FocusEvent;
19-
target: EventTarget & Target;
20-
currentTarget: EventTarget & Target;
21-
relatedTarget: Element;
22-
bubbles: boolean;
23-
cancelable: boolean;
24-
defaultPrevented: boolean;
25-
eventPhase: number;
26-
isTrusted: boolean;
27-
timeStamp: number;
28-
type: string;
29-
30-
constructor(type: string, nativeEvent: FocusEvent) {
31-
this.nativeEvent = nativeEvent;
32-
this.target = nativeEvent.target as EventTarget & Target;
33-
this.currentTarget = nativeEvent.currentTarget as EventTarget & Target;
34-
this.relatedTarget = nativeEvent.relatedTarget as Element;
35-
this.bubbles = nativeEvent.bubbles;
36-
this.cancelable = nativeEvent.cancelable;
37-
this.defaultPrevented = nativeEvent.defaultPrevented;
38-
this.eventPhase = nativeEvent.eventPhase;
39-
this.isTrusted = nativeEvent.isTrusted;
40-
this.timeStamp = nativeEvent.timeStamp;
41-
this.type = type;
42-
}
43-
44-
isDefaultPrevented(): boolean {
45-
return this.nativeEvent.defaultPrevented;
46-
}
47-
48-
preventDefault(): void {
49-
this.defaultPrevented = true;
50-
this.nativeEvent.preventDefault();
51-
}
52-
53-
stopPropagation(): void {
54-
this.nativeEvent.stopPropagation();
55-
this.isPropagationStopped = () => true;
56-
}
57-
58-
isPropagationStopped(): boolean {
59-
return false;
60-
}
15+
import {FocusEvent as ReactFocusEvent, SyntheticEvent, useCallback, useRef} from 'react';
16+
17+
// Turn a native event into a React synthetic event.
18+
export function createSyntheticEvent<E extends SyntheticEvent>(nativeEvent: Event): E {
19+
let event = nativeEvent as any as E;
20+
event.nativeEvent = nativeEvent;
21+
event.isDefaultPrevented = () => event.defaultPrevented;
22+
// cancelBubble is technically deprecated in the spec, but still supported in all browsers.
23+
event.isPropagationStopped = () => (event as any).cancelBubble;
24+
event.persist = () => {};
25+
return event;
26+
}
6127

62-
persist(): void {}
28+
export function setEventTarget(event: Event, target: Element): void {
29+
Object.defineProperty(event, 'target', {value: target});
30+
Object.defineProperty(event, 'currentTarget', {value: target});
6331
}
6432

65-
export function useSyntheticBlurEvent<Target = Element>(onBlur: (e: ReactFocusEvent<Target>) => void): (e: ReactFocusEvent<Target>) => void {
33+
export function useSyntheticBlurEvent<Target extends Element = Element>(onBlur: (e: ReactFocusEvent<Target>) => void): (e: ReactFocusEvent<Target>) => void {
6634
let stateRef = useRef({
6735
isFocused: false,
6836
observer: null as MutationObserver | null
@@ -80,7 +48,7 @@ export function useSyntheticBlurEvent<Target = Element>(onBlur: (e: ReactFocusEv
8048
};
8149
}, []);
8250

83-
let dispatchBlur = useEffectEvent((e: SyntheticFocusEvent<Target>) => {
51+
let dispatchBlur = useEffectEvent((e: ReactFocusEvent<Target>) => {
8452
onBlur?.(e);
8553
});
8654

@@ -104,7 +72,8 @@ export function useSyntheticBlurEvent<Target = Element>(onBlur: (e: ReactFocusEv
10472

10573
if (target.disabled) {
10674
// For backward compatibility, dispatch a (fake) React synthetic event.
107-
dispatchBlur(new SyntheticFocusEvent('blur', e as FocusEvent));
75+
let event = createSyntheticEvent<ReactFocusEvent<Target>>(e);
76+
dispatchBlur(event);
10877
}
10978

11079
// We no longer need the MutationObserver once the target is blurred.

0 commit comments

Comments
 (0)