Skip to content

Commit 7c3fe1f

Browse files
authored
chore: Pass through more DOM events and attributes (#8327)
* chore: Pass through more DOM events and attributes * fix * fix test * Emit onClick from MenuItem * Add PressEvents to selectable items * Add PressEvents to toggles * fixes * weird batching difference in 17 tests? * fix * fix
1 parent 849e52a commit 7c3fe1f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+1015
-379
lines changed

packages/@react-aria/autocomplete/src/useAutocomplete.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,13 @@ export function useAutocomplete(props: AriaAutocompleteOptions, state: Autocompl
272272
clearVirtualFocus();
273273
break;
274274
}
275+
case 'Enter':
276+
// Trigger click action on item when Enter key was pressed.
277+
if (focusedNodeId != null) {
278+
let item = document.getElementById(focusedNodeId);
279+
item?.click();
280+
}
281+
break;
275282
}
276283
}
277284
};

packages/@react-aria/link/src/useLink.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import {AriaLinkProps} from '@react-types/link';
1414
import {DOMAttributes, FocusableElement, RefObject} from '@react-types/shared';
15-
import {filterDOMProps, mergeProps, shouldClientNavigate, useLinkProps, useRouter} from '@react-aria/utils';
15+
import {filterDOMProps, handleLinkClick, mergeProps, useLinkProps, useRouter} from '@react-aria/utils';
1616
import React from 'react';
1717
import {useFocusable, usePress} from '@react-aria/interactions';
1818

@@ -72,20 +72,7 @@ export function useLink(props: AriaLinkOptions, ref: RefObject<FocusableElement
7272
'aria-current': props['aria-current'],
7373
onClick: (e: React.MouseEvent<HTMLAnchorElement>) => {
7474
pressProps.onClick?.(e);
75-
76-
// If a custom router is provided, prevent default and forward if this link should client navigate.
77-
if (
78-
!router.isNative &&
79-
e.currentTarget instanceof HTMLAnchorElement &&
80-
e.currentTarget.href &&
81-
// If props are applied to a router Link component, it may have already prevented default.
82-
!e.isDefaultPrevented() &&
83-
shouldClientNavigate(e.currentTarget, e) &&
84-
props.href
85-
) {
86-
e.preventDefault();
87-
router.open(e.currentTarget, e, props.href, props.routerOptions);
88-
}
75+
handleLinkClick(e, router, props.href, props.routerOptions);
8976
}
9077
})
9178
};

packages/@react-aria/menu/src/useMenuItem.ts

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {DOMAttributes, DOMProps, FocusableElement, FocusEvents, HoverEvents, Key, KeyboardEvents, PressEvent, PressEvents, RefObject, RouterOptions} from '@react-types/shared';
14-
import {filterDOMProps, mergeProps, useLinkProps, useRouter, useSlotId} from '@react-aria/utils';
13+
import {DOMAttributes, DOMProps, FocusableElement, FocusEvents, HoverEvents, Key, KeyboardEvents, PressEvent, PressEvents, RefObject} from '@react-types/shared';
14+
import {filterDOMProps, handleLinkClick, mergeProps, useLinkProps, useRouter, useSlotId} from '@react-aria/utils';
1515
import {getItemCount} from '@react-stately/collections';
1616
import {isFocusVisible, useFocus, useHover, useKeyboard, usePress} from '@react-aria/interactions';
1717
import {menuData} from './utils';
18+
import {MouseEvent, useRef} from 'react';
1819
import {SelectionManager} from '@react-stately/selection';
1920
import {TreeState} from '@react-stately/tree';
2021
import {useSelectableItem} from '@react-aria/selection';
@@ -112,9 +113,10 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
112113
'aria-haspopup': hasPopup,
113114
onPressStart: pressStartProp,
114115
onPressUp: pressUpProp,
115-
onPress: pressProp,
116-
onPressChange,
116+
onPress,
117+
onPressChange: pressChangeProp,
117118
onPressEnd,
119+
onClick: onClickProp,
118120
onHoverStart: hoverStartProp,
119121
onHoverChange,
120122
onHoverEnd,
@@ -134,7 +136,7 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
134136
let item = state.collection.getItem(key);
135137
let onClose = props.onClose || data.onClose;
136138
let router = useRouter();
137-
let performAction = (e: PressEvent) => {
139+
let performAction = () => {
138140
if (isTrigger) {
139141
return;
140142
}
@@ -150,10 +152,6 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
150152
let onAction = data.onAction;
151153
onAction(key);
152154
}
153-
154-
if (e.target instanceof HTMLAnchorElement && item) {
155-
router.open(e.target, e, item.props.href, item.props.routerOptions as RouterOptions);
156-
}
157155
};
158156

159157
let role = 'menuitem';
@@ -191,39 +189,41 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
191189
}
192190

193191
let onPressStart = (e: PressEvent) => {
194-
if (e.pointerType === 'keyboard') {
195-
performAction(e);
192+
// Trigger native click event on keydown unless this is a link (the browser will trigger onClick then).
193+
if (e.pointerType === 'keyboard' && !selectionManager.isLink(key)) {
194+
(e.target as HTMLElement).click();
196195
}
197196

198197
pressStartProp?.(e);
199198
};
200-
201-
let maybeClose = () => {
202-
// Pressing a menu item should close by default in single selection mode but not multiple
203-
// selection mode, except if overridden by the closeOnSelect prop.
204-
if (!isTrigger && onClose && (closeOnSelect ?? (selectionManager.selectionMode !== 'multiple' || selectionManager.isLink(key)))) {
205-
onClose();
206-
}
199+
let isPressedRef = useRef(false);
200+
let onPressChange = (isPressed: boolean) => {
201+
pressChangeProp?.(isPressed);
202+
isPressedRef.current = isPressed;
207203
};
208204

209205
let onPressUp = (e: PressEvent) => {
210206
// If interacting with mouse, allow the user to mouse down on the trigger button,
211207
// drag, and release over an item (matching native behavior).
212208
if (e.pointerType === 'mouse') {
213-
performAction(e);
214-
maybeClose();
209+
if (!isPressedRef.current) {
210+
(e.target as HTMLElement).click();
211+
}
212+
}
213+
214+
// Pressing a menu item should close by default in single selection mode but not multiple
215+
// selection mode, except if overridden by the closeOnSelect prop.
216+
if (e.pointerType !== 'keyboard' && !isTrigger && onClose && (closeOnSelect ?? (selectionManager.selectionMode !== 'multiple' || selectionManager.isLink(key)))) {
217+
onClose();
215218
}
216219

217220
pressUpProp?.(e);
218221
};
219222

220-
let onPress = (e: PressEvent) => {
221-
if (e.pointerType !== 'keyboard' && e.pointerType !== 'mouse') {
222-
performAction(e);
223-
maybeClose();
224-
}
225-
226-
pressProp?.(e);
223+
let onClick = (e: MouseEvent<FocusableElement>) => {
224+
onClickProp?.(e);
225+
performAction();
226+
handleLinkClick(e, router, item!.props.href, item?.props.routerOptions);
227227
};
228228

229229
let {itemProps, isFocused} = useSelectableItem({
@@ -315,7 +315,8 @@ export function useMenuItem<T>(props: AriaMenuItemProps, state: TreeState<T>, re
315315
keyboardProps,
316316
focusProps,
317317
// Prevent DOM focus from moving on mouse down when using virtual focus or this is a submenu/subdialog trigger.
318-
data.shouldUseVirtualFocus || isTrigger ? {onMouseDown: e => e.preventDefault()} : undefined
318+
data.shouldUseVirtualFocus || isTrigger ? {onMouseDown: e => e.preventDefault()} : undefined,
319+
isDisabled ? undefined : {onClick}
319320
),
320321
// If a submenu is expanded, set the tabIndex to -1 so that shift tabbing goes out of the menu instead of the parent menu item.
321322
tabIndex: itemProps.tabIndex != null && isTriggerExpanded && !data.shouldUseVirtualFocus ? -1 : itemProps.tabIndex

packages/@react-aria/radio/src/useRadio.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ export function useRadio(props: AriaRadioProps, state: RadioGroupState, ref: Ref
4444
value,
4545
children,
4646
'aria-label': ariaLabel,
47-
'aria-labelledby': ariaLabelledby
47+
'aria-labelledby': ariaLabelledby,
48+
onPressStart,
49+
onPressEnd,
50+
onPressChange,
51+
onPress,
52+
onPressUp,
53+
onClick
4854
} = props;
4955

5056
const isDisabled = props.isDisabled || state.isDisabled;
@@ -64,13 +70,25 @@ export function useRadio(props: AriaRadioProps, state: RadioGroupState, ref: Ref
6470

6571
// Handle press state for keyboard interactions and cases where labelProps is not used.
6672
let {pressProps, isPressed} = usePress({
73+
onPressStart,
74+
onPressEnd,
75+
onPressChange,
76+
onPress,
77+
onPressUp,
78+
onClick,
6779
isDisabled
6880
});
6981

7082
// Handle press state on the label.
7183
let {pressProps: labelProps, isPressed: isLabelPressed} = usePress({
84+
onPressStart,
85+
onPressEnd,
86+
onPressChange,
87+
onPressUp,
88+
onClick,
7289
isDisabled,
73-
onPress() {
90+
onPress(e) {
91+
onPress?.(e);
7492
state.setSelectedValue(value);
7593
ref.current?.focus();
7694
}

packages/@react-aria/selection/src/useSelectableItem.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13+
import {chain, isCtrlKeyPressed, mergeProps, openLink, useId, useRouter} from '@react-aria/utils';
1314
import {DOMAttributes, DOMProps, FocusableElement, Key, LongPressEvent, PointerType, PressEvent, RefObject} from '@react-types/shared';
1415
import {focusSafely, PressHookProps, useLongPress, usePress} from '@react-aria/interactions';
1516
import {getCollectionId, isNonContiguousSelectionModifier} from './utils';
16-
import {isCtrlKeyPressed, mergeProps, openLink, useId, useRouter} from '@react-aria/utils';
1717
import {moveVirtualFocus} from '@react-aria/focus';
1818
import {MultipleSelectionManager} from '@react-stately/selection';
1919
import {useEffect, useRef} from 'react';
@@ -220,15 +220,15 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
220220
let longPressEnabled = hasAction && allowsSelection;
221221
let longPressEnabledOnPressStart = useRef(false);
222222
let hadPrimaryActionOnPressStart = useRef(false);
223+
let collectionItemProps = manager.getItemProps(key);
223224

224225
let performAction = (e) => {
225226
if (onAction) {
226227
onAction();
227228
}
228229

229230
if (hasLinkAction && ref.current) {
230-
let itemProps = manager.getItemProps(key);
231-
router.open(ref.current, e, itemProps.href, itemProps.routerOptions);
231+
router.open(ref.current, e, collectionItemProps.href, collectionItemProps.routerOptions);
232232
}
233233
};
234234

@@ -337,6 +337,14 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
337337
});
338338
}
339339

340+
if (collectionItemProps) {
341+
for (let key of ['onPressStart', 'onPressEnd', 'onPressChange', 'onPress', 'onPressUp', 'onClick']) {
342+
if (collectionItemProps[key]) {
343+
itemPressProps[key] = chain(itemPressProps[key], collectionItemProps[key]);
344+
}
345+
}
346+
}
347+
340348
let {pressProps, isPressed} = usePress(itemPressProps);
341349

342350
// Double clicking with a mouse with selectionBehavior = 'replace' performs an action.
@@ -373,7 +381,7 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
373381

374382
// Prevent default on link clicks so that we control exactly
375383
// when they open (to match selection behavior).
376-
let onClick = manager.isLink(key) ? e => {
384+
let onClick = linkBehavior !== 'none' && manager.isLink(key) ? e => {
377385
if (!(openLink as any).isOpening) {
378386
e.preventDefault();
379387
}

packages/@react-aria/toggle/src/useToggle.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,13 @@ export function useToggle(props: AriaToggleProps, state: ToggleState, ref: RefOb
4848
'aria-label': ariaLabel,
4949
'aria-labelledby': ariaLabelledby,
5050
validationState = 'valid',
51-
isInvalid
51+
isInvalid,
52+
onPressStart,
53+
onPressEnd,
54+
onPressChange,
55+
onPress,
56+
onPressUp,
57+
onClick
5258
} = props;
5359

5460
let onChange = (e) => {
@@ -66,12 +72,24 @@ export function useToggle(props: AriaToggleProps, state: ToggleState, ref: RefOb
6672

6773
// Handle press state for keyboard interactions and cases where labelProps is not used.
6874
let {pressProps, isPressed} = usePress({
75+
onPressStart,
76+
onPressEnd,
77+
onPressChange,
78+
onPress,
79+
onPressUp,
80+
onClick,
6981
isDisabled
7082
});
7183

7284
// Handle press state on the label.
7385
let {pressProps: labelProps, isPressed: isLabelPressed} = usePress({
74-
onPress() {
86+
onPressStart,
87+
onPressEnd,
88+
onPressChange,
89+
onPressUp,
90+
onClick,
91+
onPress(e) {
92+
onPress?.(e);
7593
state.toggle();
7694
ref.current?.focus();
7795
},

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

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {AriaLabelingProps, DOMProps, LinkDOMProps} from '@react-types/shared';
13+
import {AriaLabelingProps, DOMProps, GlobalDOMAttributes, LinkDOMProps} from '@react-types/shared';
1414

1515
const DOMPropNames = new Set([
1616
'id'
@@ -34,13 +34,62 @@ const linkPropNames = new Set([
3434
'referrerPolicy'
3535
]);
3636

37+
const globalAttrs = new Set([
38+
'dir',
39+
'lang',
40+
'hidden',
41+
'inert',
42+
'translate'
43+
]);
44+
45+
const globalEvents = new Set([
46+
'onClick',
47+
'onAuxClick',
48+
'onContextMenu',
49+
'onDoubleClick',
50+
'onMouseDown',
51+
'onMouseEnter',
52+
'onMouseLeave',
53+
'onMouseMove',
54+
'onMouseOut',
55+
'onMouseOver',
56+
'onMouseUp',
57+
'onTouchCancel',
58+
'onTouchEnd',
59+
'onTouchMove',
60+
'onTouchStart',
61+
'onPointerDown',
62+
'onPointerMove',
63+
'onPointerUp',
64+
'onPointerCancel',
65+
'onPointerEnter',
66+
'onPointerLeave',
67+
'onPointerOver',
68+
'onPointerOut',
69+
'onGotPointerCapture',
70+
'onLostPointerCapture',
71+
'onScroll',
72+
'onWheel',
73+
'onAnimationStart',
74+
'onAnimationEnd',
75+
'onAnimationIteration',
76+
'onTransitionCancel',
77+
'onTransitionEnd',
78+
'onTransitionRun',
79+
'onTransitionStart'
80+
]);
81+
3782
interface Options {
3883
/**
3984
* If labelling associated aria properties should be included in the filter.
4085
*/
4186
labelable?: boolean,
4287
/** Whether the element is a link and should include DOM props for <a> elements. */
4388
isLink?: boolean,
89+
/** Whether to include global DOM attributes. */
90+
global?: boolean,
91+
/** Whether to include DOM events. */
92+
events?: boolean,
4493
/**
4594
* A Set of other property names that should be included in the filter.
4695
*/
@@ -54,8 +103,8 @@ const propRe = /^(data-.*)$/;
54103
* @param props - The component props to be filtered.
55104
* @param opts - Props to override.
56105
*/
57-
export function filterDOMProps(props: DOMProps & AriaLabelingProps & LinkDOMProps, opts: Options = {}): DOMProps & AriaLabelingProps {
58-
let {labelable, isLink, propNames} = opts;
106+
export function filterDOMProps(props: DOMProps & AriaLabelingProps & LinkDOMProps & GlobalDOMAttributes, opts: Options = {}): DOMProps & AriaLabelingProps & GlobalDOMAttributes {
107+
let {labelable, isLink, global, events = global, propNames} = opts;
59108
let filteredProps = {};
60109

61110
for (const prop in props) {
@@ -64,6 +113,8 @@ export function filterDOMProps(props: DOMProps & AriaLabelingProps & LinkDOMProp
64113
DOMPropNames.has(prop) ||
65114
(labelable && labelablePropNames.has(prop)) ||
66115
(isLink && linkPropNames.has(prop)) ||
116+
(global && globalAttrs.has(prop)) ||
117+
(events && globalEvents.has(prop) || (prop.endsWith('Capture') && globalEvents.has(prop.slice(0, -7)))) ||
67118
propNames?.has(prop) ||
68119
propRe.test(prop)
69120
)

0 commit comments

Comments
 (0)