Skip to content

feat: Support async Menus in RAC - #10494

Open
LFDanLu wants to merge 7 commits into
mainfrom
async_menu_rac
Open

feat: Support async Menus in RAC #10494
LFDanLu wants to merge 7 commits into
mainfrom
async_menu_rac

Conversation

@LFDanLu

@LFDanLu LFDanLu commented Aug 21, 2026

Copy link
Copy Markdown
Member

For coworker use case

This adds basic async loading/empty state support to Menu. Essentially the same implementation/behavior as our other collection components.

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit tests and storybook for this change (for new code or code which already has tests).
  • Filled out test instructions.
  • Updated documentation (if it already exists for this component).
  • Looked at the Accessibility Practices for this feature - Aria Practices
  • I understand every change in this PR and can explain why it's there.
  • If AI-assisted, I followed our AI contribution guidance and pointed my assistant at CLAUDE.md.

📝 Test Instructions:

Test the RAC Async Menu story and RAC Async Menu docs. It should load items when scrolling to the bottom and render a spinner. Empty state should render what ever the user provided via renderEmptyState

🧢 Your Project:

RSP

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

basically ListBox's implementation

@rspbot

rspbot commented Aug 21, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Aug 25, 2026

Copy link
Copy Markdown

@LFDanLu
LFDanLu marked this pull request as ready for review August 25, 2026 16:54
@LFDanLu LFDanLu changed the title feat: (WIP) Async Menu support feat: Support async Menus in RAC Aug 25, 2026
@LFDanLu LFDanLu changed the title feat: Support async Menus in RAC feat: (WIP) Support async Menus in RAC Aug 25, 2026
@LFDanLu LFDanLu changed the title feat: (WIP) Support async Menus in RAC feat: Support async Menus in RAC Aug 25, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

noticed the JSDocs didn't match the actual default class names

@rspbot

rspbot commented Aug 25, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Aug 25, 2026

Copy link
Copy Markdown
## API Changes

react-aria-components

/react-aria-components:GridListLoadMoreItem

 GridListLoadMoreItem {
   children?: ReactNode
-  className?: string = 'react-aria-GridListLoadMoreItem'
+  className?: string = 'react-aria-GridListLoadingIndicator'
   isLoading?: boolean
   onLoadMore?: () => any
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
   scrollOffset?: number = 1
 }

/react-aria-components:ListBoxLoadMoreItem

 ListBoxLoadMoreItem {
   children?: ReactNode
-  className?: string = 'react-aria-ListBoxLoadMoreItem'
+  className?: string = 'react-aria-ListBoxLoadingIndicator'
   isLoading?: boolean
   onLoadMore?: () => any
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
   scrollOffset?: number = 1
 }

/react-aria-components:TableLoadMoreItem

 TableLoadMoreItem {
   children?: ReactNode
-  className?: string = 'react-aria-TableLoadMoreItem'
+  className?: string = 'react-aria-TableLoadingIndicator'
   direction?: 'start' | 'end' = 'end'
   isLoading?: boolean
   onLoadMore?: () => any
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
   style?: CSSProperties
 }

/react-aria-components:NavigationTree

-NavigationTree <T> {
-  aria-describedby?: string
-  aria-details?: string
-  aria-label?: string
-  aria-labelledby?: string
-  autoFocus?: boolean | FocusStrategy
-  children?: ReactNode | (T) => ReactNode
-  className?: ClassNameOrFunction<NavigationTreeRenderProps> = 'react-aria-NavigationTree'
-  defaultExpandedKeys?: Iterable<Key>
-  dependencies?: ReadonlyArray<any>
-  disabledKeys?: Iterable<Key>
-  expandedKeys?: Iterable<Key>
-  id?: string
-  items?: Iterable<T>
-  onExpandedChange?: (Set<Key>) => any
-  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, NavigationTreeRenderProps>
-  selectedRoute?: string | null
-  slot?: string | null
-  style?: StyleOrFunction<NavigationTreeRenderProps>
-}

/react-aria-components:NavigationTreeItem

-NavigationTreeItem <T extends {}> {
-  aria-label?: string
-  children: ReactNode
-  className?: ClassNameOrFunction<NavigationTreeItemRenderProps> = 'react-aria-NavigationTreeItem'
-  download?: boolean | string
-  hasChildItems?: boolean
-  href?: Href
-  hrefLang?: string
-  id?: Key
-  isDisabled?: boolean
-  onClick?: (MouseEvent<FocusableElement>) => void
-  onHoverChange?: (boolean) => void
-  onHoverEnd?: (HoverEvent) => void
-  onHoverStart?: (HoverEvent) => void
-  onPress?: (PressEvent) => void
-  onPressChange?: (boolean) => void
-  onPressEnd?: (PressEvent) => void
-  onPressStart?: (PressEvent) => void
-  onPressUp?: (PressEvent) => void
-  ping?: string
-  referrerPolicy?: HTMLAttributeReferrerPolicy
-  rel?: string
-  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, NavigationTreeItemRenderProps>
-  routerOptions?: RouterOptions
-  style?: StyleOrFunction<NavigationTreeItemRenderProps>
-  target?: HTMLAttributeAnchorTarget
-  textValue: string
-}

/react-aria-components:NavigationTreeItemContent

-NavigationTreeItemContent {
-  children: ChildrenOrFunction<NavigationTreeItemContentRenderProps>
-}

/react-aria-components:NavigationTreeSection

-NavigationTreeSection <T extends {}> {
-  aria-label?: string
-  children?: ReactNode | (T) => ReactElement
-  className?: string
-  dependencies?: ReadonlyArray<any>
-  id?: Key
-  items?: Iterable<T>
-  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
-  style?: CSSProperties
-}

/react-aria-components:NavigationTreeHeader

-NavigationTreeHeader {
-  children?: ReactNode
-  className?: string
-  id?: string
-  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
-  style?: CSSProperties
-}

/react-aria-components:NavigationTreeContext

-NavigationTreeContext {
-  UNTYPED
-}

/react-aria-components:TreeLoadMoreItem

 TreeLoadMoreItem <T> {
   children?: ChildrenOrFunction<TreeLoadMoreItemRenderProps>
-  className?: ClassNameOrFunction<TreeLoadMoreItemRenderProps> = 'react-aria-TreeLoadMoreItem'
+  className?: ClassNameOrFunction<TreeLoadMoreItemRenderProps> = 'react-aria-TreeLoader'
   isLoading?: boolean
   onLoadMore?: () => any
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, TreeLoadMoreItemRenderProps>
   scrollOffset?: number = 1
 }

/react-aria-components:setInteractionModality

-setInteractionModality {
-  modality: Modality
-  returnVal: undefined
-}

/react-aria-components:GridListLoadMoreItemProps

 GridListLoadMoreItemProps {
   children?: ReactNode
-  className?: string = 'react-aria-GridListLoadMoreItem'
+  className?: string = 'react-aria-GridListLoadingIndicator'
   isLoading?: boolean
   onLoadMore?: () => any
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
   scrollOffset?: number = 1
 }

/react-aria-components:ListBoxLoadMoreItemProps

 ListBoxLoadMoreItemProps {
   children?: ReactNode
-  className?: string = 'react-aria-ListBoxLoadMoreItem'
+  className?: string = 'react-aria-ListBoxLoadingIndicator'
   isLoading?: boolean
   onLoadMore?: () => any
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
   scrollOffset?: number = 1
 }

/react-aria-components:TableLoadMoreItemProps

 TableLoadMoreItemProps {
   children?: ReactNode
-  className?: string = 'react-aria-TableLoadMoreItem'
+  className?: string = 'react-aria-TableLoadingIndicator'
   direction?: 'start' | 'end' = 'end'
   isLoading?: boolean
   onLoadMore?: () => any
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
   style?: CSSProperties
 }

/react-aria-components:NavigationTreeProps

-NavigationTreeProps <T> {
-  aria-describedby?: string
-  aria-details?: string
-  aria-label?: string
-  aria-labelledby?: string
-  autoFocus?: boolean | FocusStrategy
-  children?: ReactNode | (T) => ReactNode
-  className?: ClassNameOrFunction<NavigationTreeRenderProps> = 'react-aria-NavigationTree'
-  defaultExpandedKeys?: Iterable<Key>
-  dependencies?: ReadonlyArray<any>
-  disabledKeys?: Iterable<Key>
-  expandedKeys?: Iterable<Key>
-  id?: string
-  items?: Iterable<T>
-  onExpandedChange?: (Set<Key>) => any
-  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, NavigationTreeRenderProps>
-  selectedRoute?: string | null
-  slot?: string | null
-  style?: StyleOrFunction<NavigationTreeRenderProps>
-}

/react-aria-components:NavigationTreeRenderProps

-NavigationTreeRenderProps {
-  isEmpty: boolean
-  isFocusVisible: boolean
-  isFocused: boolean
-  state: TreeState<unknown>
-}

/react-aria-components:NavigationTreeItemProps

-NavigationTreeItemProps <T = {}> {
-  aria-label?: string
-  children: ReactNode
-  className?: ClassNameOrFunction<NavigationTreeItemRenderProps> = 'react-aria-NavigationTreeItem'
-  download?: boolean | string
-  hasChildItems?: boolean
-  href?: Href
-  hrefLang?: string
-  id?: Key
-  isDisabled?: boolean
-  onClick?: (MouseEvent<FocusableElement>) => void
-  onHoverChange?: (boolean) => void
-  onHoverEnd?: (HoverEvent) => void
-  onHoverStart?: (HoverEvent) => void
-  onPress?: (PressEvent) => void
-  onPressChange?: (boolean) => void
-  onPressEnd?: (PressEvent) => void
-  onPressStart?: (PressEvent) => void
-  onPressUp?: (PressEvent) => void
-  ping?: string
-  referrerPolicy?: HTMLAttributeReferrerPolicy
-  rel?: string
-  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, NavigationTreeItemRenderProps>
-  routerOptions?: RouterOptions
-  style?: StyleOrFunction<NavigationTreeItemRenderProps>
-  target?: HTMLAttributeAnchorTarget
-  textValue: string
-}

/react-aria-components:NavigationTreeItemRenderProps

-NavigationTreeItemRenderProps {
-  allowsDragging?: boolean
-  hasChildItems: boolean
-  id: Key
-  isCurrent: boolean
-  isCurrentAncestor: boolean
-  isDisabled: boolean
-  isDragging?: boolean
-  isDropTarget?: boolean
-  isExpanded: boolean
-  isFocusVisible: boolean
-  isFocusVisibleWithin: boolean
-  isFocused: boolean
-  isHovered: boolean
-  isPressed: boolean
-  isSelected: boolean
-  level: number
-  selectionBehavior: SelectionBehavior
-  selectionMode: SelectionMode
-  state: TreeState<unknown>
-}

/react-aria-components:NavigationTreeItemContentProps

-NavigationTreeItemContentProps {
-  children: ChildrenOrFunction<NavigationTreeItemContentRenderProps>
-}

/react-aria-components:NavigationTreeItemContentRenderProps

-NavigationTreeItemContentRenderProps {
-  allowsDragging?: boolean
-  hasChildItems: boolean
-  id: Key
-  isCurrent: boolean
-  isCurrentAncestor: boolean
-  isDisabled: boolean
-  isDragging?: boolean
-  isDropTarget?: boolean
-  isExpanded: boolean
-  isFocusVisible: boolean
-  isFocusVisibleWithin: boolean
-  isFocused: boolean
-  isHovered: boolean
-  isPressed: boolean
-  isSelected: boolean
-  level: number
-  selectionBehavior: SelectionBehavior
-  selectionMode: SelectionMode
-  state: TreeState<unknown>
-}

/react-aria-components:NavigationTreeSectionProps

-NavigationTreeSectionProps <T> {
-  aria-label?: string
-  children?: ReactNode | (T) => ReactElement
-  className?: string
-  dependencies?: ReadonlyArray<any>
-  id?: Key
-  items?: Iterable<T>
-  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
-  style?: CSSProperties
-}

/react-aria-components:NavigationTreeHeaderProps

-NavigationTreeHeaderProps {
-  children?: ReactNode
-  className?: string
-  id?: string
-  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
-  style?: CSSProperties
-}

/react-aria-components:TreeLoadMoreItemProps

 TreeLoadMoreItemProps {
   children?: ChildrenOrFunction<TreeLoadMoreItemRenderProps>
-  className?: ClassNameOrFunction<TreeLoadMoreItemRenderProps> = 'react-aria-TreeLoadMoreItem'
+  className?: ClassNameOrFunction<TreeLoadMoreItemRenderProps> = 'react-aria-TreeLoader'
   isLoading?: boolean
   onLoadMore?: () => any
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, TreeLoadMoreItemRenderProps>
   scrollOffset?: number = 1
 }

/react-aria-components:Modality

-Modality {
-  M: undefined
-}

/react-aria-components:MenuLoadMoreItem

+MenuLoadMoreItem {
+  children?: ReactNode
+  className?: string = 'react-aria-MenuLoadingIndicator'
+  isLoading?: boolean
+  onLoadMore?: () => any
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
+  scrollOffset?: number = 1
+  style?: CSSProperties
+}

/react-aria-components:MenuLoadMoreItemProps

+MenuLoadMoreItemProps {
+  children?: ReactNode
+  className?: string = 'react-aria-MenuLoadingIndicator'
+  isLoading?: boolean
+  onLoadMore?: () => any
+  render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
+  scrollOffset?: number = 1
+  style?: CSSProperties
+}

@react-spectrum/ai

/@react-spectrum/ai:AttachmentPreview

-AttachmentPreview {
-  UNSAFE_className?: UnsafeClassName
-  UNSAFE_style?: CSSProperties
-  alt?: string
-  crossOrigin?: 'anonymous' | 'use-credentials'
-  decoding?: 'async' | 'auto' | 'sync'
-  fetchPriority?: 'high' | 'low' | 'auto'
-  group?: ImageGroup
-  height?: number
-  itemProp?: string
-  loading?: 'eager' | 'lazy'
-  mimeType: string
-  referrerPolicy?: HTMLAttributeReferrerPolicy
-  renderError?: () => ReactNode
-  slot?: string | null
-  src?: string | Array<ImageSource>
-  styles?: StyleString
-  width?: number
-}

/@react-spectrum/ai:ThreadLoadMoreItem

 ThreadLoadMoreItem {
   children?: ReactNode
-  className?: string = 'react-aria-GridListLoadMoreItem'
+  className?: string = 'react-aria-GridListLoadingIndicator'
   isLoading?: boolean
   onLoadMore?: () => any
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
   scrollOffset?: number = 1
 }

/@react-spectrum/ai:AttachmentPreviewProps

-AttachmentPreviewProps {
-  UNSAFE_className?: UnsafeClassName
-  UNSAFE_style?: CSSProperties
-  alt?: string
-  crossOrigin?: 'anonymous' | 'use-credentials'
-  decoding?: 'async' | 'auto' | 'sync'
-  fetchPriority?: 'high' | 'low' | 'auto'
-  group?: ImageGroup
-  height?: number
-  itemProp?: string
-  loading?: 'eager' | 'lazy'
-  mimeType: string
-  referrerPolicy?: HTMLAttributeReferrerPolicy
-  renderError?: () => ReactNode
-  slot?: string | null
-  src?: string | Array<ImageSource>
-  styles?: StyleString
-  width?: number
-}

/@react-spectrum/ai:ThreadLoadMoreItemProps

 ThreadLoadMoreItemProps {
   children?: ReactNode
-  className?: string = 'react-aria-GridListLoadMoreItem'
+  className?: string = 'react-aria-GridListLoadingIndicator'
   isLoading?: boolean
   onLoadMore?: () => any
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, undefined>
   scrollOffset?: number = 1
 }

@react-spectrum/s2

/@react-spectrum/s2:Avatar

 Avatar {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   alt?: string
   id?: string
   isOverBackground?: boolean
   size?: number | number | number | number | number | number | number | number | number | number | number | number | number | number | (number & {
   
-}) | `${number}lh` = 24
+}) = 24
   slot?: string | null
   src?: string
   styles?: StylesPropWithoutWidth
 }

/@react-spectrum/s2:AvatarProps

 AvatarProps {
   UNSAFE_className?: UnsafeClassName
   UNSAFE_style?: CSSProperties
   alt?: string
   id?: string
   isOverBackground?: boolean
   size?: number | number | number | number | number | number | number | number | number | number | number | number | number | number | (number & {
   
-}) | `${number}lh` = 24
+}) = 24
   slot?: string | null
   src?: string
   styles?: StylesPropWithoutWidth
 }

@rspbot

rspbot commented Aug 25, 2026

Copy link
Copy Markdown

Agent Skills Changes

Added (450)

… output truncated to fit GitHub comment size limit.

...otherProps,
id: undefined,
children: item.rendered,
// TODO: this isn't consistent with other components

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Still TODO?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants