12
12
13
13
import { DOMAttributes , FocusableElement , Node as RSNode } from '@react-types/shared' ;
14
14
import { focusSafely , getFocusableTreeWalker } from '@react-aria/focus' ;
15
+ import { getLastItem } from '@react-stately/collections' ;
15
16
import { getRowId , listMap } from './utils' ;
16
17
import { getScrollParent , getSyntheticLinkProps , mergeProps , scrollIntoViewport , useSlotId } from '@react-aria/utils' ;
18
+ import { HTMLAttributes , KeyboardEvent as ReactKeyboardEvent , RefObject , useRef } from 'react' ;
17
19
import { isFocusVisible } from '@react-aria/interactions' ;
18
20
import type { ListState } from '@react-stately/list' ;
19
- import { KeyboardEvent as ReactKeyboardEvent , RefObject , useRef } from 'react' ;
20
21
import { SelectableItemStates , useSelectableItem } from '@react-aria/selection' ;
22
+ import type { TreeState } from '@react-stately/tree' ;
21
23
import { useLocale } from '@react-aria/i18n' ;
22
24
23
25
export interface AriaGridListItemOptions {
@@ -38,13 +40,24 @@ export interface GridListItemAria extends SelectableItemStates {
38
40
descriptionProps : DOMAttributes
39
41
}
40
42
43
+ const EXPANSION_KEYS = {
44
+ 'expand' : {
45
+ ltr : 'ArrowRight' ,
46
+ rtl : 'ArrowLeft'
47
+ } ,
48
+ 'collapse' : {
49
+ ltr : 'ArrowLeft' ,
50
+ rtl : 'ArrowRight'
51
+ }
52
+ } ;
53
+
41
54
/**
42
55
* Provides the behavior and accessibility implementation for a row in a grid list.
43
56
* @param props - Props for the row.
44
57
* @param state - State of the parent list, as returned by `useListState`.
45
58
* @param ref - The ref attached to the row element.
46
59
*/
47
- export function useGridListItem < T > ( props : AriaGridListItemOptions , state : ListState < T > , ref : RefObject < FocusableElement > ) : GridListItemAria {
60
+ export function useGridListItem < T > ( props : AriaGridListItemOptions , state : ListState < T > | TreeState < T > , ref : RefObject < FocusableElement > ) : GridListItemAria {
48
61
// Copied from useGridCell + some modifications to make it not so grid specific
49
62
let {
50
63
node,
@@ -64,12 +77,34 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
64
77
// (e.g. clicking on a row button)
65
78
if (
66
79
( keyWhenFocused . current != null && node . key !== keyWhenFocused . current ) ||
67
- ! ref . current . contains ( document . activeElement )
80
+ ! ref . current ? .contains ( document . activeElement )
68
81
) {
69
82
focusSafely ( ref . current ) ;
70
83
}
71
84
} ;
72
85
86
+ let treeGridRowProps : HTMLAttributes < HTMLElement > = { } ;
87
+ let hasChildRows ;
88
+ let hasLink = state . selectionManager . isLink ( node . key ) ;
89
+ if ( node != null && 'expandedKeys' in state ) {
90
+ // TODO: ideally node.hasChildNodes would be a way to tell if a row has child nodes, but the row's contents make it so that value is always
91
+ // true...
92
+ hasChildRows = [ ...state . collection . getChildren ( node . key ) ] . length > 1 ;
93
+ if ( onAction == null && ! hasLink && state . selectionManager . selectionMode === 'none' && hasChildRows ) {
94
+ onAction = ( ) => state . toggleKey ( node . key ) ;
95
+ }
96
+
97
+ let isExpanded = hasChildRows ? state . expandedKeys === 'all' || state . expandedKeys . has ( node . key ) : undefined ;
98
+ treeGridRowProps = {
99
+ 'aria-expanded' : isExpanded ,
100
+ 'aria-level' : node . level + 1 ,
101
+ 'aria-posinset' : node ?. index + 1 ,
102
+ 'aria-setsize' : node . level > 0 ?
103
+ ( getLastItem ( state . collection . getChildren ( node ?. parentKey ) ) ) . index + 1 :
104
+ [ ...state . collection ] . filter ( row => row . level === 0 ) . at ( - 1 ) . index + 1
105
+ } ;
106
+ }
107
+
73
108
let { itemProps, ...itemStates } = useSelectableItem ( {
74
109
selectionManager : state . selectionManager ,
75
110
key : node . key ,
@@ -89,6 +124,18 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
89
124
let walker = getFocusableTreeWalker ( ref . current ) ;
90
125
walker . currentNode = document . activeElement ;
91
126
127
+ if ( 'expandedKeys' in state && document . activeElement === ref . current ) {
128
+ if ( ( e . key === EXPANSION_KEYS [ 'expand' ] [ direction ] ) && state . selectionManager . focusedKey === node . key && hasChildRows && state . expandedKeys !== 'all' && ! state . expandedKeys . has ( node . key ) ) {
129
+ state . toggleKey ( node . key ) ;
130
+ e . stopPropagation ( ) ;
131
+ return ;
132
+ } else if ( ( e . key === EXPANSION_KEYS [ 'collapse' ] [ direction ] ) && state . selectionManager . focusedKey === node . key && hasChildRows && ( state . expandedKeys === 'all' || state . expandedKeys . has ( node . key ) ) ) {
133
+ state . toggleKey ( node . key ) ;
134
+ e . stopPropagation ( ) ;
135
+ return ;
136
+ }
137
+ }
138
+
92
139
switch ( e . key ) {
93
140
case 'ArrowLeft' : {
94
141
// Find the next focusable element within the row.
@@ -199,8 +246,9 @@ export function useGridListItem<T>(props: AriaGridListItemOptions, state: ListSt
199
246
'aria-colindex' : 1
200
247
} ;
201
248
249
+ // TODO: should isExpanded and hasChildRows be a item state that gets returned by the hook?
202
250
return {
203
- rowProps,
251
+ rowProps : { ... mergeProps ( rowProps , treeGridRowProps ) } ,
204
252
gridCellProps,
205
253
descriptionProps : {
206
254
id : descriptionId
0 commit comments