Skip to content

chore: Pass through more DOM events and attributes #8327

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

devongovett
Copy link
Member

@devongovett devongovett commented May 30, 2025

There have been a few use cases where it would be useful to pass through more native DOM attributes and events in React Aria Components. For example, it's not currently possible to integrate with third party libraries that generate DOM attributes, e.g. adding a third party menu trigger to a RAC button. This was improved recently by adding onClick as an alias for onPress, but other events such as onPointerDown are still not passed through.

This adds a set of global DOM events and attributes that can apply to any DOM element. These are hidden from the docs to avoid making them super long, but will appear in TypeScript and get passed through to the DOM.

Questions to answer:

  • What to do about keyboard and focus events. Currently we expose these only on elements that are focusable. Do we want them to bubble or is this sufficient for now?
  • I'm worried about onClick being misused. For example, this adds onClick to menu items. onAction is the preferred prop there because it applies also on keyboard interactions (browsers only do this for onClick on certain elements). It also has different behaviors depending on the modality, and allows the drag and release behavior. Should we make onClick an alias for onAction like we did for onPress (looks like Radix does this)? What about in more complex cases like onAction in collections, which sometimes gets triggered on double click or other interactions?
  • We will need to add keyboard events to more focusable elements regardless - e.g. looks like MenuItem doesn't pass them through for the same reason it doesn't pass onClick through. But will need to support things like third party libraries.

@rspbot
Copy link

rspbot commented May 30, 2025

Build successful! 🎉

@rspbot
Copy link

rspbot commented May 30, 2025

## API Changes

react-aria-components

/react-aria-components:GridList

 GridList <T extends {}> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children?: ReactNode | ({}) => ReactNode
   className?: string | ((GridListRenderProps & {
     defaultClassName: string | undefined
 })) => string
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: ReadonlyArray<any>
   disabledBehavior?: DisabledBehavior
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   disallowTypeAhead?: boolean = false
   dragAndDropHooks?: DragAndDropHooks
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   id?: string
   items?: Iterable<T>
   keyboardNavigationBehavior?: 'arrow' | 'tab' = 'arrow'
   layout?: 'stack' | 'grid' = 'stack'
   onAction?: (Key) => void
-  onScroll?: (UIEvent<Element>) => void
   onSelectionChange?: (Selection) => void
   renderEmptyState?: (GridListRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionBehavior?: SelectionBehavior
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   style?: CSSProperties | ((GridListRenderProps & {
     defaultStyle: CSSProperties
 })) => CSSProperties | undefined
 }

/react-aria-components:ListBox

 ListBox <T extends {}> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children?: ReactNode | ({}) => ReactNode
   className?: string | ((ListBoxRenderProps & {
     defaultClassName: string | undefined
 })) => string
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: ReadonlyArray<any>
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   dragAndDropHooks?: DragAndDropHooks
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   id?: string
   items?: Iterable<T>
   layout?: 'stack' | 'grid' = 'stack'
   onAction?: (Key) => void
   onBlur?: (FocusEvent<Target>) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
-  onScroll?: (UIEvent<Element>) => void
   onSelectionChange?: (Selection) => void
   orientation?: Orientation = 'vertical'
   renderEmptyState?: (ListBoxRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   shouldFocusOnHover?: boolean
   shouldFocusWrap?: boolean
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   style?: CSSProperties | ((ListBoxRenderProps & {
     defaultStyle: CSSProperties
 })) => CSSProperties | undefined
 }

/react-aria-components:Menu

 Menu <T extends {}> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children?: ReactNode | ({}) => ReactNode
   className?: string | ((MenuRenderProps & {
     defaultClassName: string | undefined
 })) => string
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: ReadonlyArray<any>
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   id?: string
   items?: Iterable<T>
   onAction?: (Key) => void
   onClose?: () => void
-  onScroll?: (UIEvent<Element>) => void
   onSelectionChange?: (Selection) => void
   renderEmptyState?: () => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   slot?: string | null
   style?: CSSProperties | ((MenuRenderProps & {
     defaultStyle: CSSProperties
 })) => CSSProperties | undefined
 }

/react-aria-components:Table

 Table {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   children?: ReactNode
   className?: string | ((TableRenderProps & {
     defaultClassName: string | undefined
 })) => string
   defaultSelectedKeys?: 'all' | Iterable<Key>
   disabledBehavior?: DisabledBehavior = "selection"
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   dragAndDropHooks?: DragAndDropHooks
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   onRowAction?: (Key) => void
-  onScroll?: (UIEvent<Element>) => void
   onSelectionChange?: (Selection) => void
   onSortChange?: (SortDescriptor) => any
   selectedKeys?: 'all' | Iterable<Key>
   selectionBehavior?: SelectionBehavior = "toggle"
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   sortDescriptor?: SortDescriptor
   style?: CSSProperties | ((TableRenderProps & {
     defaultStyle: CSSProperties
 })) => CSSProperties | undefined
 }

/react-aria-components:ResizableTableContainer

 ResizableTableContainer {
   children?: ReactNode
   className?: string
   id?: string
   onResize?: (Map<Key, ColumnSize>) => void
   onResizeEnd?: (Map<Key, ColumnSize>) => void
   onResizeStart?: (Map<Key, ColumnSize>) => void
-  onScroll?: (UIEvent<Element>) => void
   style?: CSSProperties
 }

/react-aria-components:Tree

 Tree <T extends {}> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children?: ReactNode | ({}) => ReactNode
   className?: string | ((TreeRenderProps & {
     defaultClassName: string | undefined
 })) => string
   defaultExpandedKeys?: Iterable<Key>
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: ReadonlyArray<any>
   disabledBehavior?: DisabledBehavior = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   dragAndDropHooks?: DragAndDropHooks
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   expandedKeys?: Iterable<Key>
   id?: string
   items?: Iterable<T>
   keyboardDelegate?: KeyboardDelegate
   onAction?: (Key) => void
   onExpandedChange?: (Set<Key>) => any
-  onScroll?: (UIEvent<Element>) => void
   onSelectionChange?: (Selection) => void
   renderEmptyState?: (TreeEmptyStateRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionBehavior?: SelectionBehavior
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   style?: CSSProperties | ((TreeRenderProps & {
     defaultStyle: CSSProperties
 })) => CSSProperties | undefined
 }

/react-aria-components:GridListProps

 GridListProps <T> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children?: ReactNode | (T) => ReactNode
   className?: string | ((GridListRenderProps & {
     defaultClassName: string | undefined
 })) => string
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: ReadonlyArray<any>
   disabledBehavior?: DisabledBehavior
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   disallowTypeAhead?: boolean = false
   dragAndDropHooks?: DragAndDropHooks
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   id?: string
   items?: Iterable<T>
   keyboardNavigationBehavior?: 'arrow' | 'tab' = 'arrow'
   layout?: 'stack' | 'grid' = 'stack'
   onAction?: (Key) => void
-  onScroll?: (UIEvent<Element>) => void
   onSelectionChange?: (Selection) => void
   renderEmptyState?: (GridListRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionBehavior?: SelectionBehavior
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   style?: CSSProperties | ((GridListRenderProps & {
     defaultStyle: CSSProperties
 })) => CSSProperties | undefined
 }

/react-aria-components:ListBoxProps

 ListBoxProps <T> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children?: ReactNode | (T) => ReactNode
   className?: string | ((ListBoxRenderProps & {
     defaultClassName: string | undefined
 })) => string
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: ReadonlyArray<any>
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   dragAndDropHooks?: DragAndDropHooks
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   id?: string
   items?: Iterable<T>
   layout?: 'stack' | 'grid' = 'stack'
   onAction?: (Key) => void
   onBlur?: (FocusEvent<Target>) => void
   onFocus?: (FocusEvent<Target>) => void
   onFocusChange?: (boolean) => void
-  onScroll?: (UIEvent<Element>) => void
   onSelectionChange?: (Selection) => void
   orientation?: Orientation = 'vertical'
   renderEmptyState?: (ListBoxRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   shouldFocusOnHover?: boolean
   shouldFocusWrap?: boolean
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   style?: CSSProperties | ((ListBoxRenderProps & {
     defaultStyle: CSSProperties
 })) => CSSProperties | undefined
 }

/react-aria-components:MenuProps

 MenuProps <T> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children?: ReactNode | (T) => ReactNode
   className?: string | ((MenuRenderProps & {
     defaultClassName: string | undefined
 })) => string
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: ReadonlyArray<any>
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   id?: string
   items?: Iterable<T>
   onAction?: (Key) => void
   onClose?: () => void
-  onScroll?: (UIEvent<Element>) => void
   onSelectionChange?: (Selection) => void
   renderEmptyState?: () => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   slot?: string | null
   style?: CSSProperties | ((MenuRenderProps & {
     defaultStyle: CSSProperties
 })) => CSSProperties | undefined
 }

/react-aria-components:TableProps

 TableProps {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   children?: ReactNode
   className?: string | ((TableRenderProps & {
     defaultClassName: string | undefined
 })) => string
   defaultSelectedKeys?: 'all' | Iterable<Key>
   disabledBehavior?: DisabledBehavior = "selection"
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   dragAndDropHooks?: DragAndDropHooks
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   onRowAction?: (Key) => void
-  onScroll?: (UIEvent<Element>) => void
   onSelectionChange?: (Selection) => void
   onSortChange?: (SortDescriptor) => any
   selectedKeys?: 'all' | Iterable<Key>
   selectionBehavior?: SelectionBehavior = "toggle"
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   sortDescriptor?: SortDescriptor
   style?: CSSProperties | ((TableRenderProps & {
     defaultStyle: CSSProperties
 })) => CSSProperties | undefined
 }

/react-aria-components:ResizableTableContainerProps

 ResizableTableContainerProps {
   children?: ReactNode
   className?: string
   id?: string
   onResize?: (Map<Key, ColumnSize>) => void
   onResizeEnd?: (Map<Key, ColumnSize>) => void
   onResizeStart?: (Map<Key, ColumnSize>) => void
-  onScroll?: (UIEvent<Element>) => void
   style?: CSSProperties
 }

/react-aria-components:TreeProps

 TreeProps <T> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children?: ReactNode | (T) => ReactNode
   className?: string | ((TreeRenderProps & {
     defaultClassName: string | undefined
 })) => string
   defaultExpandedKeys?: Iterable<Key>
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: ReadonlyArray<any>
   disabledBehavior?: DisabledBehavior = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   dragAndDropHooks?: DragAndDropHooks
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   expandedKeys?: Iterable<Key>
   id?: string
   items?: Iterable<T>
   keyboardDelegate?: KeyboardDelegate
   onAction?: (Key) => void
   onExpandedChange?: (Set<Key>) => any
-  onScroll?: (UIEvent<Element>) => void
   onSelectionChange?: (Selection) => void
   renderEmptyState?: (TreeEmptyStateRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionBehavior?: SelectionBehavior
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   style?: CSSProperties | ((TreeRenderProps & {
     defaultStyle: CSSProperties
 })) => CSSProperties | undefined
 }

@react-aria/utils

/@react-aria/utils:filterDOMProps

 filterDOMProps {
-  props: (DOMProps & AriaLabelingProps & LinkDOMProps)
+  props: (DOMProps & AriaLabelingProps & LinkDOMProps & GlobalDOMAttributes)
   opts: Options
   returnVal: undefined
 }

@react-spectrum/s2

/@react-spectrum/s2:CardView

 CardView <T extends {}> {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children?: ReactNode | (T) => ReactNode
   defaultSelectedKeys?: 'all' | Iterable<Key>
   density?: 'compact' | 'regular' | 'spacious' = 'regular'
   dependencies?: ReadonlyArray<any>
   disabledBehavior?: DisabledBehavior
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   disallowTypeAhead?: boolean = false
   dragAndDropHooks?: DragAndDropHooks
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   id?: string
   items?: Iterable<T>
   layout?: 'grid' | 'waterfall' = 'grid'
   loadingState?: LoadingState
   onAction?: (Key) => void
   onLoadMore?: () => void
-  onScroll?: (UIEvent<Element>) => void
   onSelectionChange?: (Selection) => void
   renderActionBar?: ('all' | Set<Key>) => ReactElement
   renderEmptyState?: (GridListRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionStyle?: 'checkbox' | 'highlight' = 'checkbox'
   shouldSelectOnPressUp?: boolean
   size?: 'XS' | 'S' | 'M' | 'L' | 'XL' = 'M'
   slot?: string | null
   styles?: StylesPropWithHeight
   variant?: 'primary' | 'secondary' | 'tertiary' | 'quiet' = 'primary'
 }

/@react-spectrum/s2:Menu

 Menu <T extends {}> {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children: ReactNode | ({}) => ReactNode
   defaultSelectedKeys?: 'all' | Iterable<Key>
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   hideLinkOutIcon?: boolean
   id?: string
   items?: Iterable<T>
   onAction?: (Key) => void
   onClose?: () => void
-  onScroll?: (UIEvent<Element>) => void
   onSelectionChange?: (Selection) => void
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   shouldFocusWrap?: boolean
   slot?: string | null
   styles?: StylesProp
 }

/@react-spectrum/s2:CardViewProps

 CardViewProps <T> {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children?: ReactNode | (T) => ReactNode
   defaultSelectedKeys?: 'all' | Iterable<Key>
   density?: 'compact' | 'regular' | 'spacious' = 'regular'
   dependencies?: ReadonlyArray<any>
   disabledBehavior?: DisabledBehavior
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   disallowTypeAhead?: boolean = false
   dragAndDropHooks?: DragAndDropHooks
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   id?: string
   items?: Iterable<T>
   layout?: 'grid' | 'waterfall' = 'grid'
   loadingState?: LoadingState
   onAction?: (Key) => void
   onLoadMore?: () => void
-  onScroll?: (UIEvent<Element>) => void
   onSelectionChange?: (Selection) => void
   renderActionBar?: ('all' | Set<Key>) => ReactElement
   renderEmptyState?: (GridListRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionStyle?: 'checkbox' | 'highlight' = 'checkbox'
   shouldSelectOnPressUp?: boolean
   size?: 'XS' | 'S' | 'M' | 'L' | 'XL' = 'M'
   slot?: string | null
   styles?: StylesPropWithHeight
   variant?: 'primary' | 'secondary' | 'tertiary' | 'quiet' = 'primary'
 }

/@react-spectrum/s2:MenuProps

 MenuProps <T> {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children: ReactNode | (T) => ReactNode
   defaultSelectedKeys?: 'all' | Iterable<Key>
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   hideLinkOutIcon?: boolean
   id?: string
   items?: Iterable<T>
   onAction?: (Key) => void
   onClose?: () => void
-  onScroll?: (UIEvent<Element>) => void
   onSelectionChange?: (Selection) => void
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   shouldFocusWrap?: boolean
   slot?: string | null
   styles?: StylesProp
 }

Copy link
Member

@snowystinger snowystinger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A relevant menu item conversation Add onClick/onPress to MenuItem

I think it's ok to omit focus/keyboard at first, see if this is good enough. we can always add it after we see how this goes

I'm also worried about onClick... and I'm not sure what the least bad option is. I think the option that most closely matches our stated goal, to make integration with other libraries easier, and given another library's usage, using an alias is ok I think.


return (
<CollectionBuilder content={<Collection {...props} />}>
{collection => (
<ol
ref={ref}
{...navProps}
{...mergeProps(DOMProps, navProps)}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i assume we should merge ours last always so we win on important attributes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I tried to do that. The events won't conflict (they'll merge) but other ones like inert would.

@devongovett
Copy link
Member Author

Say you mouse down on a menu trigger, then drag over a menu item and release. That will currently trigger onAction. What other events should be emitted?

  • onPressUp – this would then trigger the other events below
  • onPressStart and onPressEnd? – usually defined like onPointerDown. user pressed down on the item itself. so maybe not? also weird that these would happen after onPressUp
  • onPress – usually defined to mean the user pressed and released over the same element. so I could see not firing it, but users may associate this with onAction and expect it to fire. also weird if we do fire onClick but not onPress?
  • onClick – users may expect this to behave like onAction, or wrap a menu item in a third party library that only handles clicks, does event tracking, etc.
  • onAction

Similar conundrum for keyboard events. We trigger onAction during onPressStart for keyboard. Therefore, if the menu closes, there will be no subsequent onPressEnd, onPress, or onClick fired. Which of these should we simulate?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants