@react-aria/grid 3.0.0-beta.1 → 3.0.0-nightly-4980928d3-240906

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/dist/GridKeyboardDelegate.main.js +247 -0
  2. package/dist/GridKeyboardDelegate.main.js.map +1 -0
  3. package/dist/GridKeyboardDelegate.mjs +242 -0
  4. package/dist/GridKeyboardDelegate.module.js +242 -0
  5. package/dist/GridKeyboardDelegate.module.js.map +1 -0
  6. package/dist/ar-AE.main.js +15 -0
  7. package/dist/ar-AE.main.js.map +1 -0
  8. package/dist/ar-AE.mjs +17 -0
  9. package/dist/ar-AE.module.js +17 -0
  10. package/dist/ar-AE.module.js.map +1 -0
  11. package/dist/bg-BG.main.js +15 -0
  12. package/dist/bg-BG.main.js.map +1 -0
  13. package/dist/bg-BG.mjs +17 -0
  14. package/dist/bg-BG.module.js +17 -0
  15. package/dist/bg-BG.module.js.map +1 -0
  16. package/dist/cs-CZ.main.js +15 -0
  17. package/dist/cs-CZ.main.js.map +1 -0
  18. package/dist/cs-CZ.mjs +17 -0
  19. package/dist/cs-CZ.module.js +17 -0
  20. package/dist/cs-CZ.module.js.map +1 -0
  21. package/dist/da-DK.main.js +15 -0
  22. package/dist/da-DK.main.js.map +1 -0
  23. package/dist/da-DK.mjs +17 -0
  24. package/dist/da-DK.module.js +17 -0
  25. package/dist/da-DK.module.js.map +1 -0
  26. package/dist/de-DE.main.js +15 -0
  27. package/dist/de-DE.main.js.map +1 -0
  28. package/dist/de-DE.mjs +17 -0
  29. package/dist/de-DE.module.js +17 -0
  30. package/dist/de-DE.module.js.map +1 -0
  31. package/dist/el-GR.main.js +15 -0
  32. package/dist/el-GR.main.js.map +1 -0
  33. package/dist/el-GR.mjs +17 -0
  34. package/dist/el-GR.module.js +17 -0
  35. package/dist/el-GR.module.js.map +1 -0
  36. package/dist/en-US.main.js +15 -0
  37. package/dist/en-US.main.js.map +1 -0
  38. package/dist/en-US.mjs +17 -0
  39. package/dist/en-US.module.js +17 -0
  40. package/dist/en-US.module.js.map +1 -0
  41. package/dist/es-ES.main.js +15 -0
  42. package/dist/es-ES.main.js.map +1 -0
  43. package/dist/es-ES.mjs +17 -0
  44. package/dist/es-ES.module.js +17 -0
  45. package/dist/es-ES.module.js.map +1 -0
  46. package/dist/et-EE.main.js +15 -0
  47. package/dist/et-EE.main.js.map +1 -0
  48. package/dist/et-EE.mjs +17 -0
  49. package/dist/et-EE.module.js +17 -0
  50. package/dist/et-EE.module.js.map +1 -0
  51. package/dist/fi-FI.main.js +15 -0
  52. package/dist/fi-FI.main.js.map +1 -0
  53. package/dist/fi-FI.mjs +17 -0
  54. package/dist/fi-FI.module.js +17 -0
  55. package/dist/fi-FI.module.js.map +1 -0
  56. package/dist/fr-FR.main.js +15 -0
  57. package/dist/fr-FR.main.js.map +1 -0
  58. package/dist/fr-FR.mjs +17 -0
  59. package/dist/fr-FR.module.js +17 -0
  60. package/dist/fr-FR.module.js.map +1 -0
  61. package/dist/he-IL.main.js +15 -0
  62. package/dist/he-IL.main.js.map +1 -0
  63. package/dist/he-IL.mjs +17 -0
  64. package/dist/he-IL.module.js +17 -0
  65. package/dist/he-IL.module.js.map +1 -0
  66. package/dist/hr-HR.main.js +15 -0
  67. package/dist/hr-HR.main.js.map +1 -0
  68. package/dist/hr-HR.mjs +17 -0
  69. package/dist/hr-HR.module.js +17 -0
  70. package/dist/hr-HR.module.js.map +1 -0
  71. package/dist/hu-HU.main.js +15 -0
  72. package/dist/hu-HU.main.js.map +1 -0
  73. package/dist/hu-HU.mjs +17 -0
  74. package/dist/hu-HU.module.js +17 -0
  75. package/dist/hu-HU.module.js.map +1 -0
  76. package/dist/import.mjs +31 -0
  77. package/dist/intlStrings.main.js +108 -0
  78. package/dist/intlStrings.main.js.map +1 -0
  79. package/dist/intlStrings.mjs +110 -0
  80. package/dist/intlStrings.module.js +110 -0
  81. package/dist/intlStrings.module.js.map +1 -0
  82. package/dist/it-IT.main.js +15 -0
  83. package/dist/it-IT.main.js.map +1 -0
  84. package/dist/it-IT.mjs +17 -0
  85. package/dist/it-IT.module.js +17 -0
  86. package/dist/it-IT.module.js.map +1 -0
  87. package/dist/ja-JP.main.js +15 -0
  88. package/dist/ja-JP.main.js.map +1 -0
  89. package/dist/ja-JP.mjs +17 -0
  90. package/dist/ja-JP.module.js +17 -0
  91. package/dist/ja-JP.module.js.map +1 -0
  92. package/dist/ko-KR.main.js +15 -0
  93. package/dist/ko-KR.main.js.map +1 -0
  94. package/dist/ko-KR.mjs +17 -0
  95. package/dist/ko-KR.module.js +17 -0
  96. package/dist/ko-KR.module.js.map +1 -0
  97. package/dist/lt-LT.main.js +15 -0
  98. package/dist/lt-LT.main.js.map +1 -0
  99. package/dist/lt-LT.mjs +17 -0
  100. package/dist/lt-LT.module.js +17 -0
  101. package/dist/lt-LT.module.js.map +1 -0
  102. package/dist/lv-LV.main.js +15 -0
  103. package/dist/lv-LV.main.js.map +1 -0
  104. package/dist/lv-LV.mjs +17 -0
  105. package/dist/lv-LV.module.js +17 -0
  106. package/dist/lv-LV.module.js.map +1 -0
  107. package/dist/main.js +23 -823
  108. package/dist/main.js.map +1 -1
  109. package/dist/module.js +10 -773
  110. package/dist/module.js.map +1 -1
  111. package/dist/nb-NO.main.js +15 -0
  112. package/dist/nb-NO.main.js.map +1 -0
  113. package/dist/nb-NO.mjs +17 -0
  114. package/dist/nb-NO.module.js +17 -0
  115. package/dist/nb-NO.module.js.map +1 -0
  116. package/dist/nl-NL.main.js +15 -0
  117. package/dist/nl-NL.main.js.map +1 -0
  118. package/dist/nl-NL.mjs +17 -0
  119. package/dist/nl-NL.module.js +17 -0
  120. package/dist/nl-NL.module.js.map +1 -0
  121. package/dist/pl-PL.main.js +15 -0
  122. package/dist/pl-PL.main.js.map +1 -0
  123. package/dist/pl-PL.mjs +17 -0
  124. package/dist/pl-PL.module.js +17 -0
  125. package/dist/pl-PL.module.js.map +1 -0
  126. package/dist/pt-BR.main.js +15 -0
  127. package/dist/pt-BR.main.js.map +1 -0
  128. package/dist/pt-BR.mjs +17 -0
  129. package/dist/pt-BR.module.js +17 -0
  130. package/dist/pt-BR.module.js.map +1 -0
  131. package/dist/pt-PT.main.js +15 -0
  132. package/dist/pt-PT.main.js.map +1 -0
  133. package/dist/pt-PT.mjs +17 -0
  134. package/dist/pt-PT.module.js +17 -0
  135. package/dist/pt-PT.module.js.map +1 -0
  136. package/dist/ro-RO.main.js +15 -0
  137. package/dist/ro-RO.main.js.map +1 -0
  138. package/dist/ro-RO.mjs +17 -0
  139. package/dist/ro-RO.module.js +17 -0
  140. package/dist/ro-RO.module.js.map +1 -0
  141. package/dist/ru-RU.main.js +15 -0
  142. package/dist/ru-RU.main.js.map +1 -0
  143. package/dist/ru-RU.mjs +17 -0
  144. package/dist/ru-RU.module.js +17 -0
  145. package/dist/ru-RU.module.js.map +1 -0
  146. package/dist/sk-SK.main.js +15 -0
  147. package/dist/sk-SK.main.js.map +1 -0
  148. package/dist/sk-SK.mjs +17 -0
  149. package/dist/sk-SK.module.js +17 -0
  150. package/dist/sk-SK.module.js.map +1 -0
  151. package/dist/sl-SI.main.js +15 -0
  152. package/dist/sl-SI.main.js.map +1 -0
  153. package/dist/sl-SI.mjs +17 -0
  154. package/dist/sl-SI.module.js +17 -0
  155. package/dist/sl-SI.module.js.map +1 -0
  156. package/dist/sr-SP.main.js +15 -0
  157. package/dist/sr-SP.main.js.map +1 -0
  158. package/dist/sr-SP.mjs +17 -0
  159. package/dist/sr-SP.module.js +17 -0
  160. package/dist/sr-SP.module.js.map +1 -0
  161. package/dist/sv-SE.main.js +15 -0
  162. package/dist/sv-SE.main.js.map +1 -0
  163. package/dist/sv-SE.mjs +17 -0
  164. package/dist/sv-SE.module.js +17 -0
  165. package/dist/sv-SE.module.js.map +1 -0
  166. package/dist/tr-TR.main.js +15 -0
  167. package/dist/tr-TR.main.js.map +1 -0
  168. package/dist/tr-TR.mjs +17 -0
  169. package/dist/tr-TR.module.js +17 -0
  170. package/dist/tr-TR.module.js.map +1 -0
  171. package/dist/types.d.ts +114 -36
  172. package/dist/types.d.ts.map +1 -1
  173. package/dist/uk-UA.main.js +15 -0
  174. package/dist/uk-UA.main.js.map +1 -0
  175. package/dist/uk-UA.mjs +17 -0
  176. package/dist/uk-UA.module.js +17 -0
  177. package/dist/uk-UA.module.js.map +1 -0
  178. package/dist/useGrid.main.js +132 -0
  179. package/dist/useGrid.main.js.map +1 -0
  180. package/dist/useGrid.mjs +127 -0
  181. package/dist/useGrid.module.js +127 -0
  182. package/dist/useGrid.module.js.map +1 -0
  183. package/dist/useGridCell.main.js +220 -0
  184. package/dist/useGridCell.main.js.map +1 -0
  185. package/dist/useGridCell.mjs +215 -0
  186. package/dist/useGridCell.module.js +215 -0
  187. package/dist/useGridCell.module.js.map +1 -0
  188. package/dist/useGridRow.main.js +53 -0
  189. package/dist/useGridRow.main.js.map +1 -0
  190. package/dist/useGridRow.mjs +48 -0
  191. package/dist/useGridRow.module.js +48 -0
  192. package/dist/useGridRow.module.js.map +1 -0
  193. package/dist/useGridRowGroup.main.js +26 -0
  194. package/dist/useGridRowGroup.main.js.map +1 -0
  195. package/dist/useGridRowGroup.mjs +21 -0
  196. package/dist/useGridRowGroup.module.js +21 -0
  197. package/dist/useGridRowGroup.module.js.map +1 -0
  198. package/dist/useGridSelectionAnnouncement.main.js +94 -0
  199. package/dist/useGridSelectionAnnouncement.main.js.map +1 -0
  200. package/dist/useGridSelectionAnnouncement.mjs +89 -0
  201. package/dist/useGridSelectionAnnouncement.module.js +89 -0
  202. package/dist/useGridSelectionAnnouncement.module.js.map +1 -0
  203. package/dist/useGridSelectionCheckbox.main.js +39 -0
  204. package/dist/useGridSelectionCheckbox.main.js.map +1 -0
  205. package/dist/useGridSelectionCheckbox.mjs +34 -0
  206. package/dist/useGridSelectionCheckbox.module.js +34 -0
  207. package/dist/useGridSelectionCheckbox.module.js.map +1 -0
  208. package/dist/useHighlightSelectionDescription.main.js +55 -0
  209. package/dist/useHighlightSelectionDescription.main.js.map +1 -0
  210. package/dist/useHighlightSelectionDescription.mjs +50 -0
  211. package/dist/useHighlightSelectionDescription.module.js +50 -0
  212. package/dist/useHighlightSelectionDescription.module.js.map +1 -0
  213. package/dist/utils.main.js +20 -0
  214. package/dist/utils.main.js.map +1 -0
  215. package/dist/utils.mjs +15 -0
  216. package/dist/utils.module.js +15 -0
  217. package/dist/utils.module.js.map +1 -0
  218. package/dist/zh-CN.main.js +15 -0
  219. package/dist/zh-CN.main.js.map +1 -0
  220. package/dist/zh-CN.mjs +17 -0
  221. package/dist/zh-CN.module.js +17 -0
  222. package/dist/zh-CN.module.js.map +1 -0
  223. package/dist/zh-TW.main.js +15 -0
  224. package/dist/zh-TW.main.js.map +1 -0
  225. package/dist/zh-TW.mjs +17 -0
  226. package/dist/zh-TW.module.js +17 -0
  227. package/dist/zh-TW.module.js.map +1 -0
  228. package/package.json +23 -15
  229. package/src/GridKeyboardDelegate.ts +93 -83
  230. package/src/index.ts +17 -5
  231. package/src/useGrid.ts +77 -78
  232. package/src/useGridCell.ts +96 -46
  233. package/src/useGridRow.ts +32 -21
  234. package/src/useGridRowGroup.ts +3 -3
  235. package/src/useGridSelectionAnnouncement.ts +115 -0
  236. package/src/useGridSelectionCheckbox.ts +48 -0
  237. package/src/useHighlightSelectionDescription.ts +52 -0
  238. package/src/utils.ts +13 -3
  239. package/LICENSE +0 -201
@@ -10,31 +10,39 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
+ import {DOMAttributes, FocusableElement, RefObject} from '@react-types/shared';
13
14
  import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus';
14
- import {GridCollection} from '@react-types/grid';
15
- import {gridKeyboardDelegates} from './utils';
15
+ import {getScrollParent, mergeProps, scrollIntoViewport} from '@react-aria/utils';
16
+ import {GridCollection, GridNode} from '@react-types/grid';
17
+ import {gridMap} from './utils';
16
18
  import {GridState} from '@react-stately/grid';
17
- import {HTMLAttributes, KeyboardEvent as ReactKeyboardEvent, RefObject} from 'react';
18
- import {isFocusVisible, usePress} from '@react-aria/interactions';
19
- import {mergeProps} from '@react-aria/utils';
20
- import {Node as RSNode} from '@react-types/shared';
19
+ import {isFocusVisible} from '@react-aria/interactions';
20
+ import {KeyboardEvent as ReactKeyboardEvent, useRef} from 'react';
21
21
  import {useLocale} from '@react-aria/i18n';
22
22
  import {useSelectableItem} from '@react-aria/selection';
23
23
 
24
- interface GridCellProps {
24
+ export interface GridCellProps {
25
25
  /** An object representing the grid cell. Contains all the relevant information that makes up the grid cell. */
26
- node: RSNode<unknown>,
26
+ node: GridNode<unknown>,
27
27
  /** Whether the grid cell is contained in a virtual scroller. */
28
28
  isVirtualized?: boolean,
29
29
  /** Whether the cell or its first focusable child element should be focused when the grid cell is focused. */
30
30
  focusMode?: 'child' | 'cell',
31
31
  /** Whether selection should occur on press up instead of press down. */
32
- shouldSelectOnPressUp?: boolean
32
+ shouldSelectOnPressUp?: boolean,
33
+ /**
34
+ * Handler that is called when a user performs an action on the cell.
35
+ * Please use onCellAction at the collection level instead.
36
+ * @deprecated
37
+ **/
38
+ onAction?: () => void
33
39
  }
34
40
 
35
- interface GridCellAria {
41
+ export interface GridCellAria {
36
42
  /** Props for the grid cell element. */
37
- gridCellProps: HTMLAttributes<HTMLElement>
43
+ gridCellProps: DOMAttributes,
44
+ /** Whether the cell is currently in a pressed state. */
45
+ isPressed: boolean
38
46
  }
39
47
 
40
48
  /**
@@ -42,51 +50,62 @@ interface GridCellAria {
42
50
  * @param props - Props for the cell.
43
51
  * @param state - State of the parent grid, as returned by `useGridState`.
44
52
  */
45
- export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps, state: GridState<T, C>, ref: RefObject<HTMLElement>): GridCellAria {
53
+ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps, state: GridState<T, C>, ref: RefObject<FocusableElement | null>): GridCellAria {
46
54
  let {
47
55
  node,
48
56
  isVirtualized,
49
57
  focusMode = 'child',
50
- shouldSelectOnPressUp
58
+ shouldSelectOnPressUp,
59
+ onAction
51
60
  } = props;
52
61
 
53
62
  let {direction} = useLocale();
54
- let keyboardDelegate = gridKeyboardDelegates.get(state);
63
+ let {keyboardDelegate, actions: {onCellAction}} = gridMap.get(state);
64
+
65
+ // We need to track the key of the item at the time it was last focused so that we force
66
+ // focus to go to the item when the DOM node is reused for a different item in a virtualizer.
67
+ let keyWhenFocused = useRef(null);
55
68
 
56
69
  // Handles focusing the cell. If there is a focusable child,
57
70
  // it is focused, otherwise the cell itself is focused.
58
71
  let focus = () => {
59
72
  let treeWalker = getFocusableTreeWalker(ref.current);
60
73
  if (focusMode === 'child') {
74
+ // If focus is already on a focusable child within the cell, early return so we don't shift focus
75
+ if (ref.current.contains(document.activeElement) && ref.current !== document.activeElement) {
76
+ return;
77
+ }
78
+
61
79
  let focusable = state.selectionManager.childFocusStrategy === 'last'
62
80
  ? last(treeWalker)
63
- : treeWalker.firstChild() as HTMLElement;
81
+ : treeWalker.firstChild() as FocusableElement;
64
82
  if (focusable) {
65
83
  focusSafely(focusable);
66
84
  return;
67
85
  }
68
86
  }
69
87
 
70
- if (!ref.current.contains(document.activeElement)) {
88
+ if (
89
+ (keyWhenFocused.current != null && node.key !== keyWhenFocused.current) ||
90
+ !ref.current.contains(document.activeElement)
91
+ ) {
71
92
  focusSafely(ref.current);
72
93
  }
73
94
  };
74
95
 
75
- let {itemProps} = useSelectableItem({
96
+ let {itemProps, isPressed} = useSelectableItem({
76
97
  selectionManager: state.selectionManager,
77
98
  key: node.key,
78
99
  ref,
79
100
  isVirtualized,
80
101
  focus,
81
- shouldSelectOnPressUp
102
+ shouldSelectOnPressUp,
103
+ onAction: onCellAction ? () => onCellAction(node.key) : onAction,
104
+ isDisabled: state.collection.size === 0
82
105
  });
83
106
 
84
- // TODO: move into useSelectableItem?
85
- let isDisabled = state.disabledKeys.has(node.key) || state.disabledKeys.has(node.parentKey);
86
- let {pressProps} = usePress({...itemProps, isDisabled});
87
-
88
- let onKeyDown = (e: ReactKeyboardEvent) => {
89
- if (!e.currentTarget.contains(e.target as HTMLElement)) {
107
+ let onKeyDownCapture = (e: ReactKeyboardEvent) => {
108
+ if (!e.currentTarget.contains(e.target as Element) || state.isKeyboardNavigationDisabled) {
90
109
  return;
91
110
  }
92
111
 
@@ -97,18 +116,19 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
97
116
  case 'ArrowLeft': {
98
117
  // Find the next focusable element within the cell.
99
118
  let focusable = direction === 'rtl'
100
- ? walker.nextNode() as HTMLElement
101
- : walker.previousNode() as HTMLElement;
119
+ ? walker.nextNode() as FocusableElement
120
+ : walker.previousNode() as FocusableElement;
102
121
 
103
122
  // Don't focus the cell itself if focusMode is "child"
104
123
  if (focusMode === 'child' && focusable === ref.current) {
105
124
  focusable = null;
106
125
  }
107
126
 
127
+ e.preventDefault();
128
+ e.stopPropagation();
108
129
  if (focusable) {
109
- e.preventDefault();
110
- e.stopPropagation();
111
130
  focusSafely(focusable);
131
+ scrollIntoViewport(focusable, {containingElement: getScrollParent(ref.current)});
112
132
  } else {
113
133
  // If there is no next focusable child, then move to the next cell to the left of this one.
114
134
  // This will be handled by useSelectableCollection. However, if there is no cell to the left
@@ -117,20 +137,26 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
117
137
  // child, depending on the focus mode.
118
138
  let prev = keyboardDelegate.getKeyLeftOf(node.key);
119
139
  if (prev !== node.key) {
140
+ // We prevent the capturing event from reaching children of the cell, e.g. pickers.
141
+ // We want arrow keys to navigate to the next cell instead. We need to re-dispatch
142
+ // the event from a higher parent so it still bubbles and gets handled by useSelectableCollection.
143
+ ref.current.parentElement.dispatchEvent(
144
+ new KeyboardEvent(e.nativeEvent.type, e.nativeEvent)
145
+ );
120
146
  break;
121
147
  }
122
148
 
123
- e.preventDefault();
124
- e.stopPropagation();
125
149
  if (focusMode === 'cell' && direction === 'rtl') {
126
150
  focusSafely(ref.current);
151
+ scrollIntoViewport(ref.current, {containingElement: getScrollParent(ref.current)});
127
152
  } else {
128
153
  walker.currentNode = ref.current;
129
154
  focusable = direction === 'rtl'
130
- ? walker.firstChild() as HTMLElement
155
+ ? walker.firstChild() as FocusableElement
131
156
  : last(walker);
132
157
  if (focusable) {
133
158
  focusSafely(focusable);
159
+ scrollIntoViewport(focusable, {containingElement: getScrollParent(ref.current)});
134
160
  }
135
161
  }
136
162
  }
@@ -138,34 +164,41 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
138
164
  }
139
165
  case 'ArrowRight': {
140
166
  let focusable = direction === 'rtl'
141
- ? walker.previousNode() as HTMLElement
142
- : walker.nextNode() as HTMLElement;
167
+ ? walker.previousNode() as FocusableElement
168
+ : walker.nextNode() as FocusableElement;
143
169
 
144
170
  if (focusMode === 'child' && focusable === ref.current) {
145
171
  focusable = null;
146
172
  }
147
173
 
174
+ e.preventDefault();
175
+ e.stopPropagation();
148
176
  if (focusable) {
149
- e.preventDefault();
150
- e.stopPropagation();
151
177
  focusSafely(focusable);
178
+ scrollIntoViewport(focusable, {containingElement: getScrollParent(ref.current)});
152
179
  } else {
153
180
  let next = keyboardDelegate.getKeyRightOf(node.key);
154
181
  if (next !== node.key) {
182
+ // We prevent the capturing event from reaching children of the cell, e.g. pickers.
183
+ // We want arrow keys to navigate to the next cell instead. We need to re-dispatch
184
+ // the event from a higher parent so it still bubbles and gets handled by useSelectableCollection.
185
+ ref.current.parentElement.dispatchEvent(
186
+ new KeyboardEvent(e.nativeEvent.type, e.nativeEvent)
187
+ );
155
188
  break;
156
189
  }
157
190
 
158
- e.preventDefault();
159
- e.stopPropagation();
160
191
  if (focusMode === 'cell' && direction === 'ltr') {
161
192
  focusSafely(ref.current);
193
+ scrollIntoViewport(ref.current, {containingElement: getScrollParent(ref.current)});
162
194
  } else {
163
195
  walker.currentNode = ref.current;
164
196
  focusable = direction === 'rtl'
165
197
  ? last(walker)
166
- : walker.firstChild() as HTMLElement;
198
+ : walker.firstChild() as FocusableElement;
167
199
  if (focusable) {
168
200
  focusSafely(focusable);
201
+ scrollIntoViewport(focusable, {containingElement: getScrollParent(ref.current)});
169
202
  }
170
203
  }
171
204
  }
@@ -176,7 +209,7 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
176
209
  // Prevent this event from reaching cell children, e.g. menu buttons. We want arrow keys to navigate
177
210
  // to the cell above/below instead. We need to re-dispatch the event from a higher parent so it still
178
211
  // bubbles and gets handled by useSelectableCollection.
179
- if (!e.altKey && ref.current.contains(e.target as HTMLElement)) {
212
+ if (!e.altKey && ref.current.contains(e.target as Element)) {
180
213
  e.stopPropagation();
181
214
  e.preventDefault();
182
215
  ref.current.parentElement.dispatchEvent(
@@ -190,6 +223,7 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
190
223
  // Grid cells can have focusable elements inside them. In this case, focus should
191
224
  // be marshalled to that element rather than focusing the cell itself.
192
225
  let onFocus = (e) => {
226
+ keyWhenFocused.current = node.key;
193
227
  if (e.target !== ref.current) {
194
228
  // useSelectableItem only handles setting the focused key when
195
229
  // the focused element is the gridcell itself. We also want to
@@ -212,26 +246,42 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
212
246
  });
213
247
  };
214
248
 
215
- let gridCellProps: HTMLAttributes<HTMLElement> = mergeProps(pressProps, {
249
+ let gridCellProps: DOMAttributes = mergeProps(itemProps, {
216
250
  role: 'gridcell',
217
- onKeyDownCapture: onKeyDown,
251
+ onKeyDownCapture,
218
252
  onFocus
219
253
  });
220
254
 
221
255
  if (isVirtualized) {
222
- gridCellProps['aria-colindex'] = node.index + 1; // aria-colindex is 1-based
256
+ gridCellProps['aria-colindex'] = (node.colIndex ?? node.index) + 1; // aria-colindex is 1-based
257
+ }
258
+
259
+ // When pressing with a pointer and cell selection is not enabled, usePress will be applied to the
260
+ // row rather than the cell. However, when the row is draggable, usePress cannot preventDefault
261
+ // on pointer down, so the browser will try to focus the cell which has a tabIndex applied.
262
+ // To avoid this, remove the tabIndex from the cell briefly on pointer down.
263
+ if (shouldSelectOnPressUp && gridCellProps.tabIndex != null && gridCellProps.onPointerDown == null) {
264
+ gridCellProps.onPointerDown = (e) => {
265
+ let el = e.currentTarget;
266
+ let tabindex = el.getAttribute('tabindex');
267
+ el.removeAttribute('tabindex');
268
+ requestAnimationFrame(() => {
269
+ el.setAttribute('tabindex', tabindex);
270
+ });
271
+ };
223
272
  }
224
273
 
225
274
  return {
226
- gridCellProps
275
+ gridCellProps,
276
+ isPressed
227
277
  };
228
278
  }
229
279
 
230
280
  function last(walker: TreeWalker) {
231
- let next: HTMLElement;
232
- let last: HTMLElement;
281
+ let next: FocusableElement;
282
+ let last: FocusableElement;
233
283
  do {
234
- last = walker.lastChild() as HTMLElement;
284
+ last = walker.lastChild() as FocusableElement;
235
285
  if (last) {
236
286
  next = last;
237
287
  }
package/src/useGridRow.ts CHANGED
@@ -10,25 +10,33 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {GridCollection} from '@react-types/grid';
13
+ import {chain} from '@react-aria/utils';
14
+ import {DOMAttributes, FocusableElement, RefObject} from '@react-types/shared';
15
+ import {GridCollection, GridNode} from '@react-types/grid';
16
+ import {gridMap} from './utils';
14
17
  import {GridState} from '@react-stately/grid';
15
- import {HTMLAttributes, RefObject} from 'react';
16
- import {Node} from '@react-types/shared';
17
- import {usePress} from '@react-aria/interactions';
18
- import {useSelectableItem} from '@react-aria/selection';
18
+ import {SelectableItemStates, useSelectableItem} from '@react-aria/selection';
19
19
 
20
20
  export interface GridRowProps<T> {
21
21
  /** An object representing the grid row. Contains all the relevant information that makes up the grid row. */
22
- node: Node<T>,
22
+ node: GridNode<T>,
23
23
  /** Whether the grid row is contained in a virtual scroller. */
24
24
  isVirtualized?: boolean,
25
25
  /** Whether selection should occur on press up instead of press down. */
26
- shouldSelectOnPressUp?: boolean
26
+ shouldSelectOnPressUp?: boolean,
27
+ /**
28
+ * Handler that is called when a user performs an action on the row.
29
+ * Please use onCellAction at the collection level instead.
30
+ * @deprecated
31
+ **/
32
+ onAction?: () => void
27
33
  }
28
34
 
29
- export interface GridRowAria {
35
+ export interface GridRowAria extends SelectableItemStates {
30
36
  /** Props for the grid row element. */
31
- rowProps: HTMLAttributes<HTMLElement>
37
+ rowProps: DOMAttributes,
38
+ /** Whether the row is currently in a pressed state. */
39
+ isPressed: boolean
32
40
  }
33
41
 
34
42
  /**
@@ -36,31 +44,33 @@ export interface GridRowAria {
36
44
  * @param props - Props for the row.
37
45
  * @param state - State of the parent grid, as returned by `useGridState`.
38
46
  */
39
- export function useGridRow<T, C extends GridCollection<T>, S extends GridState<T, C>>(props: GridRowProps<T>, state: S, ref: RefObject<HTMLElement>): GridRowAria {
47
+ export function useGridRow<T, C extends GridCollection<T>, S extends GridState<T, C>>(props: GridRowProps<T>, state: S, ref: RefObject<FocusableElement | null>): GridRowAria {
40
48
  let {
41
49
  node,
42
50
  isVirtualized,
43
- shouldSelectOnPressUp
51
+ shouldSelectOnPressUp,
52
+ onAction
44
53
  } = props;
45
54
 
46
- let {itemProps} = useSelectableItem({
55
+ let {actions} = gridMap.get(state);
56
+ let onRowAction = actions.onRowAction ? () => actions.onRowAction(node.key) : onAction;
57
+ let {itemProps, ...states} = useSelectableItem({
47
58
  selectionManager: state.selectionManager,
48
59
  key: node.key,
49
60
  ref,
50
61
  isVirtualized,
51
- shouldSelectOnPressUp
62
+ shouldSelectOnPressUp,
63
+ onAction: onRowAction || node?.props?.onAction ? chain(node?.props?.onAction, onRowAction) : undefined,
64
+ isDisabled: state.collection.size === 0
52
65
  });
53
66
 
54
67
  let isSelected = state.selectionManager.isSelected(node.key);
55
- let isDisabled = state.disabledKeys.has(node.key);
56
68
 
57
- // TODO: move into useSelectableItem?
58
- let {pressProps} = usePress({...itemProps, isDisabled});
59
-
60
- let rowProps: HTMLAttributes<HTMLElement> = {
69
+ let rowProps: DOMAttributes = {
61
70
  role: 'row',
62
- 'aria-selected': isSelected,
63
- ...pressProps
71
+ 'aria-selected': state.selectionManager.selectionMode !== 'none' ? isSelected : undefined,
72
+ 'aria-disabled': states.isDisabled || undefined,
73
+ ...itemProps
64
74
  };
65
75
 
66
76
  if (isVirtualized) {
@@ -68,6 +78,7 @@ export function useGridRow<T, C extends GridCollection<T>, S extends GridState<T
68
78
  }
69
79
 
70
80
  return {
71
- rowProps
81
+ rowProps,
82
+ ...states
72
83
  };
73
84
  }
@@ -10,11 +10,11 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {HTMLAttributes} from 'react';
13
+ import {DOMAttributes} from '@react-types/shared';
14
14
 
15
- interface GridRowGroupAria {
15
+ export interface GridRowGroupAria {
16
16
  /** Props for the row group element. */
17
- rowGroupProps: HTMLAttributes<HTMLElement>
17
+ rowGroupProps: DOMAttributes
18
18
  }
19
19
 
20
20
  /**
@@ -0,0 +1,115 @@
1
+ /*
2
+ * Copyright 2022 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {announce} from '@react-aria/live-announcer';
14
+ import {Collection, Key, Node, Selection} from '@react-types/shared';
15
+ // @ts-ignore
16
+ import intlMessages from '../intl/*.json';
17
+ import {SelectionManager} from '@react-stately/selection';
18
+ import {useLocalizedStringFormatter} from '@react-aria/i18n';
19
+ import {useRef} from 'react';
20
+ import {useUpdateEffect} from '@react-aria/utils';
21
+
22
+ export interface GridSelectionAnnouncementProps {
23
+ /**
24
+ * A function that returns the text that should be announced by assistive technology when a row is added or removed from selection.
25
+ * @default (key) => state.collection.getItem(key)?.textValue
26
+ */
27
+ getRowText?: (key: Key) => string
28
+ }
29
+
30
+ interface GridSelectionState<T> {
31
+ /** A collection of items in the grid. */
32
+ collection: Collection<Node<T>>,
33
+ /** A set of items that are disabled. */
34
+ disabledKeys: Set<Key>,
35
+ /** A selection manager to read and update multiple selection state. */
36
+ selectionManager: SelectionManager
37
+ }
38
+
39
+ export function useGridSelectionAnnouncement<T>(props: GridSelectionAnnouncementProps, state: GridSelectionState<T>) {
40
+ let {
41
+ getRowText = (key) => state.collection.getTextValue?.(key) ?? state.collection.getItem(key)?.textValue
42
+ } = props;
43
+ let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/grid');
44
+
45
+ // Many screen readers do not announce when items in a grid are selected/deselected.
46
+ // We do this using an ARIA live region.
47
+ let selection = state.selectionManager.rawSelection;
48
+ let lastSelection = useRef(selection);
49
+ useUpdateEffect(() => {
50
+ if (!state.selectionManager.isFocused) {
51
+ lastSelection.current = selection;
52
+
53
+ return;
54
+ }
55
+
56
+ let addedKeys = diffSelection(selection, lastSelection.current);
57
+ let removedKeys = diffSelection(lastSelection.current, selection);
58
+
59
+ // If adding or removing a single row from the selection, announce the name of that item.
60
+ let isReplace = state.selectionManager.selectionBehavior === 'replace';
61
+ let messages = [];
62
+
63
+ if ((state.selectionManager.selectedKeys.size === 1 && isReplace)) {
64
+ if (state.collection.getItem(state.selectionManager.selectedKeys.keys().next().value)) {
65
+ let currentSelectionText = getRowText(state.selectionManager.selectedKeys.keys().next().value);
66
+ if (currentSelectionText) {
67
+ messages.push(stringFormatter.format('selectedItem', {item: currentSelectionText}));
68
+ }
69
+ }
70
+ } else if (addedKeys.size === 1 && removedKeys.size === 0) {
71
+ let addedText = getRowText(addedKeys.keys().next().value);
72
+ if (addedText) {
73
+ messages.push(stringFormatter.format('selectedItem', {item: addedText}));
74
+ }
75
+ } else if (removedKeys.size === 1 && addedKeys.size === 0) {
76
+ if (state.collection.getItem(removedKeys.keys().next().value)) {
77
+ let removedText = getRowText(removedKeys.keys().next().value);
78
+ if (removedText) {
79
+ messages.push(stringFormatter.format('deselectedItem', {item: removedText}));
80
+ }
81
+ }
82
+ }
83
+
84
+ // Announce how many items are selected, except when selecting the first item.
85
+ if (state.selectionManager.selectionMode === 'multiple') {
86
+ if (messages.length === 0 || selection === 'all' || selection.size > 1 || lastSelection.current === 'all' || lastSelection.current?.size > 1) {
87
+ messages.push(selection === 'all'
88
+ ? stringFormatter.format('selectedAll')
89
+ : stringFormatter.format('selectedCount', {count: selection.size})
90
+ );
91
+ }
92
+ }
93
+
94
+ if (messages.length > 0) {
95
+ announce(messages.join(' '));
96
+ }
97
+
98
+ lastSelection.current = selection;
99
+ }, [selection]);
100
+ }
101
+
102
+ function diffSelection(a: Selection, b: Selection): Set<Key> {
103
+ let res = new Set<Key>();
104
+ if (a === 'all' || b === 'all') {
105
+ return res;
106
+ }
107
+
108
+ for (let key of a.keys()) {
109
+ if (!b.has(key)) {
110
+ res.add(key);
111
+ }
112
+ }
113
+
114
+ return res;
115
+ }
@@ -0,0 +1,48 @@
1
+ import {AriaCheckboxProps} from '@react-types/checkbox';
2
+ import {GridCollection} from '@react-types/grid';
3
+ import {GridState} from '@react-stately/grid';
4
+ // @ts-ignore
5
+ import intlMessages from '../intl/*.json';
6
+ import {Key} from '@react-types/shared';
7
+ import {useId} from '@react-aria/utils';
8
+ import {useLocalizedStringFormatter} from '@react-aria/i18n';
9
+
10
+ export interface AriaGridSelectionCheckboxProps {
11
+ /** A unique key for the checkbox. */
12
+ key: Key
13
+ }
14
+
15
+ export interface GridSelectionCheckboxAria {
16
+ /** Props for the row selection checkbox element. */
17
+ checkboxProps: AriaCheckboxProps
18
+ }
19
+
20
+
21
+ /**
22
+ * Provides the behavior and accessibility implementation for a selection checkbox in a grid.
23
+ * @param props - Props for the selection checkbox.
24
+ * @param state - State of the grid, as returned by `useGridState`.
25
+ */
26
+ export function useGridSelectionCheckbox<T, C extends GridCollection<T>>(props: AriaGridSelectionCheckboxProps, state: GridState<T, C>): GridSelectionCheckboxAria {
27
+ let {key} = props;
28
+
29
+ let manager = state.selectionManager;
30
+ let checkboxId = useId();
31
+ let isDisabled = !state.selectionManager.canSelectItem(key);
32
+ let isSelected = state.selectionManager.isSelected(key);
33
+
34
+ // Checkbox should always toggle selection, regardless of selectionBehavior.
35
+ let onChange = () => manager.toggleSelection(key);
36
+
37
+ const stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/grid');
38
+
39
+ return {
40
+ checkboxProps: {
41
+ id: checkboxId,
42
+ 'aria-label': stringFormatter.format('select'),
43
+ isSelected,
44
+ isDisabled,
45
+ onChange
46
+ }
47
+ };
48
+ }
@@ -0,0 +1,52 @@
1
+ /*
2
+ * Copyright 2021 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {AriaLabelingProps} from '@react-types/shared';
14
+ // @ts-ignore
15
+ import intlMessages from '../intl/*.json';
16
+ import {MultipleSelectionManager} from '@react-stately/selection';
17
+ import {useDescription} from '@react-aria/utils';
18
+ import {useInteractionModality} from '@react-aria/interactions';
19
+ import {useLocalizedStringFormatter} from '@react-aria/i18n';
20
+ import {useMemo} from 'react';
21
+
22
+ export interface HighlightSelectionDescriptionProps {
23
+ selectionManager: MultipleSelectionManager,
24
+ hasItemActions?: boolean
25
+ }
26
+
27
+ /**
28
+ * Computes the description for a grid selectable collection.
29
+ * @param props
30
+ */
31
+ export function useHighlightSelectionDescription(props: HighlightSelectionDescriptionProps): AriaLabelingProps {
32
+ let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/grid');
33
+ let modality = useInteractionModality();
34
+ // null is the default if the user hasn't interacted with the table at all yet or the rest of the page
35
+ let shouldLongPress = (modality === 'pointer' || modality === 'virtual' || modality == null)
36
+ && typeof window !== 'undefined' && 'ontouchstart' in window;
37
+
38
+ let interactionDescription = useMemo(() => {
39
+ let selectionMode = props.selectionManager.selectionMode;
40
+ let selectionBehavior = props.selectionManager.selectionBehavior;
41
+
42
+ let message = undefined;
43
+ if (shouldLongPress) {
44
+ message = stringFormatter.format('longPressToSelect');
45
+ }
46
+
47
+ return selectionBehavior === 'replace' && selectionMode !== 'none' && props.hasItemActions ? message : undefined;
48
+ }, [props.selectionManager.selectionMode, props.selectionManager.selectionBehavior, props.hasItemActions, stringFormatter, shouldLongPress]);
49
+
50
+ let descriptionProps = useDescription(interactionDescription);
51
+ return descriptionProps;
52
+ }
package/src/utils.ts CHANGED
@@ -12,7 +12,17 @@
12
12
 
13
13
  import type {GridCollection} from '@react-types/grid';
14
14
  import type {GridState} from '@react-stately/grid';
15
- import type {KeyboardDelegate} from '@react-types/shared';
15
+ import type {Key, KeyboardDelegate} from '@react-types/shared';
16
16
 
17
- // Used to share keyboard delegate between useGrid and useGridCell
18
- export const gridKeyboardDelegates = new WeakMap<GridState<unknown, GridCollection<unknown>>, KeyboardDelegate>();
17
+ interface GridMapShared {
18
+ keyboardDelegate: KeyboardDelegate,
19
+ actions: {
20
+ onRowAction: (key: Key) => void,
21
+ onCellAction: (key: Key) => void
22
+ }
23
+ }
24
+
25
+ // Used to share:
26
+ // keyboard delegate between useGrid and useGridCell
27
+ // onRowAction/onCellAction across hooks
28
+ export const gridMap = new WeakMap<GridState<unknown, GridCollection<unknown>>, GridMapShared>();