feat: Support textfields and other interactive components in Gridlists#10195
feat: Support textfields and other interactive components in Gridlists#10195LFDanLu wants to merge 13 commits into
Conversation
…ection when in input field
the failing tests didnt focus the element when triggering a keypress
…nts other than tab
|
Build successful! 🎉 |
|
Build successful! 🎉 |
| <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> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); | |||
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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>( | |||
| } | |||
There was a problem hiding this comment.
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.
| <TextField aria-label="Search"> | ||
| <Input /> | ||
| </TextField>{' '} | ||
| <Button>Go</Button> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 /> |
There was a problem hiding this comment.
Caret position cant be moved with arrow keys.
There was a problem hiding this comment.
thanks for catching this, also seems to have been due to the keyboard shortcut handlers work that went in recently
…ou tab out of gridlist
This reverts commit 9e1b070.
…list_with_textfields
|
@nwidynski just wanted to say thanks for looking and testing the changes here ❤️ |
|
Build successful! 🎉 |
## 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>
} |
Agent Skills ChangesModified (7)
InstallReact Spectrum S2: React Aria: |
| 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 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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>) => { |
There was a problem hiding this comment.
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?
Closes #4674, partially handles #2328
✅ Pull Request Checklist:
📝 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