Skip to content

feat: (WIP) Thread ai component#10045

Draft
LFDanLu wants to merge 15 commits into
mainfrom
thread_ai_component
Draft

feat: (WIP) Thread ai component#10045
LFDanLu wants to merge 15 commits into
mainfrom
thread_ai_component

Conversation

@LFDanLu
Copy link
Copy Markdown
Member

@LFDanLu LFDanLu commented May 11, 2026

Closes

✅ 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

📝 Test Instructions:

🧢 Your Project:

@rspbot
Copy link
Copy Markdown

rspbot commented May 11, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented May 13, 2026

@rspbot
Copy link
Copy Markdown

rspbot commented May 13, 2026

## 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?: ClassNameOrFunction<GridListRenderProps> = 'react-aria-GridList'
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: ReadonlyArray<any>
   disabledBehavior?: DisabledBehavior = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   disallowTypeAhead?: boolean = false
   dragAndDropHooks?: DragAndDropHooks<NoInfer<{}>>
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
+  focusOnEntry?: 'first' | 'last'
   id?: string
   items?: Iterable<T>
   keyboardNavigationBehavior?: 'arrow' | 'tab' = 'arrow'
   layout?: 'stack' | 'grid' = 'stack'
   onSelectionChange?: (Selection) => void
   orientation?: Orientation = 'vertical'
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, GridListRenderProps>
   renderEmptyState?: (GridListRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionBehavior?: SelectionBehavior = 'toggle'
   selectionMode?: SelectionMode
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   style?: StyleOrFunction<GridListRenderProps>
 }

/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?: ClassNameOrFunction<GridListRenderProps> = 'react-aria-GridList'
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: ReadonlyArray<any>
   disabledBehavior?: DisabledBehavior = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   disallowTypeAhead?: boolean = false
   dragAndDropHooks?: DragAndDropHooks<NoInfer<T>>
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
+  focusOnEntry?: 'first' | 'last'
   id?: string
   items?: Iterable<T>
   keyboardNavigationBehavior?: 'arrow' | 'tab' = 'arrow'
   layout?: 'stack' | 'grid' = 'stack'
   onSelectionChange?: (Selection) => void
   orientation?: Orientation = 'vertical'
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, GridListRenderProps>
   renderEmptyState?: (GridListRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionBehavior?: SelectionBehavior = 'toggle'
   selectionMode?: SelectionMode
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   style?: StyleOrFunction<GridListRenderProps>
 }

@react-aria/gridlist

/@react-aria/gridlist:AriaGridListOptions

 AriaGridListOptions <T> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   defaultSelectedKeys?: 'all' | Iterable<Key>
   disabledBehavior?: DisabledBehavior = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   disallowTypeAhead?: boolean = false
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
+  focusOnEntry?: 'first' | 'last'
   id?: string
   isVirtualized?: boolean
   items?: Iterable<T>
   keyboardDelegate?: KeyboardDelegate
   layoutDelegate?: LayoutDelegate
   linkBehavior?: 'action' | 'selection' | 'override' = 'action'
   onAction?: (Key) => void
   onSelectionChange?: (Selection) => void
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   shouldFocusWrap?: boolean = false
   shouldSelectOnPressUp?: boolean
 }

@react-aria/selection

/@react-aria/selection:AriaSelectableCollectionOptions

 AriaSelectableCollectionOptions {
   allowsTabNavigation?: boolean
   autoFocus?: boolean | FocusStrategy = false
   disallowEmptySelection?: boolean = false
   disallowSelectAll?: boolean = false
   disallowTypeAhead?: boolean = false
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
+  focusOnEntry?: 'first' | 'last'
   isVirtualized?: boolean
   keyboardDelegate: KeyboardDelegate
   linkBehavior?: 'action' | 'selection' | 'override' = 'action'
   ref: RefObject<HTMLElement | null>
   selectOnFocus?: boolean = false
   selectionManager: MultipleSelectionManager
   shouldFocusWrap?: boolean = false
   shouldUseVirtualFocus?: boolean
 }

/@react-aria/selection:AriaSelectableListOptions

 AriaSelectableListOptions {
   allowsTabNavigation?: boolean
   autoFocus?: boolean | FocusStrategy = false
   collection: Collection<Node<unknown>>
   disabledKeys: Set<Key>
   disallowEmptySelection?: boolean = false
   disallowSelectAll?: boolean = false
   disallowTypeAhead?: boolean = false
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
+  focusOnEntry?: 'first' | 'last'
   isVirtualized?: boolean
   keyboardDelegate?: KeyboardDelegate
   layoutDelegate?: LayoutDelegate
   linkBehavior?: 'action' | 'selection' | 'override' = 'action'
   ref: RefObject<HTMLElement | null>
   scrollRef?: RefObject<HTMLElement | null>
   selectOnFocus?: boolean = false
   selectionManager: MultipleSelectionManager
   shouldFocusWrap?: boolean = false
   shouldUseVirtualFocus?: boolean
 }

@react-aria/tree

/@react-aria/tree:AriaTreeOptions

 AriaTreeOptions <T> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   defaultSelectedKeys?: 'all' | Iterable<Key>
   disabledBehavior?: DisabledBehavior = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   disallowTypeAhead?: boolean = false
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
+  focusOnEntry?: 'first' | 'last'
   id?: string
   isVirtualized?: boolean
   items?: Iterable<T>
   keyboardDelegate?: KeyboardDelegate
   layoutDelegate?: LayoutDelegate
   linkBehavior?: 'action' | 'selection' | 'override' = 'action'
   onAction?: (Key) => void
   onSelectionChange?: (Selection) => void
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   shouldSelectOnPressUp?: boolean
 }

@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 = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   disallowTypeAhead?: boolean = false
   dragAndDropHooks?: DragAndDropHooks<NoInfer<T>>
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
+  focusOnEntry?: 'first' | 'last'
   id?: string
   items?: Iterable<T>
   layout?: 'grid' | 'waterfall' = 'grid'
   loadingState?: LoadingState
   onLoadMore?: () => void
   onSelectionChange?: (Selection) => void
   orientation?: Orientation = 'vertical'
   renderActionBar?: ('all' | Set<Key>) => ReactElement
   renderEmptyState?: (GridListRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   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:ListView

 ListView <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>
   dependencies?: ReadonlyArray<any>
   disabledBehavior?: DisabledBehavior = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   disallowTypeAhead?: boolean = false
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
+  focusOnEntry?: 'first' | 'last'
   hideLinkOutIcon?: boolean
   id?: string
   isQuiet?: boolean
   items?: Iterable<T>
   onAction?: (Key) => void
   onLoadMore?: () => void
   onSelectionChange?: (Selection) => void
   overflowMode?: 'wrap' | 'truncate' = 'truncate'
   renderActionBar?: ('all' | Set<Key>) => ReactElement
   renderEmptyState?: (GridListRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   selectionStyle?: 'highlight' | 'checkbox' = 'checkbox'
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   styles?: StylesPropWithHeight
 }

/@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 = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   disallowTypeAhead?: boolean = false
   dragAndDropHooks?: DragAndDropHooks<NoInfer<T>>
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
+  focusOnEntry?: 'first' | 'last'
   id?: string
   items?: Iterable<T>
   layout?: 'grid' | 'waterfall' = 'grid'
   loadingState?: LoadingState
   onLoadMore?: () => void
   onSelectionChange?: (Selection) => void
   orientation?: Orientation = 'vertical'
   renderActionBar?: ('all' | Set<Key>) => ReactElement
   renderEmptyState?: (GridListRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   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:ListViewProps

 ListViewProps <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>
   dependencies?: ReadonlyArray<any>
   disabledBehavior?: DisabledBehavior = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   disallowTypeAhead?: boolean = false
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
+  focusOnEntry?: 'first' | 'last'
   hideLinkOutIcon?: boolean
   id?: string
   isQuiet?: boolean
   items?: Iterable<T>
   onAction?: (Key) => void
   onLoadMore?: () => void
   onSelectionChange?: (Selection) => void
   overflowMode?: 'wrap' | 'truncate' = 'truncate'
   renderActionBar?: ('all' | Set<Key>) => ReactElement
   renderEmptyState?: (GridListRenderProps) => ReactNode
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   selectionStyle?: 'highlight' | 'checkbox' = 'checkbox'
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   styles?: StylesPropWithHeight
 }

overflow: 'hidden',
flexGrow: 1
})}>
{/* TODO: move this Virtualizer into the Thread component eventually when we get column reverse support */}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Have you thought about whether you will want virtualizer rects to be positioned in negative space, or should everything be converted to positive, similar to scroll offset?

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.

honestly haven't thought about it yet haha, would need to play around with the implementation to figure out the edge cases/pros/cons. I think you mentioned you were looking into this in another comment, have you been leaning one way or the other?

Copy link
Copy Markdown
Contributor

@nwidynski nwidynski May 13, 2026

Choose a reason for hiding this comment

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

Probably leaning towards doing this "just in time" by swapping "top" for "bottom" in layoutInfoToStyle(), aka normalizing to positive.

I'm working on low-level utils for this kind of stuff, yeah. Will hopefully land today since there is a bunch of complexity with different type of elements (root, iframe, etc.).

I'm primarily focused on scrollIntoView and usePreventScroll, so there won't be any overlap with your work. Hopefully yours will just get simpler with the additions 🙏

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