@react-spectrum/s2 3.0.0-nightly-5a0b4fabc-240924 → 3.0.0-nightly-a626c2596-240926

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 (251) hide show
  1. package/dist/Accordion.cjs +1 -1
  2. package/dist/Accordion.cjs.map +1 -1
  3. package/dist/Accordion.css.map +1 -1
  4. package/dist/Accordion.mjs +2 -2
  5. package/dist/Accordion.mjs.map +1 -1
  6. package/dist/Button.cjs +5 -1
  7. package/dist/Button.cjs.map +1 -1
  8. package/dist/Button.css.map +1 -1
  9. package/dist/Button.mjs +5 -1
  10. package/dist/Button.mjs.map +1 -1
  11. package/dist/Card.cjs +1 -1
  12. package/dist/Card.cjs.map +1 -1
  13. package/dist/Card.css.map +1 -1
  14. package/dist/Card.mjs +1 -1
  15. package/dist/Card.mjs.map +1 -1
  16. package/dist/Checkbox.cjs +4 -1
  17. package/dist/Checkbox.cjs.map +1 -1
  18. package/dist/Checkbox.css +4 -0
  19. package/dist/Checkbox.css.map +1 -1
  20. package/dist/Checkbox.mjs +4 -1
  21. package/dist/Checkbox.mjs.map +1 -1
  22. package/dist/ComboBox.cjs +3 -0
  23. package/dist/ComboBox.cjs.map +1 -1
  24. package/dist/ComboBox.css.map +1 -1
  25. package/dist/ComboBox.mjs +3 -0
  26. package/dist/ComboBox.mjs.map +1 -1
  27. package/dist/Content.cjs +0 -2
  28. package/dist/Content.cjs.map +1 -1
  29. package/dist/Content.mjs +1 -2
  30. package/dist/Content.mjs.map +1 -1
  31. package/dist/Dialog.cjs +7 -5
  32. package/dist/Dialog.cjs.map +1 -1
  33. package/dist/Dialog.css.map +1 -1
  34. package/dist/Dialog.mjs +8 -6
  35. package/dist/Dialog.mjs.map +1 -1
  36. package/dist/Disclosure.cjs +2 -2
  37. package/dist/Disclosure.cjs.map +1 -1
  38. package/dist/Disclosure.css.map +1 -1
  39. package/dist/Disclosure.mjs +3 -3
  40. package/dist/Disclosure.mjs.map +1 -1
  41. package/dist/MoveHorizontalCircleTableWidget.cjs +33 -0
  42. package/dist/MoveHorizontalCircleTableWidget.cjs.map +1 -0
  43. package/dist/MoveHorizontalCircleTableWidget.mjs +28 -0
  44. package/dist/MoveHorizontalCircleTableWidget.mjs.map +1 -0
  45. package/dist/Picker.cjs +3 -0
  46. package/dist/Picker.cjs.map +1 -1
  47. package/dist/Picker.css.map +1 -1
  48. package/dist/Picker.mjs +3 -0
  49. package/dist/Picker.mjs.map +1 -1
  50. package/dist/Popover.cjs +1 -0
  51. package/dist/Popover.cjs.map +1 -1
  52. package/dist/Popover.css +4 -0
  53. package/dist/Popover.css.map +1 -1
  54. package/dist/Popover.mjs +1 -0
  55. package/dist/Popover.mjs.map +1 -1
  56. package/dist/SegmentedControl.cjs +11 -1
  57. package/dist/SegmentedControl.cjs.map +1 -1
  58. package/dist/SegmentedControl.css +24 -0
  59. package/dist/SegmentedControl.css.map +1 -1
  60. package/dist/SegmentedControl.mjs +11 -1
  61. package/dist/SegmentedControl.mjs.map +1 -1
  62. package/dist/Table.cjs +1093 -0
  63. package/dist/Table.cjs.map +1 -0
  64. package/dist/Table.css +821 -0
  65. package/dist/Table.css.map +1 -0
  66. package/dist/Table.mjs +1083 -0
  67. package/dist/Table.mjs.map +1 -0
  68. package/dist/TagGroup.cjs +5 -3
  69. package/dist/TagGroup.cjs.map +1 -1
  70. package/dist/TagGroup.css.map +1 -1
  71. package/dist/TagGroup.mjs +5 -3
  72. package/dist/TagGroup.mjs.map +1 -1
  73. package/dist/ar-AE.cjs +5 -0
  74. package/dist/ar-AE.cjs.map +1 -1
  75. package/dist/ar-AE.mjs +5 -0
  76. package/dist/ar-AE.mjs.map +1 -1
  77. package/dist/bg-BG.cjs +5 -0
  78. package/dist/bg-BG.cjs.map +1 -1
  79. package/dist/bg-BG.mjs +5 -0
  80. package/dist/bg-BG.mjs.map +1 -1
  81. package/dist/cs-CZ.cjs +5 -0
  82. package/dist/cs-CZ.cjs.map +1 -1
  83. package/dist/cs-CZ.mjs +5 -0
  84. package/dist/cs-CZ.mjs.map +1 -1
  85. package/dist/da-DK.cjs +5 -0
  86. package/dist/da-DK.cjs.map +1 -1
  87. package/dist/da-DK.mjs +5 -0
  88. package/dist/da-DK.mjs.map +1 -1
  89. package/dist/de-DE.cjs +5 -0
  90. package/dist/de-DE.cjs.map +1 -1
  91. package/dist/de-DE.mjs +5 -0
  92. package/dist/de-DE.mjs.map +1 -1
  93. package/dist/el-GR.cjs +5 -0
  94. package/dist/el-GR.cjs.map +1 -1
  95. package/dist/el-GR.mjs +5 -0
  96. package/dist/el-GR.mjs.map +1 -1
  97. package/dist/en-US.cjs +5 -0
  98. package/dist/en-US.cjs.map +1 -1
  99. package/dist/en-US.mjs +5 -0
  100. package/dist/en-US.mjs.map +1 -1
  101. package/dist/es-ES.cjs +5 -0
  102. package/dist/es-ES.cjs.map +1 -1
  103. package/dist/es-ES.mjs +5 -0
  104. package/dist/es-ES.mjs.map +1 -1
  105. package/dist/et-EE.cjs +5 -0
  106. package/dist/et-EE.cjs.map +1 -1
  107. package/dist/et-EE.mjs +5 -0
  108. package/dist/et-EE.mjs.map +1 -1
  109. package/dist/fi-FI.cjs +5 -0
  110. package/dist/fi-FI.cjs.map +1 -1
  111. package/dist/fi-FI.mjs +5 -0
  112. package/dist/fi-FI.mjs.map +1 -1
  113. package/dist/fr-FR.cjs +5 -0
  114. package/dist/fr-FR.cjs.map +1 -1
  115. package/dist/fr-FR.mjs +5 -0
  116. package/dist/fr-FR.mjs.map +1 -1
  117. package/dist/he-IL.cjs +5 -0
  118. package/dist/he-IL.cjs.map +1 -1
  119. package/dist/he-IL.mjs +5 -0
  120. package/dist/he-IL.mjs.map +1 -1
  121. package/dist/hr-HR.cjs +5 -0
  122. package/dist/hr-HR.cjs.map +1 -1
  123. package/dist/hr-HR.mjs +5 -0
  124. package/dist/hr-HR.mjs.map +1 -1
  125. package/dist/hu-HU.cjs +5 -0
  126. package/dist/hu-HU.cjs.map +1 -1
  127. package/dist/hu-HU.mjs +5 -0
  128. package/dist/hu-HU.mjs.map +1 -1
  129. package/dist/it-IT.cjs +5 -0
  130. package/dist/it-IT.cjs.map +1 -1
  131. package/dist/it-IT.mjs +5 -0
  132. package/dist/it-IT.mjs.map +1 -1
  133. package/dist/ja-JP.cjs +5 -0
  134. package/dist/ja-JP.cjs.map +1 -1
  135. package/dist/ja-JP.mjs +5 -0
  136. package/dist/ja-JP.mjs.map +1 -1
  137. package/dist/ko-KR.cjs +5 -0
  138. package/dist/ko-KR.cjs.map +1 -1
  139. package/dist/ko-KR.mjs +5 -0
  140. package/dist/ko-KR.mjs.map +1 -1
  141. package/dist/lt-LT.cjs +5 -0
  142. package/dist/lt-LT.cjs.map +1 -1
  143. package/dist/lt-LT.mjs +5 -0
  144. package/dist/lt-LT.mjs.map +1 -1
  145. package/dist/lv-LV.cjs +5 -0
  146. package/dist/lv-LV.cjs.map +1 -1
  147. package/dist/lv-LV.mjs +5 -0
  148. package/dist/lv-LV.mjs.map +1 -1
  149. package/dist/main.cjs +8 -0
  150. package/dist/main.cjs.map +1 -1
  151. package/dist/module.mjs +3 -1
  152. package/dist/module.mjs.map +1 -1
  153. package/dist/nb-NO.cjs +5 -0
  154. package/dist/nb-NO.cjs.map +1 -1
  155. package/dist/nb-NO.mjs +5 -0
  156. package/dist/nb-NO.mjs.map +1 -1
  157. package/dist/nl-NL.cjs +5 -0
  158. package/dist/nl-NL.cjs.map +1 -1
  159. package/dist/nl-NL.mjs +5 -0
  160. package/dist/nl-NL.mjs.map +1 -1
  161. package/dist/pl-PL.cjs +5 -0
  162. package/dist/pl-PL.cjs.map +1 -1
  163. package/dist/pl-PL.mjs +5 -0
  164. package/dist/pl-PL.mjs.map +1 -1
  165. package/dist/pt-BR.cjs +5 -0
  166. package/dist/pt-BR.cjs.map +1 -1
  167. package/dist/pt-BR.mjs +5 -0
  168. package/dist/pt-BR.mjs.map +1 -1
  169. package/dist/pt-PT.cjs +5 -0
  170. package/dist/pt-PT.cjs.map +1 -1
  171. package/dist/pt-PT.mjs +5 -0
  172. package/dist/pt-PT.mjs.map +1 -1
  173. package/dist/ro-RO.cjs +5 -0
  174. package/dist/ro-RO.cjs.map +1 -1
  175. package/dist/ro-RO.mjs +5 -0
  176. package/dist/ro-RO.mjs.map +1 -1
  177. package/dist/ru-RU.cjs +5 -0
  178. package/dist/ru-RU.cjs.map +1 -1
  179. package/dist/ru-RU.mjs +5 -0
  180. package/dist/ru-RU.mjs.map +1 -1
  181. package/dist/sk-SK.cjs +5 -0
  182. package/dist/sk-SK.cjs.map +1 -1
  183. package/dist/sk-SK.mjs +5 -0
  184. package/dist/sk-SK.mjs.map +1 -1
  185. package/dist/sl-SI.cjs +5 -0
  186. package/dist/sl-SI.cjs.map +1 -1
  187. package/dist/sl-SI.mjs +5 -0
  188. package/dist/sl-SI.mjs.map +1 -1
  189. package/dist/sr-SP.cjs +5 -0
  190. package/dist/sr-SP.cjs.map +1 -1
  191. package/dist/sr-SP.mjs +5 -0
  192. package/dist/sr-SP.mjs.map +1 -1
  193. package/dist/sv-SE.cjs +5 -0
  194. package/dist/sv-SE.cjs.map +1 -1
  195. package/dist/sv-SE.mjs +5 -0
  196. package/dist/sv-SE.mjs.map +1 -1
  197. package/dist/tr-TR.cjs +5 -0
  198. package/dist/tr-TR.cjs.map +1 -1
  199. package/dist/tr-TR.mjs +5 -0
  200. package/dist/tr-TR.mjs.map +1 -1
  201. package/dist/types.d.ts +93 -4
  202. package/dist/types.d.ts.map +1 -1
  203. package/dist/uk-UA.cjs +5 -0
  204. package/dist/uk-UA.cjs.map +1 -1
  205. package/dist/uk-UA.mjs +5 -0
  206. package/dist/uk-UA.mjs.map +1 -1
  207. package/dist/utils.cjs +30 -0
  208. package/dist/utils.cjs.map +1 -0
  209. package/dist/utils.mjs +25 -0
  210. package/dist/utils.mjs.map +1 -0
  211. package/dist/zh-CN.cjs +5 -0
  212. package/dist/zh-CN.cjs.map +1 -1
  213. package/dist/zh-CN.mjs +5 -0
  214. package/dist/zh-CN.mjs.map +1 -1
  215. package/dist/zh-TW.cjs +5 -0
  216. package/dist/zh-TW.cjs.map +1 -1
  217. package/dist/zh-TW.mjs +5 -0
  218. package/dist/zh-TW.mjs.map +1 -1
  219. package/package.json +18 -16
  220. package/src/Accordion.tsx +2 -2
  221. package/src/Button.tsx +22 -14
  222. package/src/Card.tsx +1 -1
  223. package/src/Checkbox.tsx +1 -0
  224. package/src/ComboBox.tsx +3 -0
  225. package/src/Content.tsx +1 -3
  226. package/src/Dialog.tsx +3 -2
  227. package/src/Disclosure.tsx +3 -3
  228. package/src/Picker.tsx +3 -0
  229. package/src/Popover.tsx +4 -1
  230. package/src/SegmentedControl.tsx +14 -3
  231. package/src/Table.tsx +1084 -48
  232. package/src/TagGroup.tsx +3 -2
  233. package/src/index.ts +2 -0
  234. package/src/utils.ts +28 -0
  235. package/style/__tests__/mergeStyles.test.js +32 -0
  236. package/style/__tests__/style-macro.test.js +128 -0
  237. package/style/dist/main.cjs +1984 -0
  238. package/style/dist/main.cjs.map +1 -0
  239. package/style/dist/module.mjs +1973 -0
  240. package/style/dist/module.mjs.map +1 -0
  241. package/style/dist/style-macro.cjs +543 -0
  242. package/style/dist/style-macro.cjs.map +1 -0
  243. package/style/dist/style-macro.mjs +534 -0
  244. package/style/dist/style-macro.mjs.map +1 -0
  245. package/style/dist/types.d.ts +780 -0
  246. package/style/dist/types.d.ts.map +1 -0
  247. package/style/runtime.ts +103 -0
  248. package/style/spectrum-theme.ts +974 -0
  249. package/style/style-macro.ts +638 -0
  250. package/style/tokens.ts +68 -0
  251. package/style/types.ts +177 -0
package/src/Table.tsx CHANGED
@@ -11,85 +11,1121 @@
11
11
  */
12
12
 
13
13
  import {
14
- Column as AriaColumn,
15
- Row as AriaRow,
16
- Table as AriaTable,
17
- TableHeader as AriaTableHeader,
18
14
  Button,
19
- Cell,
15
+ CellRenderProps,
20
16
  Collection,
21
- ColumnProps,
22
- RowProps,
23
- TableHeaderProps,
24
- TableProps,
17
+ ColumnRenderProps,
18
+ ColumnResizer,
19
+ Key,
20
+ Provider,
21
+ Cell as RACCell,
22
+ CellProps as RACCellProps,
23
+ CheckboxContext as RACCheckboxContext,
24
+ Column as RACColumn,
25
+ ColumnProps as RACColumnProps,
26
+ Row as RACRow,
27
+ RowProps as RACRowProps,
28
+ Table as RACTable,
29
+ TableBody as RACTableBody,
30
+ TableBodyProps as RACTableBodyProps,
31
+ TableHeader as RACTableHeader,
32
+ TableHeaderProps as RACTableHeaderProps,
33
+ TableProps as RACTableProps,
34
+ ResizableTableContainer,
35
+ RowRenderProps,
36
+ TableBodyRenderProps,
37
+ TableRenderProps,
38
+ UNSTABLE_TableLayout,
39
+ UNSTABLE_TableLoadingIndicator,
40
+ UNSTABLE_Virtualizer,
41
+ useSlottedContext,
25
42
  useTableOptions
26
43
  } from 'react-aria-components';
27
-
44
+ import {centerPadding, getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'};
28
45
  import {Checkbox} from './Checkbox';
46
+ import Chevron from '../ui-icons/Chevron';
47
+ import {ColumnSize} from '@react-types/table';
48
+ import {DOMRef, LoadingState, Node} from '@react-types/shared';
49
+ import {fontRelative, lightDark, size, style} from '../style/spectrum-theme' with {type: 'macro'};
50
+ import {GridNode} from '@react-types/grid';
51
+ import {IconContext} from './Icon';
52
+ // @ts-ignore
53
+ import intlMessages from '../intl/*.json';
54
+ import {LayoutNode} from '@react-stately/layout';
55
+ import {Menu, MenuItem, MenuTrigger} from './Menu';
56
+ import {mergeStyles} from '../style/runtime';
57
+ import Nubbin from '../ui-icons/S2_Icon_MoveHorizontalCircleTableWidget_16_N.svg';
58
+ import {ProgressCircle} from './ProgressCircle';
59
+ import {raw} from '../style/style-macro' with {type: 'macro'};
60
+ import React, {createContext, forwardRef, ReactNode, useCallback, useContext, useMemo, useRef, useState} from 'react';
61
+ import {Rect} from '@react-stately/virtualizer';
62
+ import SortDownArrow from '../s2wf-icons/S2_Icon_SortDown_20_N.svg';
63
+ import SortUpArrow from '../s2wf-icons/S2_Icon_SortUp_20_N.svg';
64
+ import {useDOMRef} from '@react-spectrum/utils';
65
+ import {useLoadMore} from '@react-aria/utils';
66
+ import {useLocalizedStringFormatter} from '@react-aria/i18n';
67
+ import {useScale} from './utils';
68
+ import {VisuallyHidden} from 'react-aria';
29
69
 
70
+ interface S2TableProps {
71
+ /** Whether the Table should be displayed with a quiet style. */
72
+ isQuiet?: boolean,
73
+ /**
74
+ * Sets the amount of vertical padding within each cell.
75
+ * @default 'regular'
76
+ */
77
+ density?: 'compact' | 'spacious' | 'regular',
78
+ /**
79
+ * Sets the overflow behavior for the cell contents.
80
+ * @default 'truncate'
81
+ */
82
+ overflowMode?: 'wrap' | 'truncate',
83
+ // TODO: will we contine with onAction or rename to onRowAction like it is in RAC?
84
+ /** Handler that is called when a user performs an action on a row. */
85
+ onAction?: (key: Key) => void,
86
+ /**
87
+ * Handler that is called when a user starts a column resize.
88
+ */
89
+ onResizeStart?: (widths: Map<Key, ColumnSize>) => void,
90
+ /**
91
+ * Handler that is called when a user performs a column resize.
92
+ * Can be used with the width property on columns to put the column widths into
93
+ * a controlled state.
94
+ */
95
+ onResize?: (widths: Map<Key, ColumnSize>) => void,
96
+ /**
97
+ * Handler that is called after a user performs a column resize.
98
+ * Can be used to store the widths of columns for another future session.
99
+ */
100
+ onResizeEnd?: (widths: Map<Key, ColumnSize>) => void,
101
+ /** The current loading state of the table. */
102
+ loadingState?: LoadingState,
103
+ /** Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. */
104
+ onLoadMore?: () => any
105
+ }
30
106
 
31
- export function Table(props: TableProps) {
32
- return <AriaTable {...props} />;
107
+ // TODO: Note that loadMore and loadingState are now on the Table instead of on the TableBody
108
+ export interface TableProps extends Omit<RACTableProps, 'style' | 'disabledBehavior' | 'className' | 'onRowAction' | 'selectionBehavior' | 'onScroll' | 'onCellAction' | 'dragAndDropHooks'>, UnsafeStyles, S2TableProps {
109
+ /** Spectrum-defined styles, returned by the `style()` macro. */
110
+ styles?: StylesPropWithHeight
33
111
  }
34
112
 
113
+ let InternalTableContext = createContext<TableProps & {layout?: S2TableLayout<unknown>, setIsInResizeMode?:(val: boolean) => void, isInResizeMode?: boolean}>({});
114
+
115
+ const tableWrapper = style({
116
+ minHeight: 0,
117
+ minWidth: 0,
118
+ display: 'flex',
119
+ isolation: 'isolate',
120
+ disableTapHighlight: true
121
+ });
122
+
123
+ const table = style<TableRenderProps & S2TableProps & {isCheckboxSelection?: boolean}>({
124
+ width: 'full',
125
+ userSelect: 'none',
126
+ minHeight: 0,
127
+ minWidth: 0,
128
+ fontFamily: 'sans',
129
+ fontWeight: 'normal',
130
+ overflow: 'auto',
131
+ backgroundColor: {
132
+ default: 'gray-25',
133
+ isQuiet: 'transparent',
134
+ forcedColors: 'Background'
135
+ },
136
+ outlineColor: {
137
+ default: 'gray-300',
138
+ isFocusVisible: 'focus-ring',
139
+ forcedColors: 'ButtonBorder'
140
+ },
141
+ outlineWidth: {
142
+ default: 1,
143
+ isQuiet: 0,
144
+ isFocusVisible: 2
145
+ },
146
+ outlineStyle: 'solid',
147
+ borderRadius: {
148
+ default: size(6),
149
+ isQuiet: 'none'
150
+ },
151
+ // Multiple browser bugs from scrollIntoView and scrollPadding:
152
+ // Bug: Table doesn't scroll items into view perfectly in Chrome
153
+ // https://issues.chromium.org/issues/365913982
154
+ // Bug: Table scrolls to the left when navigating up/down through the checkboxes when body is scrolled to the right.
155
+ // https://issues.chromium.org/issues/40067778
156
+ // https://bugs.webkit.org/show_bug.cgi?id=272799
157
+ // Base reproduction: https://codepen.io/lfdanlu/pen/zYVVGPW
158
+ scrollPaddingTop: 32,
159
+ scrollPaddingStart: {
160
+ isCheckboxSelection: 40
161
+ }
162
+ }, getAllowedOverrides({height: true}));
163
+
164
+ // component-height-100
165
+ const DEFAULT_HEADER_HEIGHT = {
166
+ medium: 32,
167
+ large: 40
168
+ };
169
+
170
+ const ROW_HEIGHTS = {
171
+ compact: {
172
+ medium: 32, // table-row-height-medium-compact (aka component-height-100)
173
+ large: 40
174
+ },
175
+ regular: {
176
+ medium: 40, // table-row-height-medium-regular
177
+ large: 50
178
+ },
179
+ spacious: {
180
+ medium: 48, // table-row-height-medium-spacious
181
+ large: 60
182
+ }
183
+ };
184
+
185
+ export class S2TableLayout<T> extends UNSTABLE_TableLayout<T> {
186
+ constructor(options) {
187
+ super({...options, loaderHeight: 60});
188
+ }
189
+
190
+ protected isStickyColumn(node: GridNode<T>): boolean {
191
+ return node.props.isSticky;
192
+ }
193
+
194
+ protected buildCollection(): LayoutNode[] {
195
+ let [header, body] = super.buildCollection();
196
+ let {children, layoutInfo} = body;
197
+ // TableLayout's buildCollection always sets the body width to the max width between the header width, but
198
+ // we want the body to be sticky and only as wide as the table so it is always in view if loading/empty
199
+ if (children?.length === 0) {
200
+ layoutInfo.rect.width = this.virtualizer.visibleRect.width - 80;
201
+ }
202
+
203
+ return [
204
+ header,
205
+ body
206
+ ];
207
+ }
208
+
209
+ protected buildLoader(node: Node<T>, x: number, y: number): LayoutNode {
210
+ let layoutNode = super.buildLoader(node, x, y);
211
+ let {layoutInfo} = layoutNode;
212
+ layoutInfo.allowOverflow = true;
213
+ layoutInfo.rect.width = this.virtualizer.visibleRect.width;
214
+ layoutInfo.isSticky = true;
215
+ return layoutNode;
216
+ }
217
+
218
+ protected buildBody(y: number): LayoutNode {
219
+ let layoutNode = super.buildBody(y);
220
+ let {children, layoutInfo} = layoutNode;
221
+ // Needs overflow for sticky loader
222
+ layoutInfo.allowOverflow = true;
223
+ // If loading or empty, we'll want the body to be sticky and centered
224
+ if (children?.length === 0) {
225
+ layoutInfo.rect = new Rect(40, 40, this.virtualizer.visibleRect.width - 80, this.virtualizer.visibleRect.height - 80);
226
+ layoutInfo.isSticky = true;
227
+ }
228
+
229
+ return {...layoutNode, layoutInfo};
230
+ }
231
+
232
+ protected buildRow(node: GridNode<T>, x: number, y: number): LayoutNode {
233
+ let layoutNode = super.buildRow(node, x, y);
234
+ layoutNode.layoutInfo.allowOverflow = true;
235
+ // Needs overflow for sticky selection/drag cells
236
+ return layoutNode;
237
+ }
238
+
239
+ protected buildTableHeader(): LayoutNode {
240
+ let layoutNode = super.buildTableHeader();
241
+ // Needs overflow for sticky selection/drag column
242
+ layoutNode.layoutInfo.allowOverflow = true;
243
+ return layoutNode;
244
+ }
245
+
246
+ protected buildColumn(node: GridNode<T>, x: number, y: number): LayoutNode {
247
+ let layoutNode = super.buildColumn(node, x, y);
248
+ // Needs overflow for the resize handle
249
+ layoutNode.layoutInfo.allowOverflow = true;
250
+ return layoutNode;
251
+ }
252
+ }
253
+
254
+ function Table(props: TableProps, ref: DOMRef<HTMLDivElement>) {
255
+ let {
256
+ UNSAFE_style,
257
+ UNSAFE_className,
258
+ isQuiet = false,
259
+ density = 'regular',
260
+ overflowMode = 'truncate',
261
+ styles,
262
+ loadingState,
263
+ onLoadMore,
264
+ onResize: propsOnResize,
265
+ onResizeStart: propsOnResizeStart,
266
+ onResizeEnd: propsOnResizeEnd,
267
+ onAction,
268
+ ...otherProps
269
+ } = props;
270
+
271
+ let domRef = useDOMRef(ref);
272
+ let scale = useScale();
273
+ let layout = useMemo(() => {
274
+ return new S2TableLayout({
275
+ rowHeight: overflowMode === 'wrap'
276
+ ? undefined
277
+ : ROW_HEIGHTS[density][scale],
278
+ estimatedRowHeight: overflowMode === 'wrap'
279
+ ? ROW_HEIGHTS[density][scale]
280
+ : undefined,
281
+ // No need for estimated headingHeight since the headers aren't affected by overflow mode: wrap
282
+ headingHeight: DEFAULT_HEADER_HEIGHT[scale]
283
+ });
284
+ }, [overflowMode, density, scale]);
285
+
286
+ // Starts when the user selects resize from the menu, ends when resizing ends
287
+ // used to control the visibility of the resizer Nubbin
288
+ let [isInResizeMode, setIsInResizeMode] = useState(false);
289
+ let onResizeStart = useCallback((widths) => {
290
+ propsOnResizeStart?.(widths);
291
+ }, [propsOnResizeStart]);
292
+ let onResizeEnd = useCallback((widths) => {
293
+ setIsInResizeMode(false);
294
+ propsOnResizeEnd?.(widths);
295
+ }, [propsOnResizeEnd, setIsInResizeMode]);
296
+
297
+ let context = useMemo(() => ({
298
+ isQuiet,
299
+ density,
300
+ overflowMode,
301
+ loadingState,
302
+ isInResizeMode,
303
+ setIsInResizeMode
304
+ }), [isQuiet, density, overflowMode, loadingState, isInResizeMode, setIsInResizeMode]);
305
+
306
+ let isLoading = loadingState === 'loading' || loadingState === 'loadingMore';
307
+ let scrollRef = useRef(null);
308
+ let memoedLoadMoreProps = useMemo(() => ({
309
+ isLoading: isLoading,
310
+ onLoadMore
311
+ }), [isLoading, onLoadMore]);
312
+ useLoadMore(memoedLoadMoreProps, scrollRef);
313
+ let isCheckboxSelection = props.selectionMode === 'multiple' || props.selectionMode === 'single';
314
+
315
+ return (
316
+ <ResizableTableContainer
317
+ // TODO: perhaps this ref should be attached to the RACTable but it expects a table type ref which isn't true in the virtualized case
318
+ ref={domRef}
319
+ onResize={propsOnResize}
320
+ onResizeEnd={onResizeEnd}
321
+ onResizeStart={onResizeStart}
322
+ className={(UNSAFE_className || '') + mergeStyles(tableWrapper, styles)}
323
+ style={UNSAFE_style}>
324
+ <UNSTABLE_Virtualizer layout={layout}>
325
+ <InternalTableContext.Provider value={context}>
326
+ <RACTable
327
+ ref={scrollRef}
328
+ // Fix webkit bug where scrollbars appear above the checkboxes/other table elements
329
+ style={{WebkitTransform: 'translateZ(0)'}}
330
+ className={renderProps => table({
331
+ ...renderProps,
332
+ isCheckboxSelection,
333
+ isQuiet
334
+ })}
335
+ selectionBehavior="toggle"
336
+ onRowAction={onAction}
337
+ {...otherProps} />
338
+ </InternalTableContext.Provider>
339
+ </UNSTABLE_Virtualizer>
340
+ </ResizableTableContainer>
341
+ );
342
+ }
343
+
344
+ const centeredWrapper = style({
345
+ display: 'flex',
346
+ alignItems: 'center',
347
+ justifyContent: 'center',
348
+ width: 'full',
349
+ height: 'full'
350
+ });
351
+
352
+ export interface TableBodyProps<T> extends Omit<RACTableBodyProps<T>, 'style' | 'className' | 'dependencies'> {}
353
+
354
+ /**
355
+ * The body of a `<Table>`, containing the table rows.
356
+ */
357
+ export function TableBody<T extends object>(props: TableBodyProps<T>) {
358
+ let {items, renderEmptyState, children} = props;
359
+ let {loadingState} = useContext(InternalTableContext);
360
+ let emptyRender;
361
+ let renderer = children;
362
+ let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2');
363
+ let loadMoreSpinner = (
364
+ <UNSTABLE_TableLoadingIndicator className={style({height: 'full', width: 'full'})}>
365
+ <div className={centeredWrapper}>
366
+ <ProgressCircle
367
+ isIndeterminate
368
+ aria-label={stringFormatter.format('table.loadingMore')} />
369
+ </div>
370
+ </UNSTABLE_TableLoadingIndicator>
371
+ );
372
+
373
+ // If the user is rendering their rows in dynamic fashion, wrap their render function in Collection so we can inject
374
+ // the loader. Otherwise it is a static renderer and thus we can simply add the table loader after
375
+ // TODO: this assumes that the user isn't providing their children in some wrapper though and/or isn't doing a map of children
376
+ // (though I guess they wouldn't provide items then so the check for this is still valid in the latter case)...
377
+ if (typeof children === 'function' && items) {
378
+ renderer = (
379
+ <>
380
+ <Collection items={items}>
381
+ {children}
382
+ </Collection>
383
+ {loadingState === 'loadingMore' && loadMoreSpinner}
384
+ </>
385
+ );
386
+ } else {
387
+ renderer = (
388
+ <>
389
+ {children}
390
+ {loadingState === 'loadingMore' && loadMoreSpinner}
391
+ </>
392
+ );
393
+ }
394
+
395
+ if (renderEmptyState != null && loadingState !== 'loading') {
396
+ emptyRender = (props: TableBodyRenderProps) => (
397
+ <div className={centeredWrapper}>
398
+ {renderEmptyState(props)}
399
+ </div>
400
+ );
401
+ } else if (loadingState === 'loading') {
402
+ emptyRender = () => (
403
+ <div className={centeredWrapper}>
404
+ <ProgressCircle
405
+ isIndeterminate
406
+ aria-label={stringFormatter.format('table.loading')} />
407
+ </div>
408
+ );
409
+ }
410
+
411
+ return (
412
+ <RACTableBody
413
+ className={style({height: 'full'})}
414
+ {...props}
415
+ renderEmptyState={emptyRender}
416
+ dependencies={[loadingState]}>
417
+ {renderer}
418
+ </RACTableBody>
419
+ );
420
+ }
421
+
422
+ const cellFocus = {
423
+ outlineStyle: {
424
+ default: 'none',
425
+ isFocusVisible: 'solid'
426
+ },
427
+ outlineOffset: -2,
428
+ outlineWidth: 2,
429
+ outlineColor: 'focus-ring',
430
+ borderRadius: size(6)
431
+ } as const;
432
+
433
+ function CellFocusRing(props: {isFocusVisible: boolean}) {
434
+ let {isFocusVisible} = props;
435
+ return <div role="presentation" className={style({...cellFocus, position: 'absolute', inset: 0})({isFocusVisible})} />;
436
+ }
437
+
438
+ const columnStyles = style({
439
+ height: '[inherit]',
440
+ boxSizing: 'border-box',
441
+ color: {
442
+ default: 'neutral',
443
+ forcedColors: 'ButtonText'
444
+ },
445
+ paddingX: {
446
+ default: 16,
447
+ isColumnResizable: 0
448
+ },
449
+ textAlign: {
450
+ align: {
451
+ start: 'start',
452
+ center: 'center',
453
+ end: 'end'
454
+ }
455
+ },
456
+ outlineStyle: 'none',
457
+ position: 'relative',
458
+ fontSize: 'control',
459
+ fontFamily: 'sans',
460
+ fontWeight: 'bold',
461
+ display: 'flex',
462
+ borderColor: {
463
+ default: 'gray-300',
464
+ forcedColors: 'ButtonBorder'
465
+ },
466
+ borderTopWidth: 0,
467
+ borderBottomWidth: 1,
468
+ borderStartWidth: 0,
469
+ borderEndWidth: {
470
+ default: 0,
471
+ isColumnResizable: 1
472
+ },
473
+ borderStyle: 'solid',
474
+ forcedColorAdjust: 'none'
475
+ });
476
+
477
+ export interface ColumnProps extends RACColumnProps {
478
+ /** Whether the column should render a divider between it and the next column. */
479
+ showDivider?: boolean,
480
+ /** Whether the column allows resizing. */
481
+ allowsResizing?: boolean,
482
+ /**
483
+ * The alignment of the column's contents relative to its allotted width.
484
+ * @default 'start'
485
+ */
486
+ align?: 'start' | 'center' | 'end',
487
+ /** The content to render as the column header. */
488
+ children: ReactNode
489
+ }
490
+
491
+ /**
492
+ * A column within a `<Table>`.
493
+ */
35
494
  export function Column(props: ColumnProps) {
495
+ let {isHeaderRowHovered} = useContext(InternalTableHeaderContext);
496
+ let {allowsResizing, children, align = 'start'} = props;
497
+ let isColumnResizable = allowsResizing;
498
+
36
499
  return (
37
- <AriaColumn {...props}>
38
- {({allowsSorting, sortDirection}) => (
500
+ <RACColumn {...props} style={{borderInlineEndColor: 'transparent'}} className={renderProps => columnStyles({...renderProps, isColumnResizable, align})}>
501
+ {({allowsSorting, sortDirection, isFocusVisible, sort, startResize, isHovered}) => (
39
502
  <>
40
- {props.children}
41
- {allowsSorting && (
42
- <span aria-hidden="true" className="sort-indicator">
43
- {sortDirection === 'ascending' ? '▲' : '▼'}
44
- </span>
45
- )}
503
+ {/* Note this is mainly for column's without a dropdown menu. If there is a dropdown menu, the button is styled to have a focus ring for simplicity
504
+ (no need to juggle showing this focus ring if focus is on the menu button and not if it is on the resizer) */}
505
+ {/* Separate absolutely positioned element because appyling the ring on the column directly via outline means the ring's required borderRadius will cause the bottom gray border to curve as well */}
506
+ <CellFocusRing isFocusVisible={isFocusVisible} />
507
+ {isColumnResizable ?
508
+ (
509
+ <ResizableColumnContents allowsSorting={allowsSorting} sortDirection={sortDirection} sort={sort} startResize={startResize} isHovered={isHeaderRowHovered || isHovered} align={align}>
510
+ {children}
511
+ </ResizableColumnContents>
512
+ ) : (
513
+ <ColumnContents allowsSorting={allowsSorting} sortDirection={sortDirection}>
514
+ {children}
515
+ </ColumnContents>
516
+ )
517
+ }
46
518
  </>
47
519
  )}
48
- </AriaColumn>
520
+ </RACColumn>
49
521
  );
50
522
  }
51
523
 
52
- export function TableHeader<T extends object>(
53
- {columns, children}: TableHeaderProps<T>
54
- ) {
55
- let {selectionBehavior, selectionMode, allowsDragging} = useTableOptions();
524
+ const columnContentWrapper = style({
525
+ minWidth: 0,
526
+ display: 'flex',
527
+ alignItems: 'center',
528
+ width: 'full'
529
+ });
530
+
531
+ const sortIcon = style({
532
+ size: fontRelative(16),
533
+ flexShrink: 0,
534
+ marginEnd: {
535
+ default: 8,
536
+ isButton: 'text-to-visual'
537
+ },
538
+ verticalAlign: {
539
+ default: 'bottom',
540
+ isButton: 0
541
+ },
542
+ '--iconPrimary': {
543
+ type: 'fill',
544
+ value: 'currentColor'
545
+ }
546
+ });
547
+
548
+ interface ColumnContentProps extends Pick<ColumnRenderProps, 'allowsSorting' | 'sortDirection'>, Pick<ColumnProps, 'children'> {}
549
+
550
+ function ColumnContents(props: ColumnContentProps) {
551
+ let {allowsSorting, sortDirection, children} = props;
56
552
 
57
553
  return (
58
- <AriaTableHeader>
59
- {/* Add extra columns for drag and drop and selection. */}
60
- {allowsDragging && <AriaColumn />}
61
- {selectionBehavior === 'toggle' && (
62
- <AriaColumn>
63
- {selectionMode === 'multiple' && <Checkbox slot="selection" />}
64
- </AriaColumn>
554
+ <div className={columnContentWrapper}>
555
+ {allowsSorting && (
556
+ <Provider
557
+ values={[
558
+ [IconContext, {
559
+ styles: sortIcon({})
560
+ }]
561
+ ]}>
562
+ {sortDirection != null && (
563
+ sortDirection === 'ascending' ? <SortUpArrow /> : <SortDownArrow />
564
+ )}
565
+ </Provider>
65
566
  )}
66
- <Collection items={columns}>
567
+ <span className={style({truncate: true, width: 'full'})}>
67
568
  {children}
68
- </Collection>
69
- </AriaTableHeader>
569
+ </span>
570
+ </div>
571
+ );
572
+ }
573
+
574
+ const resizableMenuButtonWrapper = style({
575
+ ...cellFocus,
576
+ color: 'gray-800', // body-color
577
+ width: 'full',
578
+ position: 'relative',
579
+ display: 'flex',
580
+ alignItems: 'center',
581
+ justifyContent: {
582
+ align: {
583
+ default: 'start',
584
+ center: 'center',
585
+ end: 'end'
586
+ }
587
+ },
588
+ // TODO: when align: end, the dropdown arrow is misaligned with the text, not sure how best to make the svg be flush with the end of the button other than modifying the
589
+ // paddingEnd
590
+ paddingX: 16,
591
+ backgroundColor: 'transparent',
592
+ borderStyle: 'none',
593
+ fontSize: 'control',
594
+ fontFamily: 'sans',
595
+ fontWeight: 'bold'
596
+ });
597
+
598
+ const resizerHandleContainer = style({
599
+ display: {
600
+ default: 'none',
601
+ isResizing: 'block',
602
+ isHovered: 'block'
603
+ },
604
+ width: 12,
605
+ height: 'full',
606
+ position: 'absolute',
607
+ top: 0,
608
+ insetEnd: size(-6),
609
+ cursor: {
610
+ default: 'none',
611
+ resizableDirection: {
612
+ 'left': 'e-resize',
613
+ 'right': 'w-resize',
614
+ 'both': 'ew-resize'
615
+ }
616
+ },
617
+ // So that the user can still hover + drag the resizer even though it's hit area is partially in the adjacent column's space
618
+ '--focus-ring-color': {
619
+ type: 'outlineColor',
620
+ value: 'focus-ring'
621
+ }
622
+ });
623
+
624
+ const resizerHandle = style({
625
+ backgroundColor: {
626
+ default: 'transparent',
627
+ isHovered: 'gray-300',
628
+ isFocusVisible: '--focus-ring-color',
629
+ isResizing: '--focus-ring-color',
630
+ forcedColors: {
631
+ default: 'Background',
632
+ isHovered: 'ButtonBorder',
633
+ isFocusVisible: 'Highlight',
634
+ isResizing: 'Highlight'
635
+ }
636
+ },
637
+ height: {
638
+ default: 'full',
639
+ isResizing: 'screen'
640
+ },
641
+ width: {
642
+ default: size(1),
643
+ isResizing: size(2)
644
+ },
645
+ position: 'absolute',
646
+ insetStart: size(6)
647
+ });
648
+
649
+ const columnHeaderText = style({
650
+ truncate: true,
651
+ // Make it so the text doesn't completely disappear when column is resized to smallest width + both sort and chevron icon is rendered
652
+ minWidth: fontRelative(16),
653
+ flexGrow: 0,
654
+ flexShrink: 1,
655
+ flexBasis: 'auto'
656
+ });
657
+
658
+ const chevronIcon = style({
659
+ rotate: 90,
660
+ marginStart: 'text-to-visual',
661
+ minWidth: fontRelative(16),
662
+ flexShrink: 0,
663
+ '--iconPrimary': {
664
+ type: 'fill',
665
+ value: 'currentColor'
666
+ }
667
+ });
668
+
669
+ const nubbin = style({
670
+ position: 'absolute',
671
+ top: 0,
672
+ insetStart: size(-1),
673
+ size: fontRelative(16),
674
+ fill: {
675
+ default: '--focus-ring-color',
676
+ forcedColors: 'Highlight'
677
+ },
678
+ '--iconPrimary': {
679
+ type: 'fill',
680
+ value: {
681
+ default: 'white',
682
+ forcedColors: 'HighlightText'
683
+ }
684
+ }
685
+ });
686
+
687
+ interface ResizableColumnContentProps extends Pick<ColumnRenderProps, 'allowsSorting' | 'sort' | 'sortDirection' | 'startResize' | 'isHovered'>, Pick<ColumnProps, 'align' | 'children'> {}
688
+
689
+ function ResizableColumnContents(props: ResizableColumnContentProps) {
690
+ let {allowsSorting, sortDirection, sort, startResize, children, isHovered, align} = props;
691
+ let {setIsInResizeMode, isInResizeMode} = useContext(InternalTableContext);
692
+ let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2');
693
+ const onMenuSelect = (key) => {
694
+ switch (key) {
695
+ case 'sort-asc':
696
+ sort('ascending');
697
+ break;
698
+ case 'sort-desc':
699
+ sort('descending');
700
+ break;
701
+ case 'resize':
702
+ setIsInResizeMode?.(true);
703
+ startResize();
704
+ break;
705
+ }
706
+ };
707
+
708
+ let items = useMemo(() => {
709
+ let options = [
710
+ {
711
+ label: stringFormatter.format('table.resizeColumn'),
712
+ id: 'resize'
713
+ }
714
+ ];
715
+ if (allowsSorting) {
716
+ options = [
717
+ {
718
+ label: stringFormatter.format('table.sortAscending'),
719
+ id: 'sort-asc'
720
+ },
721
+ {
722
+ label: stringFormatter.format('table.sortDescending'),
723
+ id: 'sort-desc'
724
+ },
725
+ ...options
726
+ ];
727
+ }
728
+ return options;
729
+ // eslint-disable-next-line react-hooks/exhaustive-deps
730
+ }, [allowsSorting]);
731
+
732
+ let buttonAlignment = 'start';
733
+ let menuAlign = 'start' as 'start' | 'end';
734
+ if (align === 'center') {
735
+ buttonAlignment = 'center';
736
+ } else if (align === 'end') {
737
+ buttonAlignment = 'end';
738
+ menuAlign = 'end';
739
+ }
740
+
741
+ return (
742
+ <>
743
+ <MenuTrigger align={menuAlign}>
744
+ <Button className={(renderProps) => resizableMenuButtonWrapper({...renderProps, align: buttonAlignment})}>
745
+ {allowsSorting && (
746
+ <Provider
747
+ values={[
748
+ [IconContext, {
749
+ styles: sortIcon({isButton: true})
750
+ }]
751
+ ]}>
752
+ {sortDirection != null && (
753
+ sortDirection === 'ascending' ? <SortUpArrow /> : <SortDownArrow />
754
+ )}
755
+ </Provider>
756
+ )}
757
+ <div className={columnHeaderText}>
758
+ {children}
759
+ </div>
760
+ <Chevron size="M" className={chevronIcon} />
761
+ </Button>
762
+ <Menu onAction={onMenuSelect} items={items} styles={style({minWidth: 128})}>
763
+ {(item) => <MenuItem>{item?.label}</MenuItem>}
764
+ </Menu>
765
+ </MenuTrigger>
766
+ <div data-react-aria-prevent-focus="true">
767
+ <ColumnResizer data-react-aria-prevent-focus="true" className={({resizableDirection, isResizing}) => resizerHandleContainer({resizableDirection, isResizing, isHovered: isInResizeMode || isHovered})}>
768
+ {({isFocusVisible, isResizing}) => (
769
+ <>
770
+ <ResizerIndicator isInResizeMode={isInResizeMode} isFocusVisible={isFocusVisible} isHovered={isHovered} isResizing={isResizing} />
771
+ {(isFocusVisible || isInResizeMode) && isResizing && <div className={nubbin}><Nubbin /></div>}
772
+ </>
773
+ )}
774
+ </ColumnResizer>
775
+ </div>
776
+ </>
70
777
  );
71
778
  }
72
779
 
73
- export function Row<T extends object>(
74
- {id, columns, children, ...otherProps}: RowProps<T>
75
- ) {
76
- let {selectionBehavior, allowsDragging} = useTableOptions();
780
+ function ResizerIndicator({isFocusVisible, isHovered, isResizing, isInResizeMode}) {
781
+ return (
782
+ <div className={resizerHandle({isFocusVisible, isHovered: isHovered || isInResizeMode, isResizing})} />
783
+ );
784
+ }
785
+
786
+ const tableHeader = style({
787
+ height: 'full',
788
+ width: 'full',
789
+ backgroundColor: 'gray-75',
790
+ // Attempt to prevent 1px area where you can see scrolled cell content between the table outline and the table header
791
+ marginTop: '[-1px]'
792
+ });
793
+
794
+ const selectAllCheckbox = style({
795
+ marginStart: 16 // table-edge-to-content, same between mobile and desktop
796
+ });
797
+
798
+ const selectAllCheckboxColumn = style({
799
+ padding: 0,
800
+ height: '[calc(100% - 1px)]',
801
+ outlineStyle: 'none',
802
+ position: 'relative',
803
+ alignContent: 'center',
804
+ borderColor: {
805
+ default: 'gray-300',
806
+ forcedColors: 'ButtonBorder'
807
+ },
808
+ borderXWidth: 0,
809
+ borderTopWidth: 0,
810
+ borderBottomWidth: 1,
811
+ borderStyle: 'solid',
812
+ backgroundColor: 'gray-75'
813
+ });
814
+
815
+ let InternalTableHeaderContext = createContext<{isHeaderRowHovered?: boolean}>({isHeaderRowHovered: false});
816
+
817
+ export interface TableHeaderProps<T> extends Omit<RACTableHeaderProps<T>, 'style' | 'className' | 'dependencies'> {}
818
+
819
+ /**
820
+ * A header within a `<Table>`, containing the table columns.
821
+ */
822
+ export function TableHeader<T extends object>({columns, children}: TableHeaderProps<T>) {
823
+ let scale = useScale();
824
+ let {selectionBehavior, selectionMode} = useTableOptions();
825
+ let [isHeaderRowHovered, setHeaderRowHovered] = useState(false);
77
826
 
78
827
  return (
79
- <AriaRow id={id} {...otherProps}>
80
- {allowsDragging && (
81
- <Cell>
82
- <Button slot="drag">≡</Button>
83
- </Cell>
828
+ <InternalTableHeaderContext.Provider value={{isHeaderRowHovered}}>
829
+ <RACTableHeader onHoverChange={setHeaderRowHovered} className={tableHeader}>
830
+ {/* Add extra columns for selection. */}
831
+ {selectionBehavior === 'toggle' && (
832
+ // Also isSticky prop is applied just for the layout, will decide what the RAC api should be later
833
+ // @ts-ignore
834
+ <RACColumn isSticky width={scale === 'medium' ? 40 : 52} minWidth={scale === 'medium' ? 40 : 52} className={selectAllCheckboxColumn}>
835
+ {({isFocusVisible}) => (
836
+ <>
837
+ {selectionMode === 'single' &&
838
+ <>
839
+ <CellFocusRing isFocusVisible={isFocusVisible} />
840
+ <VisuallyHiddenSelectAllLabel />
841
+ </>
842
+ }
843
+ {selectionMode === 'multiple' &&
844
+ <Checkbox isEmphasized styles={selectAllCheckbox} slot="selection" />
845
+ }
846
+ </>
847
+ )}
848
+ </RACColumn>
849
+ )}
850
+ <Collection items={columns}>
851
+ {children}
852
+ </Collection>
853
+ </RACTableHeader>
854
+ </InternalTableHeaderContext.Provider>
855
+ );
856
+ }
857
+
858
+ function VisuallyHiddenSelectAllLabel() {
859
+ let checkboxProps = useSlottedContext(RACCheckboxContext, 'selection');
860
+
861
+ return (
862
+ <VisuallyHidden>{checkboxProps?.['aria-label']}</VisuallyHidden>
863
+ );
864
+ }
865
+
866
+ const commonCellStyles = {
867
+ borderColor: 'transparent',
868
+ borderBottomWidth: 1,
869
+ borderTopWidth: 0,
870
+ borderXWidth: 0,
871
+ borderStyle: 'solid',
872
+ position: 'relative',
873
+ color: {
874
+ default: 'gray-800',
875
+ forcedColors: 'ButtonText'
876
+ },
877
+ outlineStyle: 'none',
878
+ paddingX: 16 // table-edge-to-content
879
+ } as const;
880
+
881
+ const cell = style<CellRenderProps & S2TableProps & {isDivider: boolean}>({
882
+ ...commonCellStyles,
883
+ color: 'neutral',
884
+ paddingY: centerPadding(),
885
+ minHeight: {
886
+ default: 40,
887
+ density: {
888
+ compact: 32,
889
+ spacious: 48
890
+ }
891
+ },
892
+ boxSizing: 'border-box',
893
+ height: 'full',
894
+ width: 'full',
895
+ fontSize: 'control',
896
+ alignItems: 'center',
897
+ display: 'flex',
898
+ borderStyle: {
899
+ default: 'none',
900
+ isDivider: 'solid'
901
+ },
902
+ borderEndWidth: {
903
+ default: 0,
904
+ isDivider: 1
905
+ },
906
+ borderColor: {
907
+ default: 'gray-300',
908
+ forcedColors: 'ButtonBorder'
909
+ }
910
+ });
911
+
912
+ const stickyCell = {
913
+ backgroundColor: 'gray-25'
914
+ } as const;
915
+
916
+ const checkboxCellStyle = style({
917
+ ...commonCellStyles,
918
+ ...stickyCell,
919
+ paddingStart: 16,
920
+ alignContent: 'center',
921
+ height: '[calc(100% - 1px)]',
922
+ borderBottomWidth: 0
923
+ // TODO: problem with having the checkbox cell itself use the row background color directly instead
924
+ // of having a separate white rectangle div base below a div with the row background color set above it as a mask
925
+ // is that it doesn't come out as the same color as the other cells because the base below the sticky cell will be the blue of the
926
+ // other cells, not the same white base. If I could convert informative-900/10 (and the rest of the rowBackgroundColors) to an equivalent without any opacity
927
+ // then this would be possible. Currently waiting request for Spectrum to provide tokens for these equivalent values
928
+ // backgroundColor: '--rowBackgroundColor'
929
+ });
930
+
931
+ const cellContent = style({
932
+ truncate: true,
933
+ whiteSpace: {
934
+ default: 'nowrap',
935
+ overflowMode: {
936
+ wrap: 'normal'
937
+ }
938
+ },
939
+ textAlign: {
940
+ align: {
941
+ start: 'start',
942
+ center: 'center',
943
+ end: 'end'
944
+ }
945
+ },
946
+ width: 'full',
947
+ isolation: 'isolate',
948
+ padding: {
949
+ default: 4,
950
+ isSticky: 0
951
+ },
952
+ margin: {
953
+ default: -4,
954
+ isSticky: 0
955
+ }
956
+ });
957
+
958
+ const cellBackground = style({
959
+ position: 'absolute',
960
+ top: 0,
961
+ bottom: 0,
962
+ right: 0,
963
+ left: 0,
964
+ backgroundColor: {
965
+ default: 'transparent',
966
+ isSticky: '--rowBackgroundColor'
967
+ }
968
+ });
969
+
970
+ export interface CellProps extends RACCellProps, Pick<ColumnProps, 'align' | 'showDivider'> {
971
+ /** @private */
972
+ isSticky?: boolean,
973
+ /** The content to render as the cell children. */
974
+ children: ReactNode
975
+ }
976
+
977
+ /**
978
+ * A cell within a table row.
979
+ */
980
+ export function Cell(props: CellProps) {
981
+ let {children, isSticky, showDivider = false, align, textValue, ...otherProps} = props;
982
+ let tableVisualOptions = useContext(InternalTableContext);
983
+ textValue ||= typeof children === 'string' ? children : undefined;
984
+
985
+ return (
986
+ <RACCell
987
+ // Also isSticky prop is applied just for the layout, will decide what the RAC api should be later
988
+ // @ts-ignore
989
+ isSticky={isSticky}
990
+ className={renderProps => cell({
991
+ ...renderProps,
992
+ ...tableVisualOptions,
993
+ isDivider: showDivider
994
+ })}
995
+ textValue={textValue}
996
+ {...otherProps}>
997
+ {({isFocusVisible}) => (
998
+ <>
999
+ {/*
1000
+ // TODO: problem with having the checkbox cell itself use the row background color directly instead
1001
+ of having a separate white rectangle div base below a div with the row background color set above it as a mask
1002
+ is that it doesn't come out as the same color as the other cells because the base below the sticky cell when other selected cells are scrolled below it will be the blue of the
1003
+ other cells, not the same white base. If I could convert informative-900/10 (and the rest of the rowBackgroundColors) to an equivalent without any opacity
1004
+ then I could do away with this styling. To reproduce this, comment out the stickyCell gray-25, get rid of the below div and apply backgroundColor: '--rowBackgroundColor' to checkboxCellStyle.
1005
+ Having the CellFocusRing here instead of applying a outline on the cell directly also makes it NOT overlap with the border (can be remedied with a -3px outline offset) and applying a border radius to get the curved outline focus ring messes
1006
+ with the divider rendered on the cell since those are also borders
1007
+ */}
1008
+ <div role="presentation" className={cellBackground({isSticky})} />
1009
+ <CellFocusRing isFocusVisible={isFocusVisible} />
1010
+ <span className={cellContent({...tableVisualOptions, isSticky, align: align || 'start'})}>{children}</span>
1011
+ </>
84
1012
  )}
85
- {selectionBehavior === 'toggle' && (
86
- <Cell>
87
- <Checkbox slot="selection" />
1013
+ </RACCell>
1014
+ );
1015
+ }
1016
+
1017
+ const rowBackgroundColor = {
1018
+ default: 'gray-25',
1019
+ isFocusVisibleWithin: 'gray-900/7', // table-row-hover-color
1020
+ isHovered: 'gray-900/7', // table-row-hover-color
1021
+ isPressed: 'gray-900/10', // table-row-hover-color
1022
+ isSelected: {
1023
+ default: lightDark('informative-900/10', 'informative-700/10'), // table-selected-row-background-color, opacity /10
1024
+ isFocusVisibleWithin: lightDark('informative-900/15', 'informative-700/15'), // table-selected-row-background-color, opacity /15
1025
+ isHovered: lightDark('informative-900/15', 'informative-700/15'), // table-selected-row-background-color, opacity /15
1026
+ isPressed: lightDark('informative-900/15', 'informative-700/15') // table-selected-row-background-color, opacity /15
1027
+ },
1028
+ isQuiet: {
1029
+ default: 'transparent',
1030
+ isFocusVisibleWithin: 'gray-900/7', // table-row-hover-color
1031
+ isHovered: 'gray-900/7', // table-row-hover-color
1032
+ isPressed: 'gray-900/10', // table-row-hover-color
1033
+ isSelected: {
1034
+ default: lightDark('informative-900/10', 'informative-700/10'), // table-selected-row-background-color, opacity /10
1035
+ isFocusVisibleWithin: lightDark('informative-900/15', 'informative-700/15'), // table-selected-row-background-color, opacity /15
1036
+ isHovered: lightDark('informative-900/15', 'informative-700/15'), // table-selected-row-background-color, opacity /15
1037
+ isPressed: lightDark('informative-900/15', 'informative-700/15') // table-selected-row-background-color, opacity /15
1038
+ }
1039
+ },
1040
+ forcedColors: {
1041
+ default: 'Background'
1042
+ }
1043
+ } as const;
1044
+
1045
+ const row = style<RowRenderProps & S2TableProps>({
1046
+ height: 'full',
1047
+ position: 'relative',
1048
+ boxSizing: 'border-box',
1049
+ backgroundColor: rowBackgroundColor,
1050
+ '--rowBackgroundColor': {
1051
+ type: 'backgroundColor',
1052
+ value: rowBackgroundColor
1053
+ },
1054
+ '--rowFocusIndicatorColor': {
1055
+ type: 'outlineColor',
1056
+ value: {
1057
+ default: 'focus-ring',
1058
+ forcedColors: 'Highlight'
1059
+ }
1060
+ },
1061
+ // TODO: outline here is to emulate v3 forcedColors experience but runs into the same problem where the sticky column covers the outline
1062
+ // This doesn't quite work because it gets cut off by the checkbox cell background masking element, figure out another way. Could shrink the checkbox cell's content even more
1063
+ // and offset it by margin top but that messes up the checkbox centering a bit
1064
+ // outlineWidth: {
1065
+ // forcedColors: {
1066
+ // isFocusVisible: 2
1067
+ // }
1068
+ // },
1069
+ // outlineOffset: {
1070
+ // forcedColors: {
1071
+ // isFocusVisible: -1
1072
+ // }
1073
+ // },
1074
+ // outlineColor: {
1075
+ // forcedColors: {
1076
+ // isFocusVisible: 'ButtonBorder'
1077
+ // }
1078
+ // },
1079
+ // outlineStyle: {
1080
+ // default: 'none',
1081
+ // forcedColors: {
1082
+ // isFocusVisible: 'solid'
1083
+ // }
1084
+ // },
1085
+ outlineStyle: 'none',
1086
+ borderTopWidth: 0,
1087
+ borderBottomWidth: 1,
1088
+ borderStartWidth: 0,
1089
+ borderEndWidth: 0,
1090
+ borderStyle: 'solid',
1091
+ borderColor: {
1092
+ default: 'gray-300',
1093
+ forcedColors: 'ButtonBorder'
1094
+ },
1095
+ forcedColorAdjust: 'none'
1096
+ });
1097
+
1098
+ export interface RowProps<T> extends Pick<RACRowProps<T>, 'id' | 'columns' | 'children' | 'textValue'> {}
1099
+
1100
+ /**
1101
+ * A row within a `<Table>`.
1102
+ */
1103
+ export function Row<T extends object>({id, columns, children, ...otherProps}: RowProps<T>) {
1104
+ let {selectionBehavior, selectionMode} = useTableOptions();
1105
+ let tableVisualOptions = useContext(InternalTableContext);
1106
+
1107
+ return (
1108
+ <RACRow
1109
+ id={id}
1110
+ className={renderProps => row({
1111
+ ...renderProps,
1112
+ ...tableVisualOptions
1113
+ }) + (renderProps.isFocusVisible && ' ' + raw('&:before { content: ""; display: inline-block; position: sticky; inset-inline-start: 0; width: 3px; height: 100%; margin-inline-end: -3px; margin-block-end: 1px; z-index: 3; background-color: var(--rowFocusIndicatorColor)'))}
1114
+ {...otherProps}>
1115
+ {selectionMode !== 'none' && selectionBehavior === 'toggle' && (
1116
+ <Cell isSticky className={checkboxCellStyle}>
1117
+ <Checkbox isEmphasized slot="selection" />
88
1118
  </Cell>
89
1119
  )}
90
1120
  <Collection items={columns}>
91
1121
  {children}
92
1122
  </Collection>
93
- </AriaRow>
1123
+ </RACRow>
94
1124
  );
95
1125
  }
1126
+
1127
+ /**
1128
+ * Tables are containers for displaying information. They allow users to quickly scan, sort, compare, and take action on large amounts of data.
1129
+ */
1130
+ const _Table = forwardRef(Table);
1131
+ export {_Table as Table};