Skip to content

Commit 41fc17a

Browse files
authored
feat: Support textfields and other interactive components in Gridlists (adobe#10195)
* initial changes to support textfields in gridlists * use robs event leak type select and prevent space from triggering selection when in input field * fix tests, but also use a more robust check instead of active element the failing tests didnt focus the element when triggering a keypress * add S2 Card story and tests for tree and gridlist * make presses on components in rows not trigger selection * add docs for rac * update docs and dedupe tree logic in useGridListItem keyboard handlers * pull other updates from other branch and change to stop all other events other than tab * clean up outdated test comment * fix so that Tab from combobox and toolbar propagate upwards so that you tab out of gridlist * Revert "feat: Keyboard shortcut handler (adobe#9929)" This reverts commit 9e1b070. * propagate tab in combobox so it exits the gridlist * typo
1 parent 1a37e3f commit 41fc17a

18 files changed

Lines changed: 1043 additions & 86 deletions

File tree

packages/@react-spectrum/s2/src/TreeView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export interface TreeViewProps<T>
104104
| 'selectionBehavior'
105105
| 'onScroll'
106106
| 'onCellAction'
107+
| 'keyboardNavigationBehavior'
107108
| keyof GlobalDOMAttributes
108109
>,
109110
UnsafeStyles,

packages/@react-spectrum/s2/stories/CardView.stories.tsx

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {MenuItem} from '../src/Menu';
2727
import type {Meta, StoryObj} from '@storybook/react';
2828
import {SkeletonCollection} from '../src/SkeletonCollection';
2929
import {style} from '../style/spectrum-theme' with {type: 'macro'};
30+
import {TextField} from '../src/TextField';
3031
import {useAsyncList} from 'react-stately/useAsyncList';
3132

3233
const meta: Meta<typeof CardView> = {
@@ -72,7 +73,15 @@ const avatarSize = {
7273
XL: 32
7374
} as const;
7475

75-
export function PhotoCard({item, layout}: {item: Item; layout: string}) {
76+
export function PhotoCard({
77+
item,
78+
layout,
79+
interactive
80+
}: {
81+
item: Item;
82+
layout: string;
83+
interactive?: React.ReactNode;
84+
}) {
7685
return (
7786
<Card id={item.id} textValue={item.description || item.alt_description}>
7887
{({size}) => (
@@ -112,12 +121,20 @@ export function PhotoCard({item, layout}: {item: Item; layout: string}) {
112121
<div
113122
className={style({
114123
display: 'flex',
115-
alignItems: 'center',
124+
flexDirection: 'column',
116125
gap: 8,
117126
gridArea: 'description'
118127
})}>
119-
<Avatar src={item.user.profile_image.small} size={avatarSize[size]} />
120-
<Text slot="description">{item.user.name}</Text>
128+
<div
129+
className={style({
130+
display: 'flex',
131+
alignItems: 'center',
132+
gap: 8
133+
})}>
134+
<Avatar src={item.user.profile_image.small} size={avatarSize[size]} />
135+
<Text slot="description">{item.user.name}</Text>
136+
</div>
137+
{interactive}
121138
</div>
122139
</Content>
123140
</>
@@ -126,7 +143,7 @@ export function PhotoCard({item, layout}: {item: Item; layout: string}) {
126143
);
127144
}
128145

129-
export const ExampleRender = (args: CardViewProps<any>) => {
146+
export const ExampleRender = (args: CardViewProps<any> & {interactive?: React.ReactNode}) => {
130147
let list = useAsyncList<Item, number | null>({
131148
async load({signal, cursor, items}) {
132149
let page = cursor || 1;
@@ -155,7 +172,9 @@ export const ExampleRender = (args: CardViewProps<any>) => {
155172
onLoadMore={args.loadingState === 'idle' ? list.loadMore : undefined}
156173
styles={cardViewStyles}>
157174
<Collection items={items} dependencies={[args.layout]}>
158-
{item => <PhotoCard item={item} layout={args.layout || 'grid'} />}
175+
{item => (
176+
<PhotoCard interactive={args.interactive} item={item} layout={args.layout || 'grid'} />
177+
)}
159178
</Collection>
160179
{(loadingState === 'loading' || loadingState === 'loadingMore') && (
161180
<SkeletonCollection>
@@ -288,3 +307,14 @@ export const CollectionCards: Story = {
288307
onAction: undefined
289308
}
290309
};
310+
311+
export const CardViewWithTextField: Story = {
312+
render: args => (
313+
<ExampleRender {...args} interactive={<TextField aria-label="search photos" />} />
314+
),
315+
args: {
316+
loadingState: 'idle',
317+
onAction: undefined,
318+
selectionMode: 'multiple'
319+
}
320+
};

packages/dev/s2-docs/pages/react-aria/GridList.mdx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -672,7 +672,7 @@ function Example(props) {
672672

673673
Use the `layout` and `orientation` props to arrange items in horizontal and vertical stacks and grids. This affects keyboard navigation and drag and drop behavior.
674674

675-
```tsx render docs={docs.exports.GridList} links={docs.links} props={['layout', 'orientation', 'keyboardNavigationBehavior']} initialProps={{layout: 'grid', orientation: 'horizontal', keyboardNavigationBehavior: 'tab'}} wide
675+
```tsx render docs={docs.exports.GridList} links={docs.links} props={['layout', 'orientation']} initialProps={{layout: 'grid', orientation: 'horizontal'}} wide
676676
"use client";
677677
import {GridList, GridListItem, Text} from 'vanilla-starter/GridList';
678678

@@ -704,6 +704,56 @@ let photos = [
704704
</GridList>
705705
```
706706

707+
## Keyboard navigation
708+
709+
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.
710+
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.
711+
712+
```tsx render
713+
"use client";
714+
import {GridList, GridListItem, Text} from 'vanilla-starter/GridList';
715+
import {ComboBox, ComboBoxItem} from 'vanilla-starter/ComboBox';
716+
717+
///- begin collapse -///
718+
///- begin collapse -///
719+
let photos = [
720+
{id: 1, title: 'Desert Sunset', description: 'PNG • 2/3/2024', src: 'https://images.unsplash.com/photo-1705034598432-1694e203cdf3?q=80&w=600&auto=format&fit=crop'},
721+
{id: 2, title: 'Hiking Trail', description: 'JPEG • 1/10/2022', src: 'https://images.unsplash.com/photo-1722233987129-61dc344db8b6?q=80&w=600&auto=format&fit=crop'},
722+
{id: 3, title: 'Lion', description: 'JPEG • 8/28/2021', src: 'https://images.unsplash.com/photo-1629812456605-4a044aa38fbc?q=80&w=600&auto=format&fit=crop'},
723+
{id: 4, title: 'Mountain Sunrise', description: 'PNG • 3/15/2015', src: 'https://images.unsplash.com/photo-1722172118908-1a97c312ce8c?q=80&w=600&auto=format&fit=crop'},
724+
{id: 5, title: 'Giraffe tongue', description: 'PNG • 11/27/2019', src: 'https://images.unsplash.com/photo-1574870111867-089730e5a72b?q=80&w=600&auto=format&fit=crop'},
725+
{id: 6, title: 'Golden Hour', description: 'WEBP • 7/24/2024', src: 'https://images.unsplash.com/photo-1718378037953-ab21bf2cf771?q=80&w=600&auto=format&fit=crop'},
726+
];
727+
728+
function PermissionPicker({label}) {
729+
return (
730+
<ComboBox style={{paddingInlineStart: 12, width: '80%'}} aria-label={label} defaultSelectedKey="view" placeholder="Permission">
731+
<ComboBoxItem id="view">Can view</ComboBoxItem>
732+
<ComboBoxItem id="comment">Can comment</ComboBoxItem>
733+
<ComboBoxItem id="edit">Can edit</ComboBoxItem>
734+
</ComboBox>
735+
);
736+
}
737+
///- end collapse -///
738+
739+
<GridList
740+
/*- begin highlight -*/
741+
keyboardNavigationBehavior="tab"
742+
/*- end highlight -*/
743+
items={photos}
744+
selectionMode="multiple"
745+
aria-label="Shared files">
746+
{item => (
747+
<GridListItem textValue={item.title}>
748+
<img src={item.src} alt="" />
749+
<Text>{item.title}</Text>
750+
<Text slot="description">{item.description}</Text>
751+
<PermissionPicker label={`${item.title} permission`} />
752+
</GridListItem>
753+
)}
754+
</GridList>
755+
```
756+
707757
## Drag and drop
708758

709759
GridList supports drag and drop interactions when the `dragAndDropHooks` prop is provided using the <TypeLink links={docs.links} type={docs.exports.useDragAndDrop} /> hook. Users can drop data on the list as a whole, on individual items, insert new items between existing ones, or reorder items. React Aria supports drag and drop via mouse, touch, keyboard, and screen reader interactions. See the [drag and drop guide](dnd?component=GridList) to learn more.

packages/dev/s2-docs/pages/react-aria/Tree.mdx

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ import {Tree, TreeHeader, TreeItem, TreeSection} from 'vanilla-starter/Tree';
257257
<TreeItem id="shared-photos" title="Shared Photos">
258258
<TreeItem id="shared-photo-1" title="Shared Photo 1" />
259259
<TreeItem id="shared-photo-2" title="Shared Photo 2" />
260-
</TreeItem>
260+
</TreeItem>
261261
</TreeSection>
262262
<TreeSection>
263263
<TreeHeader>Documents</TreeHeader>
@@ -322,6 +322,61 @@ function Example(props) {
322322
}
323323
```
324324

325+
## Keyboard navigation
326+
327+
By default, Tree 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.
328+
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.
329+
330+
```tsx render
331+
"use client";
332+
import {Tree, TreeItem, TreeItemContent} from 'vanilla-starter/Tree';
333+
import {ComboBox, ComboBoxItem} from 'vanilla-starter/ComboBox';
334+
335+
///- begin collapse -///
336+
function PermissionPicker({label}) {
337+
return (
338+
<ComboBox style={{marginInlineStart: 'auto', flexShrink: 0}} aria-label={label} defaultSelectedKey="view" placeholder="Permission">
339+
<ComboBoxItem id="view">Can view</ComboBoxItem>
340+
<ComboBoxItem id="comment">Can comment</ComboBoxItem>
341+
<ComboBoxItem id="edit">Can edit</ComboBoxItem>
342+
</ComboBox>
343+
);
344+
}
345+
///- end collapse -///
346+
347+
<Tree
348+
/*- begin highlight -*/
349+
keyboardNavigationBehavior="tab"
350+
/*- end highlight -*/
351+
selectionMode="multiple"
352+
defaultExpandedKeys={['documents', 'photos']}
353+
aria-label="Shared files"
354+
style={{width: 420}}>
355+
<TreeItem id="documents" title="Documents">
356+
<TreeItem id="weekly" textValue="Weekly Report.pdf">
357+
<TreeItemContent>
358+
Weekly Report.pdf
359+
<PermissionPicker label="Weekly Report.pdf permission" />
360+
</TreeItemContent>
361+
</TreeItem>
362+
<TreeItem id="budget" textValue="Budget.xlsx">
363+
<TreeItemContent>
364+
Budget.xlsx
365+
<PermissionPicker label="Budget.xlsx permission" />
366+
</TreeItemContent>
367+
</TreeItem>
368+
</TreeItem>
369+
<TreeItem id="photos" title="Photos">
370+
<TreeItem id="sunset" textValue="Sunset.jpg">
371+
<TreeItemContent>
372+
Sunset.jpg
373+
<PermissionPicker label="Sunset.jpg permission" />
374+
</TreeItemContent>
375+
</TreeItem>
376+
</TreeItem>
377+
</Tree>
378+
```
379+
325380
## Drag and drop
326381

327382
Tree supports drag and drop interactions when the `dragAndDropHooks` prop is provided using the <TypeLink links={docs.links} type={docs.exports.useDragAndDrop} /> hook. Users can drop data on the list as a whole, on individual items, insert new items between existing ones, or reorder items. React Aria supports drag and drop via mouse, touch, keyboard, and screen reader interactions. See the [drag and drop guide](dnd?component=Tree) to learn more.

0 commit comments

Comments
 (0)