Skip to content

feat: Support textfields and other interactive components in Gridlists#10195

Open
LFDanLu wants to merge 13 commits into
mainfrom
only_gridlist_with_textfields
Open

feat: Support textfields and other interactive components in Gridlists#10195
LFDanLu wants to merge 13 commits into
mainfrom
only_gridlist_with_textfields

Conversation

@LFDanLu

@LFDanLu LFDanLu commented Jun 12, 2026

Copy link
Copy Markdown
Member

Closes #4674, partially handles #2328

✅ 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:

Test GridList/Tree with textfield stories and test keyboardNavigationBehavior=tab behavior. You should be able to interact/type/etc with the interactive elements without causing focus navigation/row selection even if you use the arrow keys/Enter/Space/etc. It should require Tab from the cell/row to the cell/row's content to interact with the children, and Shift-Tab back to the cell/row to perform row navigation/selection via arrow keys/Enter/Space as usual.

Test the same in S2 CardView/TagGroup.

🧢 Your Project:

RSP

@rspbot

rspbot commented Jun 12, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jun 12, 2026

Copy link
Copy Markdown

Comment on lines +1013 to +1017
<Toolbar aria-label="Text formatting" style={{gap: 4}}>
<Button onPress={action('Bold press')}>Bold</Button>
<Button onPress={action('Italics press')}>Italic</Button>
<Button onPress={action('Underline press')}>Underline</Button>
</Toolbar>

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.

Tabbing while focus is on button yields focus on the menu trigger in the adjacent cell. In this case the sibling also appears to fail at recognizing focus within?

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.

this actually turned out to be due to a stopPropagation we added to toolbar keyboard handling as a result of some other work, should work now

@@ -181,36 +187,10 @@ export function useGridListItem<T>(
let walker = getFocusableTreeWalker(ref.current);

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.

Unrelated to this PR, but surfaced especially with interactive content. I believe this is a bug. Arrow key movement should also respect tabbable attributes imo.

Same goes for useGridCell, see #8691 (comment)

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.

thanks for bringing it up, will make a note but just a fyi that it probably won't make it in for the upcoming release since we are keeping it light

@@ -310,7 +290,7 @@ export function useGridListItem<T>(
}

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.

I saw Rob's comment in the keyboard PR about backwards tabbing, so I know you are aware of the issues with it.

Just wanted to raise the fix in https://github.com/adobe/react-spectrum/pull/7277/files#diff-ac8d9d018f61f2d88a6e491d488d043d4000e05ee9ff93df7a65032c2b9dbfedR261-R280 in case you want to include it in this cycle. Just replace the sibling check with a comparison of the data-collection attribute.

Comment on lines +1006 to +1009
<TextField aria-label="Search">
<Input />
</TextField>{' '}
<Button>Go</Button>

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.

Entering a value into the textfield, then shift tabbing to the row, then back into the field causes a text selection up to the prior caret position. Seems like shift is sticky somewhere?

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.

I'm not sure I'm seeing this behavior, it seems to select all the text always for me regardless of caret position. This seems to match browser behavior as well: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input

ComboBox
<ComboBox aria-label="combobox" allowsEmptyCollection>
<div style={{display: 'flex'}}>
<Input />

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.

Caret position cant be moved with arrow keys.

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.

thanks for catching this, also seems to have been due to the keyboard shortcut handlers work that went in recently

@LFDanLu

LFDanLu commented Jun 12, 2026

Copy link
Copy Markdown
Member Author

@nwidynski just wanted to say thanks for looking and testing the changes here ❤️

@rspbot

rspbot commented Jun 12, 2026

Copy link
Copy Markdown

@rspbot

rspbot commented Jun 12, 2026

Copy link
Copy Markdown
## API Changes

react-aria-components

/react-aria-components:Tree

 Tree <T> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children?: ReactNode | (T) => ReactNode
   className?: ClassNameOrFunction<TreeRenderProps> = 'react-aria-Tree'
   defaultExpandedKeys?: Iterable<Key>
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: ReadonlyArray<any>
   disabledBehavior?: DisabledBehavior = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   dragAndDropHooks?: DragAndDropHooks<NoInfer<T>>
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   expandedKeys?: Iterable<Key>
   id?: string
   items?: Iterable<T>
+  keyboardNavigationBehavior?: 'arrow' | 'tab' = 'arrow'
   onAction?: (Key) => void
   onExpandedChange?: (Set<Key>) => any
   onSelectionChange?: (Selection) => void
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, TreeRenderProps>
   selectedKeys?: 'all' | Iterable<Key>
   selectionBehavior?: SelectionBehavior = 'toggle'
   selectionMode?: SelectionMode
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   style?: StyleOrFunction<TreeRenderProps>
 }

/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?: ClassNameOrFunction<TreeRenderProps> = 'react-aria-Tree'
   defaultExpandedKeys?: Iterable<Key>
   defaultSelectedKeys?: 'all' | Iterable<Key>
   dependencies?: ReadonlyArray<any>
   disabledBehavior?: DisabledBehavior = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   dragAndDropHooks?: DragAndDropHooks<NoInfer<T>>
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   expandedKeys?: Iterable<Key>
   id?: string
   items?: Iterable<T>
+  keyboardNavigationBehavior?: 'arrow' | 'tab' = 'arrow'
   onAction?: (Key) => void
   onExpandedChange?: (Set<Key>) => any
   onSelectionChange?: (Selection) => void
   render?: DOMRenderFunction<keyof React.JSX.IntrinsicElements, TreeRenderProps>
   selectedKeys?: 'all' | Iterable<Key>
   selectionBehavior?: SelectionBehavior = 'toggle'
   selectionMode?: SelectionMode
   shouldSelectOnPressUp?: boolean
   slot?: string | null
   style?: StyleOrFunction<TreeRenderProps>
 }

@react-aria/tree

/@react-aria/tree:AriaTreeProps

 AriaTreeProps <T> {
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   children: CollectionChildren<T>
   defaultSelectedKeys?: 'all' | Iterable<Key>
   disabledBehavior?: DisabledBehavior = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   id?: string
   items?: Iterable<T>
+  keyboardNavigationBehavior?: 'arrow' | 'tab' = 'arrow'
   onAction?: (Key) => void
   onSelectionChange?: (Selection) => void
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
 }

@react-spectrum/tree

/@react-spectrum/tree:TreeView

 TreeView <T extends {}> {
   UNSAFE_className?: string
   UNSAFE_style?: CSSProperties
   alignSelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'center' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'stretch'>
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   bottom?: Responsive<DimensionValue>
   children?: ReactNode | ({}) => ReactNode
   defaultExpandedKeys?: Iterable<Key>
   defaultSelectedKeys?: 'all' | Iterable<Key>
   disabledBehavior?: DisabledBehavior = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   end?: Responsive<DimensionValue>
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   expandedKeys?: Iterable<Key>
   flex?: Responsive<string | number | boolean>
   flexBasis?: Responsive<number | string>
   flexGrow?: Responsive<number>
   flexShrink?: Responsive<number>
   gridArea?: Responsive<string>
   gridColumn?: Responsive<string>
   gridColumnEnd?: Responsive<string>
   gridColumnStart?: Responsive<string>
   gridRow?: Responsive<string>
   gridRowEnd?: Responsive<string>
   gridRowStart?: Responsive<string>
   height?: Responsive<DimensionValue>
   id?: string
   isHidden?: Responsive<boolean>
   items?: Iterable<T>
   justifySelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'center' | 'left' | 'right' | 'stretch'>
+  keyboardNavigationBehavior?: 'arrow' | 'tab' = 'arrow'
   left?: Responsive<DimensionValue>
   margin?: Responsive<DimensionValue>
   marginBottom?: Responsive<DimensionValue>
   marginEnd?: Responsive<DimensionValue>
   marginTop?: Responsive<DimensionValue>
   marginX?: Responsive<DimensionValue>
   marginY?: Responsive<DimensionValue>
   maxHeight?: Responsive<DimensionValue>
   maxWidth?: Responsive<DimensionValue>
   minHeight?: Responsive<DimensionValue>
   minWidth?: Responsive<DimensionValue>
   onAction?: (Key) => void
   onExpandedChange?: (Set<Key>) => any
   onSelectionChange?: (Selection) => void
   order?: Responsive<number>
   position?: Responsive<'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'>
   renderEmptyState?: () => JSX.Element
   right?: Responsive<DimensionValue>
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   selectionStyle?: 'checkbox' | 'highlight'
   shouldSelectOnPressUp?: boolean
   start?: Responsive<DimensionValue>
   top?: Responsive<DimensionValue>
   width?: Responsive<DimensionValue>
   zIndex?: Responsive<number>
 }

/@react-spectrum/tree:SpectrumTreeViewProps

 SpectrumTreeViewProps <T> {
   UNSAFE_className?: string
   UNSAFE_style?: CSSProperties
   alignSelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'center' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'stretch'>
   aria-describedby?: string
   aria-details?: string
   aria-label?: string
   aria-labelledby?: string
   autoFocus?: boolean | FocusStrategy
   bottom?: Responsive<DimensionValue>
   children?: ReactNode | (T) => ReactNode
   defaultExpandedKeys?: Iterable<Key>
   defaultSelectedKeys?: 'all' | Iterable<Key>
   disabledBehavior?: DisabledBehavior = 'all'
   disabledKeys?: Iterable<Key>
   disallowEmptySelection?: boolean
   end?: Responsive<DimensionValue>
   escapeKeyBehavior?: 'clearSelection' | 'none' = 'clearSelection'
   expandedKeys?: Iterable<Key>
   flex?: Responsive<string | number | boolean>
   flexBasis?: Responsive<number | string>
   flexGrow?: Responsive<number>
   flexShrink?: Responsive<number>
   gridArea?: Responsive<string>
   gridColumn?: Responsive<string>
   gridColumnEnd?: Responsive<string>
   gridColumnStart?: Responsive<string>
   gridRow?: Responsive<string>
   gridRowEnd?: Responsive<string>
   gridRowStart?: Responsive<string>
   height?: Responsive<DimensionValue>
   id?: string
   isHidden?: Responsive<boolean>
   items?: Iterable<T>
   justifySelf?: Responsive<'auto' | 'normal' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'self-start' | 'self-end' | 'center' | 'left' | 'right' | 'stretch'>
+  keyboardNavigationBehavior?: 'arrow' | 'tab' = 'arrow'
   left?: Responsive<DimensionValue>
   margin?: Responsive<DimensionValue>
   marginBottom?: Responsive<DimensionValue>
   marginEnd?: Responsive<DimensionValue>
   marginTop?: Responsive<DimensionValue>
   marginX?: Responsive<DimensionValue>
   marginY?: Responsive<DimensionValue>
   maxHeight?: Responsive<DimensionValue>
   maxWidth?: Responsive<DimensionValue>
   minHeight?: Responsive<DimensionValue>
   minWidth?: Responsive<DimensionValue>
   onAction?: (Key) => void
   onExpandedChange?: (Set<Key>) => any
   onSelectionChange?: (Selection) => void
   order?: Responsive<number>
   position?: Responsive<'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'>
   renderEmptyState?: () => JSX.Element
   right?: Responsive<DimensionValue>
   selectedKeys?: 'all' | Iterable<Key>
   selectionMode?: SelectionMode
   selectionStyle?: 'checkbox' | 'highlight'
   shouldSelectOnPressUp?: boolean
   start?: Responsive<DimensionValue>
   top?: Responsive<DimensionValue>
   width?: Responsive<DimensionValue>
   zIndex?: Responsive<number>
 }

@rspbot

rspbot commented Jun 12, 2026

Copy link
Copy Markdown

Agent Skills Changes

Modified (7)
Install

React Spectrum S2:

npx skills add https://d1pzu54gtk2aed.cloudfront.net/pr/8a4e0b4de38fc3be5613016346d35cf4ee17099a/

React Aria:

npx skills add https://d5iwopk28bdhl.cloudfront.net/pr/8a4e0b4de38fc3be5613016346d35cf4ee17099a/

@devongovett devongovett left a comment

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.

approving for testing

By default, GridList uses arrow key navigation to move focus into rows. Set `keyboardNavigationBehavior="tab"` to have <Keyboard>Tab</Keyboard> move focus in and out of a row.
Use this when rows contain interactive elements such as text fields, where arrow keys and typing in the field should not trigger grid navigation or selection.

```tsx render

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.

Perhaps not the most realistic example... maybe we can think about it

## Keyboard navigation

By default, GridList uses arrow key navigation to move focus into rows. Set `keyboardNavigationBehavior="tab"` to have <Keyboard>Tab</Keyboard> move focus in and out of a row.
Use this when rows contain interactive elements such as text fields, where arrow keys and typing in the field should not trigger grid navigation or selection.

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.

Perhaps a little confusing because the starter example uses keyboardNavigationBehavior="tab" by default already since it is a grid layout...

Use keyboardNavigationBehavior="tab" to allow users to tab to focusable elements within an item. When focus is on an item itself, users can navigate between items via the arrow keys. When focus is within an item, the arrow keys control the focused element (e.g. moving the cursor in a text field).

Not sure how much we want to explain in the docs though.


## Keyboard navigation

By default, Tree uses arrow key navigation to move focus into rows. Set `keyboardNavigationBehavior="tab"` to have <Keyboard>Option</Keyboard> move focus in and out of a row.

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.

I guess this should be Tab rather than Option? But make this description match whatever we end up with for GridList.

};

let baseOnMouseDown = rowProps.onMouseDown;
rowProps.onMouseDown = (e: ReactMouseEvent<FocusableElement>) => {

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.

We can discuss whether these overrides should just go in useSelectableItem at some point. Seems like these would always apply, not just in grid list?

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.

Input field inside GridList

4 participants