Skip to content

Commit

Permalink
feat: Support onClick as an alias for onPress
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett committed Mar 7, 2025
1 parent 55113ca commit 6f68c4a
Show file tree
Hide file tree
Showing 12 changed files with 254 additions and 155 deletions.
12 changes: 3 additions & 9 deletions packages/@react-aria/button/src/useButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ export function useButton(props: AriaButtonOptions<ElementType>, ref: RefObject<
preventFocusOnPress,
// @ts-ignore - undocumented
allowFocusWhenDisabled,
// @ts-ignore
onClick: deprecatedOnClick,
onClick,
href,
target,
rel,
Expand Down Expand Up @@ -88,6 +87,7 @@ export function useButton(props: AriaButtonOptions<ElementType>, ref: RefObject<
onPressChange,
onPress,
onPressUp,
onClick,
isDisabled,
preventFocusOnPress,
ref
Expand All @@ -106,13 +106,7 @@ export function useButton(props: AriaButtonOptions<ElementType>, ref: RefObject<
'aria-expanded': props['aria-expanded'],
'aria-controls': props['aria-controls'],
'aria-pressed': props['aria-pressed'],
'aria-current': props['aria-current'],
onClick: (e) => {
if (deprecatedOnClick) {
deprecatedOnClick(e);
console.warn('onClick is deprecated, please use onPress');
}
}
'aria-current': props['aria-current']
})
};
}
6 changes: 3 additions & 3 deletions packages/@react-aria/dnd/src/useDrop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
*/

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

Expand Down Expand Up @@ -56,7 +56,7 @@ export interface DropOptions {

export interface DropResult {
/** Props for the droppable element. */
dropProps: HTMLAttributes<HTMLElement>,
dropProps: DOMAttributes,
/** Whether the drop target is currently focused or hovered. */
isDropTarget: boolean,
/** Props for the explicit drop button affordance, if any. */
Expand Down
8 changes: 4 additions & 4 deletions packages/@react-aria/interactions/src/useFocusWithin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
// NOTICE file in the root directory of this source tree.
// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions

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

export interface FocusWithinProps {
/** Whether the focus within events should be disabled. */
Expand Down Expand Up @@ -104,9 +104,9 @@ export function useFocusWithin(props: FocusWithinProps): FocusWithinResult {
let currentTarget = e.currentTarget;
addGlobalListener(ownerDocument, 'focus', e => {
if (state.current.isFocusWithin && !nodeContains(currentTarget, e.target as Element)) {
let event = new SyntheticFocusEvent('blur', new ownerDocument.defaultView!.FocusEvent('blur', {relatedTarget: e.target}));
event.target = currentTarget;
event.currentTarget = currentTarget;
let nativeEvent = new ownerDocument.defaultView!.FocusEvent('blur', {relatedTarget: e.target});
setEventTarget(nativeEvent, currentTarget);
let event = createSyntheticEvent<FocusEvent>(nativeEvent);
onBlur(event);
}
}, {capture: true});
Expand Down
35 changes: 31 additions & 4 deletions packages/@react-aria/interactions/src/usePress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ import {
useGlobalListeners,
useSyncRef
} from '@react-aria/utils';
import {createSyntheticEvent, preventFocus, setEventTarget} from './utils';
import {disableTextSelection, restoreTextSelection} from './textSelection';
import {DOMAttributes, FocusableElement, PressEvent as IPressEvent, PointerType, PressEvents, RefObject} from '@react-types/shared';
import {flushSync} from 'react-dom';
import {PressResponderContext} from './context';
import {preventFocus} from './utils';
import {TouchEvent as RTouchEvent, useContext, useEffect, useMemo, useRef, useState} from 'react';
import {MouseEvent as RMouseEvent, TouchEvent as RTouchEvent, useContext, useEffect, useMemo, useRef, useState} from 'react';

export interface PressProps extends PressEvents {
/** Whether the target is in a controlled press state (e.g. an overlay it triggers is open). */
Expand Down Expand Up @@ -170,6 +170,7 @@ export function usePress(props: PressHookProps): PressResult {
onPressStart,
onPressEnd,
onPressUp,
onClick,
isDisabled,
isPressed: isPressedProp,
preventFocusOnPress,
Expand Down Expand Up @@ -295,6 +296,23 @@ export function usePress(props: PressHookProps): PressResult {
}
});

let triggerClick = useEffectEvent((e: RMouseEvent<FocusableElement>) => {
onClick?.(e);
});

let triggerSyntheticClick = useEffectEvent((e: KeyboardEvent | TouchEvent, target: FocusableElement) => {
// Some third-party libraries pass in onClick instead of onPress.
// Create a fake mouse event and trigger onClick as well.
// This matches the browser's native activation behavior for certain elements (e.g. button).
// https://html.spec.whatwg.org/#activation
// https://html.spec.whatwg.org/#fire-a-synthetic-pointer-event
if (onClick) {
let event = new MouseEvent('click', e);
setEventTarget(event, target);
onClick(createSyntheticEvent(event));
}
});

let pressProps = useMemo(() => {
let state = ref.current;
let pressProps: DOMAttributes = {
Expand Down Expand Up @@ -362,11 +380,13 @@ export function usePress(props: PressHookProps): PressResult {
let stopPressStart = triggerPressStart(e, 'virtual');
let stopPressUp = triggerPressUp(e, 'virtual');
let stopPressEnd = triggerPressEnd(e, 'virtual');
triggerClick(e);
shouldStopPropagation = stopPressStart && stopPressUp && stopPressEnd;
} else if (state.isPressed && state.pointerType !== 'keyboard') {
let pointerType = state.pointerType || (e.nativeEvent as PointerEvent).pointerType as PointerType || 'virtual';
shouldStopPropagation = triggerPressEnd(createEvent(e.currentTarget, e), pointerType, true);
state.isOverTarget = false;
triggerClick(e);
cancel(e);
}

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

let target = getEventTarget(e);
triggerPressEnd(createEvent(state.target, e), 'keyboard', nodeContains(state.target, getEventTarget(e)));
let wasPressed = nodeContains(state.target, getEventTarget(e));
triggerPressEnd(createEvent(state.target, e), 'keyboard', wasPressed);
if (wasPressed) {
triggerSyntheticClick(e, state.target);
}
removeAllGlobalListeners();

// If a link was triggered with a key other than Enter, open the URL ourselves.
Expand Down Expand Up @@ -723,6 +747,7 @@ export function usePress(props: PressHookProps): PressResult {
if (touch && isOverTarget(touch, e.currentTarget) && state.pointerType != null) {
triggerPressUp(createTouchEvent(state.target!, e), state.pointerType);
shouldStopPropagation = triggerPressEnd(createTouchEvent(state.target!, e), state.pointerType);
triggerSyntheticClick(e.nativeEvent, state.target!);
} else if (state.isOverTarget && state.pointerType != null) {
shouldStopPropagation = triggerPressEnd(createTouchEvent(state.target!, e), state.pointerType, false);
}
Expand Down Expand Up @@ -784,7 +809,9 @@ export function usePress(props: PressHookProps): PressResult {
cancelOnPointerExit,
triggerPressEnd,
triggerPressStart,
triggerPressUp
triggerPressUp,
triggerClick,
triggerSyntheticClick
]);

// Remove user-select: none in case component unmounts immediately after pressStart
Expand Down
69 changes: 19 additions & 50 deletions packages/@react-aria/interactions/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,57 +12,25 @@

import {FocusableElement} from '@react-types/shared';
import {focusWithoutScrolling, getOwnerWindow, isFocusable, useEffectEvent, useLayoutEffect} from '@react-aria/utils';
import {FocusEvent as ReactFocusEvent, useCallback, useRef} from 'react';

export class SyntheticFocusEvent<Target = Element> implements ReactFocusEvent<Target> {
nativeEvent: FocusEvent;
target: EventTarget & Target;
currentTarget: EventTarget & Target;
relatedTarget: Element;
bubbles: boolean;
cancelable: boolean;
defaultPrevented: boolean;
eventPhase: number;
isTrusted: boolean;
timeStamp: number;
type: string;

constructor(type: string, nativeEvent: FocusEvent) {
this.nativeEvent = nativeEvent;
this.target = nativeEvent.target as EventTarget & Target;
this.currentTarget = nativeEvent.currentTarget as EventTarget & Target;
this.relatedTarget = nativeEvent.relatedTarget as Element;
this.bubbles = nativeEvent.bubbles;
this.cancelable = nativeEvent.cancelable;
this.defaultPrevented = nativeEvent.defaultPrevented;
this.eventPhase = nativeEvent.eventPhase;
this.isTrusted = nativeEvent.isTrusted;
this.timeStamp = nativeEvent.timeStamp;
this.type = type;
}

isDefaultPrevented(): boolean {
return this.nativeEvent.defaultPrevented;
}

preventDefault(): void {
this.defaultPrevented = true;
this.nativeEvent.preventDefault();
}

stopPropagation(): void {
this.nativeEvent.stopPropagation();
this.isPropagationStopped = () => true;
}

isPropagationStopped(): boolean {
return false;
}
import {FocusEvent as ReactFocusEvent, SyntheticEvent, useCallback, useRef} from 'react';

// Turn a native event into a React synthetic event.
export function createSyntheticEvent<E extends SyntheticEvent>(nativeEvent: Event): E {
let event = nativeEvent as any as E;
event.nativeEvent = nativeEvent;
event.isDefaultPrevented = () => event.defaultPrevented;
// cancelBubble is technically deprecated in the spec, but still supported in all browsers.
event.isPropagationStopped = () => (event as any).cancelBubble;
event.persist = () => {};
return event;
}

persist() {}
export function setEventTarget(event: Event, target: Element) {
Object.defineProperty(event, 'target', {value: target});
Object.defineProperty(event, 'currentTarget', {value: target});
}

export function useSyntheticBlurEvent<Target = Element>(onBlur: (e: ReactFocusEvent<Target>) => void) {
export function useSyntheticBlurEvent<Target extends Element = Element>(onBlur: (e: ReactFocusEvent<Target>) => void) {
let stateRef = useRef({
isFocused: false,
observer: null as MutationObserver | null
Expand All @@ -80,7 +48,7 @@ export function useSyntheticBlurEvent<Target = Element>(onBlur: (e: ReactFocusEv
};
}, []);

let dispatchBlur = useEffectEvent((e: SyntheticFocusEvent<Target>) => {
let dispatchBlur = useEffectEvent((e: ReactFocusEvent<Target>) => {
onBlur?.(e);
});

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

if (target.disabled) {
// For backward compatibility, dispatch a (fake) React synthetic event.
dispatchBlur(new SyntheticFocusEvent('blur', e as FocusEvent));
let event = createSyntheticEvent<ReactFocusEvent<Target>>(e);
dispatchBlur(event);
}

// We no longer need the MutationObserver once the target is blurred.
Expand Down
Loading

0 comments on commit 6f68c4a

Please sign in to comment.