@react-spectrum/table 3.10.1-nightly.4028 → 3.10.1-nightly.4036
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.
- package/dist/import.mjs +282 -105
- package/dist/main.css +1 -1
- package/dist/main.js +279 -102
- package/dist/main.js.map +1 -1
- package/dist/module.js +282 -105
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +28 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +32 -31
- package/src/InsertionIndicator.tsx +1 -1
- package/src/Resizer.tsx +1 -1
- package/src/RootDropIndicator.tsx +1 -1
- package/src/TableView.tsx +12 -1418
- package/src/TableViewBase.tsx +1493 -0
- package/src/TableViewWrapper.tsx +101 -0
- package/src/TreeGridTableView.tsx +47 -0
- package/src/index.ts +2 -2
- package/src/table.css +7 -0
|
@@ -0,0 +1,1493 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 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 ArrowDownSmall from '@spectrum-icons/ui/ArrowDownSmall';
|
|
14
|
+
import {chain, isAndroid, mergeProps, scrollIntoView, scrollIntoViewport} from '@react-aria/utils';
|
|
15
|
+
import {Checkbox} from '@react-spectrum/checkbox';
|
|
16
|
+
import ChevronDownMedium from '@spectrum-icons/ui/ChevronDownMedium';
|
|
17
|
+
import ChevronLeftMedium from '@spectrum-icons/ui/ChevronLeftMedium';
|
|
18
|
+
import ChevronRightMedium from '@spectrum-icons/ui/ChevronRightMedium';
|
|
19
|
+
import {
|
|
20
|
+
classNames,
|
|
21
|
+
useDOMRef,
|
|
22
|
+
useFocusableRef,
|
|
23
|
+
useIsMobileDevice,
|
|
24
|
+
useStyleProps,
|
|
25
|
+
useUnwrapDOMRef
|
|
26
|
+
} from '@react-spectrum/utils';
|
|
27
|
+
import {ColumnSize, SpectrumColumnProps} from '@react-types/table';
|
|
28
|
+
import {DOMRef, DropTarget, FocusableElement, FocusableRef} from '@react-types/shared';
|
|
29
|
+
import type {DragAndDropHooks} from '@react-spectrum/dnd';
|
|
30
|
+
import type {DraggableCollectionState, DroppableCollectionState} from '@react-stately/dnd';
|
|
31
|
+
import type {DraggableItemResult, DropIndicatorAria, DroppableCollectionResult, DroppableItemResult} from '@react-aria/dnd';
|
|
32
|
+
import {FocusRing, FocusScope, useFocusRing} from '@react-aria/focus';
|
|
33
|
+
import {getInteractionModality, isFocusVisible, useHover, usePress} from '@react-aria/interactions';
|
|
34
|
+
import {GridNode} from '@react-types/grid';
|
|
35
|
+
import {InsertionIndicator} from './InsertionIndicator';
|
|
36
|
+
// @ts-ignore
|
|
37
|
+
import intlMessages from '../intl/*.json';
|
|
38
|
+
import {Item, Menu, MenuTrigger} from '@react-spectrum/menu';
|
|
39
|
+
import {layoutInfoToStyle, ScrollView, setScrollLeft, useVirtualizer, VirtualizerItem} from '@react-aria/virtualizer';
|
|
40
|
+
import ListGripper from '@spectrum-icons/ui/ListGripper';
|
|
41
|
+
import {Nubbin} from './Nubbin';
|
|
42
|
+
import {ProgressCircle} from '@react-spectrum/progress';
|
|
43
|
+
import React, {DOMAttributes, HTMLAttributes, Key, ReactElement, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
|
|
44
|
+
import {Resizer} from './Resizer';
|
|
45
|
+
import {ReusableView, useVirtualizerState} from '@react-stately/virtualizer';
|
|
46
|
+
import {RootDropIndicator} from './RootDropIndicator';
|
|
47
|
+
import {DragPreview as SpectrumDragPreview} from './DragPreview';
|
|
48
|
+
import {SpectrumTableProps} from './TableViewWrapper';
|
|
49
|
+
import styles from '@adobe/spectrum-css-temp/components/table/vars.css';
|
|
50
|
+
import stylesOverrides from './table.css';
|
|
51
|
+
import {TableColumnLayout, TableState, TreeGridState} from '@react-stately/table';
|
|
52
|
+
import {TableLayout} from '@react-stately/layout';
|
|
53
|
+
import {Tooltip, TooltipTrigger} from '@react-spectrum/tooltip';
|
|
54
|
+
import {useButton} from '@react-aria/button';
|
|
55
|
+
import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n';
|
|
56
|
+
import {useProvider, useProviderProps} from '@react-spectrum/provider';
|
|
57
|
+
import {
|
|
58
|
+
useTable,
|
|
59
|
+
useTableCell,
|
|
60
|
+
useTableColumnHeader,
|
|
61
|
+
useTableHeaderRow,
|
|
62
|
+
useTableRow,
|
|
63
|
+
useTableRowGroup,
|
|
64
|
+
useTableSelectAllCheckbox,
|
|
65
|
+
useTableSelectionCheckbox
|
|
66
|
+
} from '@react-aria/table';
|
|
67
|
+
import {useVisuallyHidden, VisuallyHidden} from '@react-aria/visually-hidden';
|
|
68
|
+
|
|
69
|
+
const DEFAULT_HEADER_HEIGHT = {
|
|
70
|
+
medium: 34,
|
|
71
|
+
large: 40
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const DEFAULT_HIDE_HEADER_CELL_WIDTH = {
|
|
75
|
+
medium: 38,
|
|
76
|
+
large: 46
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const ROW_HEIGHTS = {
|
|
80
|
+
compact: {
|
|
81
|
+
medium: 32,
|
|
82
|
+
large: 40
|
|
83
|
+
},
|
|
84
|
+
regular: {
|
|
85
|
+
medium: 40,
|
|
86
|
+
large: 50
|
|
87
|
+
},
|
|
88
|
+
spacious: {
|
|
89
|
+
medium: 48,
|
|
90
|
+
large: 60
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const SELECTION_CELL_DEFAULT_WIDTH = {
|
|
95
|
+
medium: 38,
|
|
96
|
+
large: 48
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const DRAG_BUTTON_CELL_DEFAULT_WIDTH = {
|
|
100
|
+
medium: 16,
|
|
101
|
+
large: 20
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const LEVEL_OFFSET_WIDTH = {
|
|
105
|
+
medium: 16,
|
|
106
|
+
large: 20
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export interface TableContextValue<T> {
|
|
110
|
+
state: TableState<T> | TreeGridState<T>,
|
|
111
|
+
dragState: DraggableCollectionState,
|
|
112
|
+
dropState: DroppableCollectionState,
|
|
113
|
+
dragAndDropHooks: DragAndDropHooks['dragAndDropHooks'],
|
|
114
|
+
isTableDraggable: boolean,
|
|
115
|
+
isTableDroppable: boolean,
|
|
116
|
+
layout: TableLayout<T> & {tableState: TableState<T> | TreeGridState<T>},
|
|
117
|
+
headerRowHovered: boolean,
|
|
118
|
+
isInResizeMode: boolean,
|
|
119
|
+
setIsInResizeMode: (val: boolean) => void,
|
|
120
|
+
isEmpty: boolean,
|
|
121
|
+
onFocusedResizer: () => void,
|
|
122
|
+
onResizeStart: (widths: Map<Key, ColumnSize>) => void,
|
|
123
|
+
onResize: (widths: Map<Key, ColumnSize>) => void,
|
|
124
|
+
onResizeEnd: (widths: Map<Key, ColumnSize>) => void,
|
|
125
|
+
headerMenuOpen: boolean,
|
|
126
|
+
setHeaderMenuOpen: (val: boolean) => void
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const TableContext = React.createContext<TableContextValue<unknown>>(null);
|
|
130
|
+
export function useTableContext() {
|
|
131
|
+
return useContext(TableContext);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const VirtualizerContext = React.createContext(null);
|
|
135
|
+
export function useVirtualizerContext() {
|
|
136
|
+
return useContext(VirtualizerContext);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface TableBaseProps<T> extends SpectrumTableProps<T> {
|
|
140
|
+
state: TableState<T> | TreeGridState<T>
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function TableViewBase<T extends object>(props: TableBaseProps<T>, ref: DOMRef<HTMLDivElement>) {
|
|
144
|
+
props = useProviderProps(props);
|
|
145
|
+
let {
|
|
146
|
+
isQuiet,
|
|
147
|
+
onAction,
|
|
148
|
+
onResizeStart: propsOnResizeStart,
|
|
149
|
+
onResizeEnd: propsOnResizeEnd,
|
|
150
|
+
dragAndDropHooks,
|
|
151
|
+
state
|
|
152
|
+
} = props;
|
|
153
|
+
let isTableDraggable = !!dragAndDropHooks?.useDraggableCollectionState;
|
|
154
|
+
let isTableDroppable = !!dragAndDropHooks?.useDroppableCollectionState;
|
|
155
|
+
let dragHooksProvided = useRef(isTableDraggable);
|
|
156
|
+
let dropHooksProvided = useRef(isTableDroppable);
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (dragHooksProvided.current !== isTableDraggable) {
|
|
159
|
+
console.warn('Drag hooks were provided during one render, but not another. This should be avoided as it may produce unexpected behavior.');
|
|
160
|
+
}
|
|
161
|
+
if (dropHooksProvided.current !== isTableDroppable) {
|
|
162
|
+
console.warn('Drop hooks were provided during one render, but not another. This should be avoided as it may produce unexpected behavior.');
|
|
163
|
+
}
|
|
164
|
+
if ('expandedKeys' in state && (isTableDraggable || isTableDroppable)) {
|
|
165
|
+
console.warn('Drag and drop is not yet fully supported with expandable rows and may produce unexpected results.');
|
|
166
|
+
}
|
|
167
|
+
}, [isTableDraggable, isTableDroppable, state]);
|
|
168
|
+
|
|
169
|
+
let {styleProps} = useStyleProps(props);
|
|
170
|
+
let {direction} = useLocale();
|
|
171
|
+
let {scale} = useProvider();
|
|
172
|
+
|
|
173
|
+
const getDefaultWidth = useCallback(({props: {hideHeader, isSelectionCell, showDivider, isDragButtonCell}}: GridNode<T>): ColumnSize | null | undefined => {
|
|
174
|
+
if (hideHeader) {
|
|
175
|
+
let width = DEFAULT_HIDE_HEADER_CELL_WIDTH[scale];
|
|
176
|
+
return showDivider ? width + 1 : width;
|
|
177
|
+
} else if (isSelectionCell) {
|
|
178
|
+
return SELECTION_CELL_DEFAULT_WIDTH[scale];
|
|
179
|
+
} else if (isDragButtonCell) {
|
|
180
|
+
return DRAG_BUTTON_CELL_DEFAULT_WIDTH[scale];
|
|
181
|
+
}
|
|
182
|
+
}, [scale]);
|
|
183
|
+
|
|
184
|
+
const getDefaultMinWidth = useCallback(({props: {hideHeader, isSelectionCell, showDivider, isDragButtonCell}}: GridNode<T>): ColumnSize | null | undefined => {
|
|
185
|
+
if (hideHeader) {
|
|
186
|
+
let width = DEFAULT_HIDE_HEADER_CELL_WIDTH[scale];
|
|
187
|
+
return showDivider ? width + 1 : width;
|
|
188
|
+
} else if (isSelectionCell) {
|
|
189
|
+
return SELECTION_CELL_DEFAULT_WIDTH[scale];
|
|
190
|
+
} else if (isDragButtonCell) {
|
|
191
|
+
return DRAG_BUTTON_CELL_DEFAULT_WIDTH[scale];
|
|
192
|
+
}
|
|
193
|
+
return 75;
|
|
194
|
+
}, [scale]);
|
|
195
|
+
|
|
196
|
+
// Starts when the user selects resize from the menu, ends when resizing ends
|
|
197
|
+
// used to control the visibility of the resizer Nubbin
|
|
198
|
+
let [isInResizeMode, setIsInResizeMode] = useState(false);
|
|
199
|
+
// Starts when the resizer is actually moved
|
|
200
|
+
// entering resizing/exiting resizing doesn't trigger a render
|
|
201
|
+
// with table layout, so we need to track it here
|
|
202
|
+
let [, setIsResizing] = useState(false);
|
|
203
|
+
|
|
204
|
+
let domRef = useDOMRef(ref);
|
|
205
|
+
let headerRef = useRef<HTMLDivElement>();
|
|
206
|
+
let bodyRef = useRef<HTMLDivElement>();
|
|
207
|
+
let stringFormatter = useLocalizedStringFormatter(intlMessages);
|
|
208
|
+
|
|
209
|
+
let density = props.density || 'regular';
|
|
210
|
+
let columnLayout = useMemo(
|
|
211
|
+
() => new TableColumnLayout({
|
|
212
|
+
getDefaultWidth,
|
|
213
|
+
getDefaultMinWidth
|
|
214
|
+
}),
|
|
215
|
+
[getDefaultWidth, getDefaultMinWidth]
|
|
216
|
+
);
|
|
217
|
+
let tableLayout = useMemo(() => new TableLayout({
|
|
218
|
+
// If props.rowHeight is auto, then use estimated heights based on scale, otherwise use fixed heights.
|
|
219
|
+
rowHeight: props.overflowMode === 'wrap'
|
|
220
|
+
? null
|
|
221
|
+
: ROW_HEIGHTS[density][scale],
|
|
222
|
+
estimatedRowHeight: props.overflowMode === 'wrap'
|
|
223
|
+
? ROW_HEIGHTS[density][scale]
|
|
224
|
+
: null,
|
|
225
|
+
headingHeight: props.overflowMode === 'wrap'
|
|
226
|
+
? null
|
|
227
|
+
: DEFAULT_HEADER_HEIGHT[scale],
|
|
228
|
+
estimatedHeadingHeight: props.overflowMode === 'wrap'
|
|
229
|
+
? DEFAULT_HEADER_HEIGHT[scale]
|
|
230
|
+
: null,
|
|
231
|
+
columnLayout,
|
|
232
|
+
initialCollection: state.collection
|
|
233
|
+
}),
|
|
234
|
+
// don't recompute when state.collection changes, only used for initial value
|
|
235
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
236
|
+
[props.overflowMode, scale, density, columnLayout]
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// Use a proxy so that a new object is created for each render so that alternate instances aren't affected by mutation.
|
|
240
|
+
// This can be thought of as equivalent to `{…tableLayout, tableState: state}`, but works with classes as well.
|
|
241
|
+
let layout = useMemo(() => {
|
|
242
|
+
let proxy = new Proxy(tableLayout, {
|
|
243
|
+
get(target, prop, receiver) {
|
|
244
|
+
return prop === 'tableState' ? state : Reflect.get(target, prop, receiver);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
return proxy as TableLayout<T> & {tableState: TableState<T> | TreeGridState<T>};
|
|
248
|
+
}, [state, tableLayout]);
|
|
249
|
+
|
|
250
|
+
let dragState: DraggableCollectionState;
|
|
251
|
+
let preview = useRef(null);
|
|
252
|
+
if (isTableDraggable) {
|
|
253
|
+
dragState = dragAndDropHooks.useDraggableCollectionState({
|
|
254
|
+
collection: state.collection,
|
|
255
|
+
selectionManager: state.selectionManager,
|
|
256
|
+
preview
|
|
257
|
+
});
|
|
258
|
+
dragAndDropHooks.useDraggableCollection({}, dragState, domRef);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let DragPreview = dragAndDropHooks?.DragPreview;
|
|
262
|
+
let dropState: DroppableCollectionState;
|
|
263
|
+
let droppableCollection: DroppableCollectionResult;
|
|
264
|
+
let isRootDropTarget: boolean;
|
|
265
|
+
if (isTableDroppable) {
|
|
266
|
+
dropState = dragAndDropHooks.useDroppableCollectionState({
|
|
267
|
+
collection: state.collection,
|
|
268
|
+
selectionManager: state.selectionManager
|
|
269
|
+
});
|
|
270
|
+
droppableCollection = dragAndDropHooks.useDroppableCollection({
|
|
271
|
+
keyboardDelegate: layout,
|
|
272
|
+
dropTargetDelegate: layout
|
|
273
|
+
}, dropState, domRef);
|
|
274
|
+
|
|
275
|
+
isRootDropTarget = dropState.isDropTarget({type: 'root'});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let {gridProps} = useTable({
|
|
279
|
+
...props,
|
|
280
|
+
isVirtualized: true,
|
|
281
|
+
layout,
|
|
282
|
+
onRowAction: onAction
|
|
283
|
+
}, state, domRef);
|
|
284
|
+
let [headerMenuOpen, setHeaderMenuOpen] = useState(false);
|
|
285
|
+
let [headerRowHovered, setHeaderRowHovered] = useState(false);
|
|
286
|
+
|
|
287
|
+
// This overrides collection view's renderWrapper to support DOM hierarchy.
|
|
288
|
+
type View = ReusableView<GridNode<T>, ReactNode>;
|
|
289
|
+
let renderWrapper = (parent: View, reusableView: View, children: View[], renderChildren: (views: View[]) => ReactElement[]) => {
|
|
290
|
+
let style = layoutInfoToStyle(reusableView.layoutInfo, direction, parent && parent.layoutInfo);
|
|
291
|
+
if (style.overflow === 'hidden') {
|
|
292
|
+
style.overflow = 'visible'; // needed to support position: sticky
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (reusableView.viewType === 'rowgroup') {
|
|
296
|
+
return (
|
|
297
|
+
<TableRowGroup key={reusableView.key} style={style}>
|
|
298
|
+
{isTableDroppable &&
|
|
299
|
+
<RootDropIndicator key="root" />
|
|
300
|
+
}
|
|
301
|
+
{renderChildren(children)}
|
|
302
|
+
</TableRowGroup>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (reusableView.viewType === 'header') {
|
|
307
|
+
return (
|
|
308
|
+
<TableHeader
|
|
309
|
+
key={reusableView.key}
|
|
310
|
+
style={style}>
|
|
311
|
+
{renderChildren(children)}
|
|
312
|
+
</TableHeader>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (reusableView.viewType === 'row') {
|
|
317
|
+
return (
|
|
318
|
+
<TableRow
|
|
319
|
+
key={reusableView.key}
|
|
320
|
+
item={reusableView.content}
|
|
321
|
+
style={style}
|
|
322
|
+
hasActions={onAction}
|
|
323
|
+
isTableDroppable={isTableDroppable}
|
|
324
|
+
isTableDraggable={isTableDraggable}>
|
|
325
|
+
{renderChildren(children)}
|
|
326
|
+
</TableRow>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (reusableView.viewType === 'headerrow') {
|
|
331
|
+
return (
|
|
332
|
+
<TableHeaderRow
|
|
333
|
+
onHoverChange={setHeaderRowHovered}
|
|
334
|
+
key={reusableView.key}
|
|
335
|
+
style={style}
|
|
336
|
+
item={reusableView.content}>
|
|
337
|
+
{renderChildren(children)}
|
|
338
|
+
</TableHeaderRow>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
let isDropTarget: boolean;
|
|
342
|
+
let isRootDroptarget: boolean;
|
|
343
|
+
if (isTableDroppable) {
|
|
344
|
+
if (parent.content) {
|
|
345
|
+
isDropTarget = dropState.isDropTarget({type: 'item', dropPosition: 'on', key: parent.content.key});
|
|
346
|
+
}
|
|
347
|
+
isRootDroptarget = dropState.isDropTarget({type: 'root'});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return (
|
|
351
|
+
<VirtualizerItem
|
|
352
|
+
key={reusableView.key}
|
|
353
|
+
layoutInfo={reusableView.layoutInfo}
|
|
354
|
+
virtualizer={reusableView.virtualizer}
|
|
355
|
+
parent={parent?.layoutInfo}
|
|
356
|
+
className={
|
|
357
|
+
classNames(
|
|
358
|
+
styles,
|
|
359
|
+
'spectrum-Table-cellWrapper',
|
|
360
|
+
classNames(
|
|
361
|
+
stylesOverrides,
|
|
362
|
+
{
|
|
363
|
+
'react-spectrum-Table-cellWrapper': !reusableView.layoutInfo.estimatedSize,
|
|
364
|
+
'react-spectrum-Table-cellWrapper--dropTarget': isDropTarget || isRootDroptarget
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
}>
|
|
369
|
+
{reusableView.rendered}
|
|
370
|
+
</VirtualizerItem>
|
|
371
|
+
);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
let renderView = (type: string, item: GridNode<T>) => {
|
|
375
|
+
switch (type) {
|
|
376
|
+
case 'header':
|
|
377
|
+
case 'rowgroup':
|
|
378
|
+
case 'section':
|
|
379
|
+
case 'row':
|
|
380
|
+
case 'headerrow':
|
|
381
|
+
return null;
|
|
382
|
+
case 'cell': {
|
|
383
|
+
if (item.props.isSelectionCell) {
|
|
384
|
+
return <TableCheckboxCell cell={item} />;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (item.props.isDragButtonCell) {
|
|
388
|
+
return <TableDragCell cell={item} />;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return <TableCell cell={item} />;
|
|
392
|
+
}
|
|
393
|
+
case 'placeholder':
|
|
394
|
+
// TODO: move to react-aria?
|
|
395
|
+
return (
|
|
396
|
+
<div
|
|
397
|
+
role="gridcell"
|
|
398
|
+
aria-colindex={item.index + 1}
|
|
399
|
+
aria-colspan={item.colspan > 1 ? item.colspan : null} />
|
|
400
|
+
);
|
|
401
|
+
case 'column':
|
|
402
|
+
if (item.props.isSelectionCell) {
|
|
403
|
+
return <TableSelectAllCell column={item} />;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (item.props.isDragButtonCell) {
|
|
407
|
+
return <TableDragHeaderCell column={item} />;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// TODO: consider this case, what if we have hidden headers and a empty table
|
|
411
|
+
if (item.props.hideHeader) {
|
|
412
|
+
return (
|
|
413
|
+
<TooltipTrigger placement="top" trigger="focus">
|
|
414
|
+
<TableColumnHeader column={item} />
|
|
415
|
+
<Tooltip placement="top">{item.rendered}</Tooltip>
|
|
416
|
+
</TooltipTrigger>
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (item.props.allowsResizing && !item.hasChildNodes) {
|
|
421
|
+
return <ResizableTableColumnHeader tableRef={domRef} column={item} />;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<TableColumnHeader column={item} />
|
|
426
|
+
);
|
|
427
|
+
case 'loader':
|
|
428
|
+
return (
|
|
429
|
+
<CenteredWrapper>
|
|
430
|
+
<ProgressCircle
|
|
431
|
+
isIndeterminate
|
|
432
|
+
aria-label={state.collection.size > 0 ? stringFormatter.format('loadingMore') : stringFormatter.format('loading')} />
|
|
433
|
+
</CenteredWrapper>
|
|
434
|
+
);
|
|
435
|
+
case 'empty': {
|
|
436
|
+
let emptyState = props.renderEmptyState ? props.renderEmptyState() : null;
|
|
437
|
+
if (emptyState == null) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return (
|
|
442
|
+
<CenteredWrapper>
|
|
443
|
+
{emptyState}
|
|
444
|
+
</CenteredWrapper>
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
let [isVerticalScrollbarVisible, setVerticalScollbarVisible] = useState(false);
|
|
451
|
+
let [isHorizontalScrollbarVisible, setHorizontalScollbarVisible] = useState(false);
|
|
452
|
+
let viewport = useRef({x: 0, y: 0, width: 0, height: 0});
|
|
453
|
+
let onVisibleRectChange = useCallback((e) => {
|
|
454
|
+
if (viewport.current.width === e.width && viewport.current.height === e.height) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
viewport.current = e;
|
|
458
|
+
if (bodyRef.current) {
|
|
459
|
+
setVerticalScollbarVisible(bodyRef.current.clientWidth + 2 < bodyRef.current.offsetWidth);
|
|
460
|
+
setHorizontalScollbarVisible(bodyRef.current.clientHeight + 2 < bodyRef.current.offsetHeight);
|
|
461
|
+
}
|
|
462
|
+
}, []);
|
|
463
|
+
let {isFocusVisible, focusProps} = useFocusRing();
|
|
464
|
+
let isEmpty = state.collection.size === 0;
|
|
465
|
+
|
|
466
|
+
let onFocusedResizer = () => {
|
|
467
|
+
bodyRef.current.scrollLeft = headerRef.current.scrollLeft;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
let onResizeStart = useCallback((widths) => {
|
|
471
|
+
setIsResizing(true);
|
|
472
|
+
propsOnResizeStart?.(widths);
|
|
473
|
+
}, [setIsResizing, propsOnResizeStart]);
|
|
474
|
+
let onResizeEnd = useCallback((widths) => {
|
|
475
|
+
setIsInResizeMode(false);
|
|
476
|
+
setIsResizing(false);
|
|
477
|
+
propsOnResizeEnd?.(widths);
|
|
478
|
+
}, [propsOnResizeEnd, setIsInResizeMode, setIsResizing]);
|
|
479
|
+
|
|
480
|
+
let focusedKey = state.selectionManager.focusedKey;
|
|
481
|
+
if (dropState?.target?.type === 'item') {
|
|
482
|
+
focusedKey = dropState.target.key;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
let mergedProps = mergeProps(
|
|
486
|
+
isTableDroppable && droppableCollection?.collectionProps,
|
|
487
|
+
gridProps,
|
|
488
|
+
focusProps,
|
|
489
|
+
dragAndDropHooks?.isVirtualDragging() && {tabIndex: null}
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
return (
|
|
493
|
+
<TableContext.Provider value={{state, dragState, dropState, dragAndDropHooks, isTableDraggable, isTableDroppable, layout, onResizeStart, onResize: props.onResize, onResizeEnd, headerRowHovered, isInResizeMode, setIsInResizeMode, isEmpty, onFocusedResizer, headerMenuOpen, setHeaderMenuOpen}}>
|
|
494
|
+
<TableVirtualizer
|
|
495
|
+
{...mergedProps}
|
|
496
|
+
{...styleProps}
|
|
497
|
+
className={
|
|
498
|
+
classNames(
|
|
499
|
+
styles,
|
|
500
|
+
'spectrum-Table',
|
|
501
|
+
`spectrum-Table--${density}`,
|
|
502
|
+
{
|
|
503
|
+
'spectrum-Table--quiet': isQuiet,
|
|
504
|
+
'spectrum-Table--wrap': props.overflowMode === 'wrap',
|
|
505
|
+
'spectrum-Table--loadingMore': state.collection.body.props.loadingState === 'loadingMore',
|
|
506
|
+
'spectrum-Table--isVerticalScrollbarVisible': isVerticalScrollbarVisible,
|
|
507
|
+
'spectrum-Table--isHorizontalScrollbarVisible': isHorizontalScrollbarVisible
|
|
508
|
+
},
|
|
509
|
+
classNames(
|
|
510
|
+
stylesOverrides,
|
|
511
|
+
'react-spectrum-Table'
|
|
512
|
+
),
|
|
513
|
+
styleProps.className
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
layout={layout}
|
|
517
|
+
collection={state.collection}
|
|
518
|
+
focusedKey={focusedKey}
|
|
519
|
+
renderView={renderView}
|
|
520
|
+
renderWrapper={renderWrapper}
|
|
521
|
+
onVisibleRectChange={onVisibleRectChange}
|
|
522
|
+
domRef={domRef}
|
|
523
|
+
headerRef={headerRef}
|
|
524
|
+
bodyRef={bodyRef}
|
|
525
|
+
isFocusVisible={isFocusVisible}
|
|
526
|
+
isVirtualDragging={dragAndDropHooks?.isVirtualDragging()}
|
|
527
|
+
isRootDropTarget={isRootDropTarget} />
|
|
528
|
+
{DragPreview && isTableDraggable &&
|
|
529
|
+
<DragPreview ref={preview}>
|
|
530
|
+
{() => {
|
|
531
|
+
if (dragAndDropHooks.renderPreview) {
|
|
532
|
+
return dragAndDropHooks.renderPreview(dragState.draggingKeys, dragState.draggedKey);
|
|
533
|
+
}
|
|
534
|
+
let itemCount = dragState.draggingKeys.size;
|
|
535
|
+
let maxWidth = bodyRef.current.getBoundingClientRect().width;
|
|
536
|
+
let height = ROW_HEIGHTS[density][scale];
|
|
537
|
+
let itemText = state.collection.getTextValue(dragState.draggedKey);
|
|
538
|
+
return <SpectrumDragPreview itemText={itemText} itemCount={itemCount} height={height} maxWidth={maxWidth} />;
|
|
539
|
+
}}
|
|
540
|
+
</DragPreview>
|
|
541
|
+
}
|
|
542
|
+
</TableContext.Provider>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// This is a custom Virtualizer that also has a header that syncs its scroll position with the body.
|
|
547
|
+
function TableVirtualizer(props) {
|
|
548
|
+
let {layout, collection, focusedKey, renderView, renderWrapper, domRef, bodyRef, headerRef, onVisibleRectChange: onVisibleRectChangeProp, isFocusVisible, isVirtualDragging, isRootDropTarget, ...otherProps} = props;
|
|
549
|
+
let {direction} = useLocale();
|
|
550
|
+
let loadingState = collection.body.props.loadingState;
|
|
551
|
+
let isLoading = loadingState === 'loading' || loadingState === 'loadingMore';
|
|
552
|
+
let onLoadMore = collection.body.props.onLoadMore;
|
|
553
|
+
let transitionDuration = 220;
|
|
554
|
+
if (isLoading) {
|
|
555
|
+
transitionDuration = 160;
|
|
556
|
+
}
|
|
557
|
+
if (layout.resizingColumn != null) {
|
|
558
|
+
// while resizing, prop changes should not cause animations
|
|
559
|
+
transitionDuration = 0;
|
|
560
|
+
}
|
|
561
|
+
let state = useVirtualizerState<object, ReactNode, ReactNode>({
|
|
562
|
+
layout,
|
|
563
|
+
collection,
|
|
564
|
+
renderView,
|
|
565
|
+
renderWrapper,
|
|
566
|
+
onVisibleRectChange(rect) {
|
|
567
|
+
bodyRef.current.scrollTop = rect.y;
|
|
568
|
+
setScrollLeft(bodyRef.current, direction, rect.x);
|
|
569
|
+
},
|
|
570
|
+
transitionDuration
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
let scrollToItem = useCallback((key) => {
|
|
574
|
+
let item = collection.getItem(key);
|
|
575
|
+
let column = collection.columns[0];
|
|
576
|
+
let virtualizer = state.virtualizer;
|
|
577
|
+
|
|
578
|
+
virtualizer.scrollToItem(key, {
|
|
579
|
+
duration: 0,
|
|
580
|
+
// Prevent scrolling to the top when clicking on column headers.
|
|
581
|
+
shouldScrollY: item?.type !== 'column',
|
|
582
|
+
// Offset scroll position by width of selection cell
|
|
583
|
+
// (which is sticky and will overlap the cell we're scrolling to).
|
|
584
|
+
offsetX: column.props.isSelectionCell || column.props.isDragButtonCell
|
|
585
|
+
? layout.getColumnWidth(column.key)
|
|
586
|
+
: 0
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// Sync the scroll positions of the column headers and the body so scrollIntoViewport can
|
|
590
|
+
// properly decide if the column is outside the viewport or not
|
|
591
|
+
headerRef.current.scrollLeft = bodyRef.current.scrollLeft;
|
|
592
|
+
}, [collection, bodyRef, headerRef, layout, state.virtualizer]);
|
|
593
|
+
|
|
594
|
+
let memoedVirtualizerProps = useMemo(() => ({
|
|
595
|
+
tabIndex: otherProps.tabIndex,
|
|
596
|
+
focusedKey,
|
|
597
|
+
scrollToItem,
|
|
598
|
+
isLoading,
|
|
599
|
+
onLoadMore
|
|
600
|
+
}), [otherProps.tabIndex, focusedKey, scrollToItem, isLoading, onLoadMore]);
|
|
601
|
+
|
|
602
|
+
let {virtualizerProps, scrollViewProps: {onVisibleRectChange}} = useVirtualizer(memoedVirtualizerProps, state, domRef);
|
|
603
|
+
|
|
604
|
+
// this effect runs whenever the contentSize changes, it doesn't matter what the content size is
|
|
605
|
+
// only that it changes in a resize, and when that happens, we want to sync the body to the
|
|
606
|
+
// header scroll position
|
|
607
|
+
useEffect(() => {
|
|
608
|
+
if (getInteractionModality() === 'keyboard' && headerRef.current.contains(document.activeElement)) {
|
|
609
|
+
scrollIntoView(headerRef.current, document.activeElement as HTMLElement);
|
|
610
|
+
scrollIntoViewport(document.activeElement, {containingElement: domRef.current});
|
|
611
|
+
bodyRef.current.scrollLeft = headerRef.current.scrollLeft;
|
|
612
|
+
}
|
|
613
|
+
}, [state.contentSize, headerRef, bodyRef, domRef]);
|
|
614
|
+
|
|
615
|
+
let headerHeight = layout.getLayoutInfo('header')?.rect.height || 0;
|
|
616
|
+
let visibleRect = state.virtualizer.visibleRect;
|
|
617
|
+
|
|
618
|
+
// Sync the scroll position from the table body to the header container.
|
|
619
|
+
let onScroll = useCallback(() => {
|
|
620
|
+
headerRef.current.scrollLeft = bodyRef.current.scrollLeft;
|
|
621
|
+
}, [bodyRef, headerRef]);
|
|
622
|
+
|
|
623
|
+
let resizerPosition = layout.getResizerPosition() - 2;
|
|
624
|
+
|
|
625
|
+
let resizerAtEdge = resizerPosition > Math.max(state.virtualizer.contentSize.width, state.virtualizer.visibleRect.width) - 3;
|
|
626
|
+
// this should be fine, every movement of the resizer causes a rerender
|
|
627
|
+
// scrolling can cause it to lag for a moment, but it's always updated
|
|
628
|
+
let resizerInVisibleRegion = resizerPosition < state.virtualizer.visibleRect.maxX;
|
|
629
|
+
let shouldHardCornerResizeCorner = resizerAtEdge && resizerInVisibleRegion;
|
|
630
|
+
|
|
631
|
+
// minimize re-render caused on Resizers by memoing this
|
|
632
|
+
let resizingColumnWidth = layout.getColumnWidth(layout.resizingColumn);
|
|
633
|
+
let resizingColumn = useMemo(() => ({
|
|
634
|
+
width: resizingColumnWidth,
|
|
635
|
+
key: layout.resizingColumn
|
|
636
|
+
}), [resizingColumnWidth, layout.resizingColumn]);
|
|
637
|
+
let mergedProps = mergeProps(
|
|
638
|
+
otherProps,
|
|
639
|
+
virtualizerProps,
|
|
640
|
+
isVirtualDragging && {tabIndex: null}
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
return (
|
|
644
|
+
<VirtualizerContext.Provider value={resizingColumn}>
|
|
645
|
+
<FocusScope>
|
|
646
|
+
<div
|
|
647
|
+
{...mergedProps}
|
|
648
|
+
ref={domRef}>
|
|
649
|
+
<div
|
|
650
|
+
role="presentation"
|
|
651
|
+
className={classNames(styles, 'spectrum-Table-headWrapper')}
|
|
652
|
+
style={{
|
|
653
|
+
width: visibleRect.width,
|
|
654
|
+
height: headerHeight,
|
|
655
|
+
overflow: 'hidden',
|
|
656
|
+
position: 'relative',
|
|
657
|
+
willChange: state.isScrolling ? 'scroll-position' : undefined,
|
|
658
|
+
transition: state.isAnimating ? `none ${state.virtualizer.transitionDuration}ms` : undefined
|
|
659
|
+
}}
|
|
660
|
+
ref={headerRef}>
|
|
661
|
+
{state.visibleViews[0]}
|
|
662
|
+
</div>
|
|
663
|
+
<ScrollView
|
|
664
|
+
role="presentation"
|
|
665
|
+
className={
|
|
666
|
+
classNames(
|
|
667
|
+
styles,
|
|
668
|
+
'spectrum-Table-body',
|
|
669
|
+
{
|
|
670
|
+
'focus-ring': isFocusVisible,
|
|
671
|
+
'spectrum-Table-body--resizerAtTableEdge': shouldHardCornerResizeCorner
|
|
672
|
+
},
|
|
673
|
+
classNames(
|
|
674
|
+
stylesOverrides,
|
|
675
|
+
'react-spectrum-Table-body',
|
|
676
|
+
{
|
|
677
|
+
'react-spectrum-Table-body--dropTarget': !!isRootDropTarget
|
|
678
|
+
}
|
|
679
|
+
)
|
|
680
|
+
)
|
|
681
|
+
}
|
|
682
|
+
tabIndex={isVirtualDragging ? null : -1}
|
|
683
|
+
style={{flex: 1}}
|
|
684
|
+
innerStyle={{overflow: 'visible', transition: state.isAnimating ? `none ${state.virtualizer.transitionDuration}ms` : undefined}}
|
|
685
|
+
ref={bodyRef}
|
|
686
|
+
contentSize={state.contentSize}
|
|
687
|
+
onVisibleRectChange={chain(onVisibleRectChange, onVisibleRectChangeProp)}
|
|
688
|
+
onScrollStart={state.startScrolling}
|
|
689
|
+
onScrollEnd={state.endScrolling}
|
|
690
|
+
onScroll={onScroll}>
|
|
691
|
+
{state.visibleViews[1]}
|
|
692
|
+
<div
|
|
693
|
+
className={classNames(styles, 'spectrum-Table-bodyResizeIndicator')}
|
|
694
|
+
style={{[direction === 'ltr' ? 'left' : 'right']: `${resizerPosition}px`, height: `${Math.max(state.virtualizer.contentSize.height, state.virtualizer.visibleRect.height)}px`, display: layout.resizingColumn ? 'block' : 'none'}} />
|
|
695
|
+
</ScrollView>
|
|
696
|
+
</div>
|
|
697
|
+
</FocusScope>
|
|
698
|
+
</VirtualizerContext.Provider>
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function TableHeader({children, ...otherProps}) {
|
|
703
|
+
let {rowGroupProps} = useTableRowGroup();
|
|
704
|
+
|
|
705
|
+
return (
|
|
706
|
+
<div {...rowGroupProps} {...otherProps} className={classNames(styles, 'spectrum-Table-head')}>
|
|
707
|
+
{children}
|
|
708
|
+
</div>
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function TableColumnHeader(props) {
|
|
713
|
+
let {column} = props;
|
|
714
|
+
let ref = useRef<HTMLDivElement>(null);
|
|
715
|
+
let {state, isEmpty} = useTableContext();
|
|
716
|
+
let {pressProps, isPressed} = usePress({isDisabled: isEmpty});
|
|
717
|
+
let columnProps = column.props as SpectrumColumnProps<unknown>;
|
|
718
|
+
useEffect(() => {
|
|
719
|
+
if (column.hasChildNodes && columnProps.allowsResizing) {
|
|
720
|
+
console.warn(`Column key: ${column.key}. Columns with child columns don't allow resizing.`);
|
|
721
|
+
}
|
|
722
|
+
}, [column.hasChildNodes, column.key, columnProps.allowsResizing]);
|
|
723
|
+
|
|
724
|
+
let {columnHeaderProps} = useTableColumnHeader({
|
|
725
|
+
node: column,
|
|
726
|
+
isVirtualized: true
|
|
727
|
+
}, state, ref);
|
|
728
|
+
|
|
729
|
+
let {hoverProps, isHovered} = useHover({...props, isDisabled: isEmpty});
|
|
730
|
+
|
|
731
|
+
const allProps = [columnHeaderProps, hoverProps, pressProps];
|
|
732
|
+
|
|
733
|
+
return (
|
|
734
|
+
<FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
|
|
735
|
+
<div
|
|
736
|
+
{...mergeProps(...allProps)}
|
|
737
|
+
ref={ref}
|
|
738
|
+
className={
|
|
739
|
+
classNames(
|
|
740
|
+
styles,
|
|
741
|
+
'spectrum-Table-headCell',
|
|
742
|
+
{
|
|
743
|
+
'is-active': isPressed,
|
|
744
|
+
'is-sortable': columnProps.allowsSorting,
|
|
745
|
+
'is-sorted-desc': state.sortDescriptor?.column === column.key && state.sortDescriptor?.direction === 'descending',
|
|
746
|
+
'is-sorted-asc': state.sortDescriptor?.column === column.key && state.sortDescriptor?.direction === 'ascending',
|
|
747
|
+
'is-hovered': isHovered,
|
|
748
|
+
'spectrum-Table-cell--hideHeader': columnProps.hideHeader
|
|
749
|
+
},
|
|
750
|
+
classNames(
|
|
751
|
+
stylesOverrides,
|
|
752
|
+
'react-spectrum-Table-cell',
|
|
753
|
+
{
|
|
754
|
+
'react-spectrum-Table-cell--alignCenter': columnProps.align === 'center' || column.colspan > 1,
|
|
755
|
+
'react-spectrum-Table-cell--alignEnd': columnProps.align === 'end'
|
|
756
|
+
}
|
|
757
|
+
)
|
|
758
|
+
)
|
|
759
|
+
}>
|
|
760
|
+
{columnProps.allowsSorting &&
|
|
761
|
+
<ArrowDownSmall UNSAFE_className={classNames(styles, 'spectrum-Table-sortedIcon')} />
|
|
762
|
+
}
|
|
763
|
+
{columnProps.hideHeader ?
|
|
764
|
+
<VisuallyHidden>{column.rendered}</VisuallyHidden> :
|
|
765
|
+
<div className={classNames(styles, 'spectrum-Table-headCellContents')}>{column.rendered}</div>
|
|
766
|
+
}
|
|
767
|
+
</div>
|
|
768
|
+
</FocusRing>
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
let _TableColumnHeaderButton = (props, ref: FocusableRef<HTMLDivElement>) => {
|
|
773
|
+
let {focusProps, alignment, ...otherProps} = props;
|
|
774
|
+
let {isEmpty} = useTableContext();
|
|
775
|
+
let domRef = useFocusableRef(ref);
|
|
776
|
+
let {buttonProps} = useButton({...otherProps, elementType: 'div', isDisabled: isEmpty}, domRef);
|
|
777
|
+
let {hoverProps, isHovered} = useHover({...otherProps, isDisabled: isEmpty});
|
|
778
|
+
|
|
779
|
+
return (
|
|
780
|
+
<div
|
|
781
|
+
className={
|
|
782
|
+
classNames(
|
|
783
|
+
styles,
|
|
784
|
+
'spectrum-Table-headCellContents',
|
|
785
|
+
{
|
|
786
|
+
'is-hovered': isHovered
|
|
787
|
+
}
|
|
788
|
+
)
|
|
789
|
+
}
|
|
790
|
+
{...hoverProps}>
|
|
791
|
+
<div
|
|
792
|
+
className={
|
|
793
|
+
classNames(
|
|
794
|
+
styles,
|
|
795
|
+
'spectrum-Table-headCellButton',
|
|
796
|
+
{
|
|
797
|
+
'spectrum-Table-headCellButton--alignStart': alignment === 'start',
|
|
798
|
+
'spectrum-Table-headCellButton--alignCenter': alignment === 'center',
|
|
799
|
+
'spectrum-Table-headCellButton--alignEnd': alignment === 'end'
|
|
800
|
+
}
|
|
801
|
+
)
|
|
802
|
+
}
|
|
803
|
+
{...mergeProps(buttonProps, focusProps)}
|
|
804
|
+
ref={domRef}>
|
|
805
|
+
{props.children}
|
|
806
|
+
</div>
|
|
807
|
+
</div>
|
|
808
|
+
);
|
|
809
|
+
};
|
|
810
|
+
let TableColumnHeaderButton = React.forwardRef(_TableColumnHeaderButton);
|
|
811
|
+
|
|
812
|
+
function ResizableTableColumnHeader(props) {
|
|
813
|
+
let {column} = props;
|
|
814
|
+
let ref = useRef(null);
|
|
815
|
+
let triggerRef = useRef(null);
|
|
816
|
+
let resizingRef = useRef(null);
|
|
817
|
+
let {
|
|
818
|
+
state,
|
|
819
|
+
layout,
|
|
820
|
+
onResizeStart,
|
|
821
|
+
onResize,
|
|
822
|
+
onResizeEnd,
|
|
823
|
+
headerRowHovered,
|
|
824
|
+
setIsInResizeMode,
|
|
825
|
+
isEmpty,
|
|
826
|
+
onFocusedResizer,
|
|
827
|
+
isInResizeMode,
|
|
828
|
+
headerMenuOpen,
|
|
829
|
+
setHeaderMenuOpen
|
|
830
|
+
} = useTableContext();
|
|
831
|
+
let stringFormatter = useLocalizedStringFormatter(intlMessages);
|
|
832
|
+
let {pressProps, isPressed} = usePress({isDisabled: isEmpty});
|
|
833
|
+
let {columnHeaderProps} = useTableColumnHeader({
|
|
834
|
+
node: column,
|
|
835
|
+
isVirtualized: true
|
|
836
|
+
}, state, ref);
|
|
837
|
+
|
|
838
|
+
let {hoverProps, isHovered} = useHover({...props, isDisabled: isEmpty || headerMenuOpen});
|
|
839
|
+
|
|
840
|
+
const allProps = [columnHeaderProps, pressProps, hoverProps];
|
|
841
|
+
|
|
842
|
+
let columnProps = column.props as SpectrumColumnProps<unknown>;
|
|
843
|
+
|
|
844
|
+
let {isFocusVisible, focusProps} = useFocusRing();
|
|
845
|
+
|
|
846
|
+
const onMenuSelect = (key) => {
|
|
847
|
+
switch (key) {
|
|
848
|
+
case 'sort-asc':
|
|
849
|
+
state.sort(column.key, 'ascending');
|
|
850
|
+
break;
|
|
851
|
+
case 'sort-desc':
|
|
852
|
+
state.sort(column.key, 'descending');
|
|
853
|
+
break;
|
|
854
|
+
case 'resize':
|
|
855
|
+
layout.startResize(column.key);
|
|
856
|
+
setIsInResizeMode(true);
|
|
857
|
+
state.setKeyboardNavigationDisabled(true);
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
let allowsSorting = column.props?.allowsSorting;
|
|
862
|
+
let items = useMemo(() => {
|
|
863
|
+
let options = [
|
|
864
|
+
allowsSorting ? {
|
|
865
|
+
label: stringFormatter.format('sortAscending'),
|
|
866
|
+
id: 'sort-asc'
|
|
867
|
+
} : undefined,
|
|
868
|
+
allowsSorting ? {
|
|
869
|
+
label: stringFormatter.format('sortDescending'),
|
|
870
|
+
id: 'sort-desc'
|
|
871
|
+
} : undefined,
|
|
872
|
+
{
|
|
873
|
+
label: stringFormatter.format('resizeColumn'),
|
|
874
|
+
id: 'resize'
|
|
875
|
+
}
|
|
876
|
+
];
|
|
877
|
+
return options;
|
|
878
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
879
|
+
}, [allowsSorting]);
|
|
880
|
+
let isMobile = useIsMobileDevice();
|
|
881
|
+
|
|
882
|
+
let resizingColumn = layout.resizingColumn;
|
|
883
|
+
let prevResizingColumn = useRef(null);
|
|
884
|
+
let timeout = useRef(null);
|
|
885
|
+
useEffect(() => {
|
|
886
|
+
if (prevResizingColumn.current !== resizingColumn &&
|
|
887
|
+
resizingColumn != null &&
|
|
888
|
+
resizingColumn === column.key) {
|
|
889
|
+
if (timeout.current) {
|
|
890
|
+
clearTimeout(timeout.current);
|
|
891
|
+
}
|
|
892
|
+
// focusSafely won't actually focus because the focus moves from the menuitem to the body during the after transition wait
|
|
893
|
+
// without the immediate timeout, Android Chrome doesn't move focus to the resizer
|
|
894
|
+
let focusResizer = () => {
|
|
895
|
+
resizingRef.current.focus();
|
|
896
|
+
onFocusedResizer();
|
|
897
|
+
timeout.current = null;
|
|
898
|
+
};
|
|
899
|
+
if (isMobile) {
|
|
900
|
+
timeout.current = setTimeout(focusResizer, 400);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
timeout.current = setTimeout(focusResizer, 0);
|
|
904
|
+
}
|
|
905
|
+
prevResizingColumn.current = resizingColumn;
|
|
906
|
+
}, [resizingColumn, column.key, isMobile, onFocusedResizer, resizingRef, prevResizingColumn, timeout]);
|
|
907
|
+
|
|
908
|
+
// eslint-disable-next-line arrow-body-style
|
|
909
|
+
useEffect(() => {
|
|
910
|
+
return () => clearTimeout(timeout.current);
|
|
911
|
+
}, []);
|
|
912
|
+
|
|
913
|
+
let showResizer = !isEmpty && ((headerRowHovered && getInteractionModality() !== 'keyboard') || resizingColumn != null);
|
|
914
|
+
let alignment = 'start';
|
|
915
|
+
let menuAlign = 'start' as 'start' | 'end';
|
|
916
|
+
if (columnProps.align === 'center' || column.colspan > 1) {
|
|
917
|
+
alignment = 'center';
|
|
918
|
+
} else if (columnProps.align === 'end') {
|
|
919
|
+
alignment = 'end';
|
|
920
|
+
menuAlign = 'end';
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
return (
|
|
924
|
+
<FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
|
|
925
|
+
<div
|
|
926
|
+
{...mergeProps(...allProps)}
|
|
927
|
+
ref={ref}
|
|
928
|
+
className={
|
|
929
|
+
classNames(
|
|
930
|
+
styles,
|
|
931
|
+
'spectrum-Table-headCell',
|
|
932
|
+
{
|
|
933
|
+
'is-active': isPressed,
|
|
934
|
+
'is-resizable': columnProps.allowsResizing,
|
|
935
|
+
'is-sortable': columnProps.allowsSorting,
|
|
936
|
+
'is-sorted-desc': state.sortDescriptor?.column === column.key && state.sortDescriptor?.direction === 'descending',
|
|
937
|
+
'is-sorted-asc': state.sortDescriptor?.column === column.key && state.sortDescriptor?.direction === 'ascending',
|
|
938
|
+
'is-hovered': isHovered,
|
|
939
|
+
'focus-ring': isFocusVisible,
|
|
940
|
+
'spectrum-Table-cell--hideHeader': columnProps.hideHeader
|
|
941
|
+
},
|
|
942
|
+
classNames(
|
|
943
|
+
stylesOverrides,
|
|
944
|
+
'react-spectrum-Table-cell',
|
|
945
|
+
{
|
|
946
|
+
'react-spectrum-Table-cell--alignCenter': alignment === 'center',
|
|
947
|
+
'react-spectrum-Table-cell--alignEnd': alignment === 'end'
|
|
948
|
+
}
|
|
949
|
+
)
|
|
950
|
+
)
|
|
951
|
+
}>
|
|
952
|
+
<MenuTrigger onOpenChange={setHeaderMenuOpen} align={menuAlign}>
|
|
953
|
+
<TableColumnHeaderButton alignment={alignment} ref={triggerRef} focusProps={focusProps}>
|
|
954
|
+
{columnProps.allowsSorting &&
|
|
955
|
+
<ArrowDownSmall UNSAFE_className={classNames(styles, 'spectrum-Table-sortedIcon')} />
|
|
956
|
+
}
|
|
957
|
+
{columnProps.hideHeader ?
|
|
958
|
+
<VisuallyHidden>{column.rendered}</VisuallyHidden> :
|
|
959
|
+
<div className={classNames(styles, 'spectrum-Table-headerCellText')}>{column.rendered}</div>
|
|
960
|
+
}
|
|
961
|
+
{
|
|
962
|
+
columnProps.allowsResizing && <ChevronDownMedium UNSAFE_className={classNames(styles, 'spectrum-Table-menuChevron')} />
|
|
963
|
+
}
|
|
964
|
+
</TableColumnHeaderButton>
|
|
965
|
+
<Menu onAction={onMenuSelect} minWidth="size-2000" items={items}>
|
|
966
|
+
{(item) => (
|
|
967
|
+
<Item>
|
|
968
|
+
{item.label}
|
|
969
|
+
</Item>
|
|
970
|
+
)}
|
|
971
|
+
</Menu>
|
|
972
|
+
</MenuTrigger>
|
|
973
|
+
<Resizer
|
|
974
|
+
ref={resizingRef}
|
|
975
|
+
column={column}
|
|
976
|
+
showResizer={showResizer}
|
|
977
|
+
onResizeStart={onResizeStart}
|
|
978
|
+
onResize={onResize}
|
|
979
|
+
onResizeEnd={onResizeEnd}
|
|
980
|
+
triggerRef={useUnwrapDOMRef(triggerRef)} />
|
|
981
|
+
<div
|
|
982
|
+
aria-hidden
|
|
983
|
+
className={classNames(
|
|
984
|
+
styles,
|
|
985
|
+
'spectrum-Table-colResizeIndicator',
|
|
986
|
+
{
|
|
987
|
+
'spectrum-Table-colResizeIndicator--visible': resizingColumn != null,
|
|
988
|
+
'spectrum-Table-colResizeIndicator--resizing': resizingColumn === column.key
|
|
989
|
+
}
|
|
990
|
+
)}>
|
|
991
|
+
<div
|
|
992
|
+
className={classNames(
|
|
993
|
+
styles,
|
|
994
|
+
'spectrum-Table-colResizeNubbin',
|
|
995
|
+
{
|
|
996
|
+
'spectrum-Table-colResizeNubbin--visible': isInResizeMode && resizingColumn === column.key
|
|
997
|
+
}
|
|
998
|
+
)}>
|
|
999
|
+
<Nubbin />
|
|
1000
|
+
</div>
|
|
1001
|
+
</div>
|
|
1002
|
+
</div>
|
|
1003
|
+
</FocusRing>
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function TableSelectAllCell({column}) {
|
|
1008
|
+
let ref = useRef();
|
|
1009
|
+
let {state} = useTableContext();
|
|
1010
|
+
let isSingleSelectionMode = state.selectionManager.selectionMode === 'single';
|
|
1011
|
+
let {columnHeaderProps} = useTableColumnHeader({
|
|
1012
|
+
node: column,
|
|
1013
|
+
isVirtualized: true
|
|
1014
|
+
}, state, ref);
|
|
1015
|
+
|
|
1016
|
+
let {checkboxProps} = useTableSelectAllCheckbox(state);
|
|
1017
|
+
let {hoverProps, isHovered} = useHover({});
|
|
1018
|
+
|
|
1019
|
+
return (
|
|
1020
|
+
<FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
|
|
1021
|
+
<div
|
|
1022
|
+
{...mergeProps(columnHeaderProps, hoverProps)}
|
|
1023
|
+
ref={ref}
|
|
1024
|
+
className={
|
|
1025
|
+
classNames(
|
|
1026
|
+
styles,
|
|
1027
|
+
'spectrum-Table-headCell',
|
|
1028
|
+
'spectrum-Table-checkboxCell',
|
|
1029
|
+
{
|
|
1030
|
+
'is-hovered': isHovered
|
|
1031
|
+
}
|
|
1032
|
+
)
|
|
1033
|
+
}>
|
|
1034
|
+
{
|
|
1035
|
+
/*
|
|
1036
|
+
In single selection mode, the checkbox will be hidden.
|
|
1037
|
+
So to avoid leaving a column header with no accessible content,
|
|
1038
|
+
we use a VisuallyHidden component to include the aria-label from the checkbox,
|
|
1039
|
+
which for single selection will be "Select."
|
|
1040
|
+
*/
|
|
1041
|
+
isSingleSelectionMode &&
|
|
1042
|
+
<VisuallyHidden>{checkboxProps['aria-label']}</VisuallyHidden>
|
|
1043
|
+
}
|
|
1044
|
+
<Checkbox
|
|
1045
|
+
{...checkboxProps}
|
|
1046
|
+
isEmphasized
|
|
1047
|
+
UNSAFE_style={isSingleSelectionMode ? {visibility: 'hidden'} : undefined}
|
|
1048
|
+
UNSAFE_className={classNames(styles, 'spectrum-Table-checkbox')} />
|
|
1049
|
+
</div>
|
|
1050
|
+
</FocusRing>
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function TableDragHeaderCell({column}) {
|
|
1055
|
+
let ref = useRef();
|
|
1056
|
+
let {state} = useTableContext();
|
|
1057
|
+
let {columnHeaderProps} = useTableColumnHeader({
|
|
1058
|
+
node: column,
|
|
1059
|
+
isVirtualized: true
|
|
1060
|
+
}, state, ref);
|
|
1061
|
+
let stringFormatter = useLocalizedStringFormatter(intlMessages);
|
|
1062
|
+
|
|
1063
|
+
return (
|
|
1064
|
+
<FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
|
|
1065
|
+
<div
|
|
1066
|
+
{...columnHeaderProps}
|
|
1067
|
+
ref={ref}
|
|
1068
|
+
className={
|
|
1069
|
+
classNames(
|
|
1070
|
+
styles,
|
|
1071
|
+
'spectrum-Table-headCell',
|
|
1072
|
+
classNames(
|
|
1073
|
+
stylesOverrides,
|
|
1074
|
+
'react-spectrum-Table-headCell',
|
|
1075
|
+
'react-spectrum-Table-dragButtonHeadCell'
|
|
1076
|
+
)
|
|
1077
|
+
)
|
|
1078
|
+
}>
|
|
1079
|
+
<VisuallyHidden>{stringFormatter.format('drag')}</VisuallyHidden>
|
|
1080
|
+
</div>
|
|
1081
|
+
</FocusRing>
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function TableRowGroup({children, ...otherProps}) {
|
|
1086
|
+
let {rowGroupProps} = useTableRowGroup();
|
|
1087
|
+
|
|
1088
|
+
return (
|
|
1089
|
+
<div {...rowGroupProps} {...otherProps}>
|
|
1090
|
+
{children}
|
|
1091
|
+
</div>
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function DragButton() {
|
|
1096
|
+
let {dragButtonProps, dragButtonRef, isFocusVisibleWithin} = useTableRowContext();
|
|
1097
|
+
let {visuallyHiddenProps} = useVisuallyHidden();
|
|
1098
|
+
return (
|
|
1099
|
+
<FocusRing focusRingClass={classNames(stylesOverrides, 'focus-ring')}>
|
|
1100
|
+
<div
|
|
1101
|
+
{...dragButtonProps as React.HTMLAttributes<HTMLElement>}
|
|
1102
|
+
className={
|
|
1103
|
+
classNames(
|
|
1104
|
+
stylesOverrides,
|
|
1105
|
+
'react-spectrum-Table-dragButton'
|
|
1106
|
+
)
|
|
1107
|
+
}
|
|
1108
|
+
style={!isFocusVisibleWithin ? {...visuallyHiddenProps.style} : {}}
|
|
1109
|
+
ref={dragButtonRef}
|
|
1110
|
+
draggable="true">
|
|
1111
|
+
<ListGripper UNSAFE_className={classNames(stylesOverrides)} />
|
|
1112
|
+
</div>
|
|
1113
|
+
</FocusRing>
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
interface TableRowContextValue {
|
|
1118
|
+
dragButtonProps: React.HTMLAttributes<HTMLDivElement>,
|
|
1119
|
+
dragButtonRef: React.MutableRefObject<undefined>,
|
|
1120
|
+
isFocusVisibleWithin: boolean
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
const TableRowContext = React.createContext<TableRowContextValue>(null);
|
|
1125
|
+
export function useTableRowContext() {
|
|
1126
|
+
return useContext(TableRowContext);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function TableRow({item, children, hasActions, isTableDraggable, isTableDroppable, ...otherProps}) {
|
|
1130
|
+
let ref = useRef();
|
|
1131
|
+
let {state, layout, dragAndDropHooks, dragState, dropState} = useTableContext();
|
|
1132
|
+
let allowsInteraction = state.selectionManager.selectionMode !== 'none' || hasActions;
|
|
1133
|
+
let isDisabled = !allowsInteraction || state.disabledKeys.has(item.key);
|
|
1134
|
+
let isDroppable = isTableDroppable && !isDisabled;
|
|
1135
|
+
let isSelected = state.selectionManager.isSelected(item.key);
|
|
1136
|
+
let {rowProps} = useTableRow({
|
|
1137
|
+
node: item,
|
|
1138
|
+
isVirtualized: true,
|
|
1139
|
+
shouldSelectOnPressUp: isTableDraggable
|
|
1140
|
+
}, state, ref);
|
|
1141
|
+
|
|
1142
|
+
let {pressProps, isPressed} = usePress({isDisabled});
|
|
1143
|
+
|
|
1144
|
+
// The row should show the focus background style when any cell inside it is focused.
|
|
1145
|
+
// If the row itself is focused, then it should have a blue focus indicator on the left.
|
|
1146
|
+
let {
|
|
1147
|
+
isFocusVisible: isFocusVisibleWithin,
|
|
1148
|
+
focusProps: focusWithinProps
|
|
1149
|
+
} = useFocusRing({within: true});
|
|
1150
|
+
let {isFocusVisible, focusProps} = useFocusRing();
|
|
1151
|
+
let {hoverProps, isHovered} = useHover({isDisabled});
|
|
1152
|
+
let isFirstRow = state.collection.rows.find(row => row.level === 1)?.key === item.key;
|
|
1153
|
+
let isLastRow = item.nextKey == null;
|
|
1154
|
+
// Figure out if the TableView content is equal or greater in height to the container. If so, we'll need to round the bottom
|
|
1155
|
+
// border corners of the last row when selected.
|
|
1156
|
+
let isFlushWithContainerBottom = false;
|
|
1157
|
+
if (isLastRow) {
|
|
1158
|
+
if (layout.getContentSize()?.height >= layout.virtualizer?.getVisibleRect().height) {
|
|
1159
|
+
isFlushWithContainerBottom = true;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
let draggableItem: DraggableItemResult;
|
|
1164
|
+
if (isTableDraggable) {
|
|
1165
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
1166
|
+
draggableItem = dragAndDropHooks.useDraggableItem({key: item.key, hasDragButton: true}, dragState);
|
|
1167
|
+
if (isDisabled) {
|
|
1168
|
+
draggableItem = null;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
let droppableItem: DroppableItemResult;
|
|
1172
|
+
let isDropTarget: boolean;
|
|
1173
|
+
let dropIndicator: DropIndicatorAria;
|
|
1174
|
+
let dropIndicatorRef = useRef();
|
|
1175
|
+
if (isTableDroppable) {
|
|
1176
|
+
let target = {type: 'item', key: item.key, dropPosition: 'on'} as DropTarget;
|
|
1177
|
+
isDropTarget = dropState.isDropTarget(target);
|
|
1178
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
1179
|
+
dropIndicator = dragAndDropHooks.useDropIndicator({target}, dropState, dropIndicatorRef);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
let dragButtonRef = React.useRef();
|
|
1183
|
+
let {buttonProps: dragButtonProps} = useButton({
|
|
1184
|
+
...draggableItem?.dragButtonProps,
|
|
1185
|
+
elementType: 'div'
|
|
1186
|
+
}, dragButtonRef);
|
|
1187
|
+
|
|
1188
|
+
let props = mergeProps(
|
|
1189
|
+
rowProps,
|
|
1190
|
+
otherProps,
|
|
1191
|
+
focusWithinProps,
|
|
1192
|
+
focusProps,
|
|
1193
|
+
hoverProps,
|
|
1194
|
+
pressProps,
|
|
1195
|
+
draggableItem?.dragProps,
|
|
1196
|
+
// Remove tab index from list row if performing a screenreader drag. This prevents TalkBack from focusing the row,
|
|
1197
|
+
// allowing for single swipe navigation between row drop indicator
|
|
1198
|
+
dragAndDropHooks?.isVirtualDragging() && {tabIndex: null}
|
|
1199
|
+
) as HTMLAttributes<HTMLElement> & DOMAttributes<FocusableElement>;
|
|
1200
|
+
|
|
1201
|
+
let dropProps = isDroppable ? droppableItem?.dropProps : {'aria-hidden': droppableItem?.dropProps['aria-hidden']};
|
|
1202
|
+
let {visuallyHiddenProps} = useVisuallyHidden();
|
|
1203
|
+
|
|
1204
|
+
return (
|
|
1205
|
+
<TableRowContext.Provider value={{dragButtonProps, dragButtonRef, isFocusVisibleWithin}}>
|
|
1206
|
+
{isTableDroppable && isFirstRow &&
|
|
1207
|
+
<InsertionIndicator
|
|
1208
|
+
rowProps={props}
|
|
1209
|
+
key={`${item.key}-before`}
|
|
1210
|
+
target={{key: item.key, type: 'item', dropPosition: 'before'}} />
|
|
1211
|
+
}
|
|
1212
|
+
{isTableDroppable && !dropIndicator?.isHidden &&
|
|
1213
|
+
<div role="row" {...visuallyHiddenProps}>
|
|
1214
|
+
<div role="gridcell">
|
|
1215
|
+
<div role="button" {...dropIndicator?.dropIndicatorProps} ref={dropIndicatorRef} />
|
|
1216
|
+
</div>
|
|
1217
|
+
</div>
|
|
1218
|
+
}
|
|
1219
|
+
<div
|
|
1220
|
+
{...mergeProps(props, dropProps)}
|
|
1221
|
+
ref={ref}
|
|
1222
|
+
className={
|
|
1223
|
+
classNames(
|
|
1224
|
+
styles,
|
|
1225
|
+
'spectrum-Table-row',
|
|
1226
|
+
{
|
|
1227
|
+
'is-active': isPressed,
|
|
1228
|
+
'is-selected': isSelected,
|
|
1229
|
+
'spectrum-Table-row--highlightSelection': state.selectionManager.selectionBehavior === 'replace',
|
|
1230
|
+
'is-next-selected': state.selectionManager.isSelected(item.nextKey),
|
|
1231
|
+
'is-focused': isFocusVisibleWithin,
|
|
1232
|
+
'focus-ring': isFocusVisible,
|
|
1233
|
+
'is-hovered': isHovered,
|
|
1234
|
+
'is-disabled': isDisabled,
|
|
1235
|
+
'spectrum-Table-row--firstRow': isFirstRow,
|
|
1236
|
+
'spectrum-Table-row--lastRow': isLastRow,
|
|
1237
|
+
'spectrum-Table-row--isFlushBottom': isFlushWithContainerBottom
|
|
1238
|
+
},
|
|
1239
|
+
classNames(
|
|
1240
|
+
stylesOverrides,
|
|
1241
|
+
'react-spectrum-Table-row',
|
|
1242
|
+
{'react-spectrum-Table-row--dropTarget': isDropTarget}
|
|
1243
|
+
)
|
|
1244
|
+
)
|
|
1245
|
+
}>
|
|
1246
|
+
{children}
|
|
1247
|
+
</div>
|
|
1248
|
+
{isTableDroppable &&
|
|
1249
|
+
<InsertionIndicator
|
|
1250
|
+
rowProps={props}
|
|
1251
|
+
key={`${item.key}-after`}
|
|
1252
|
+
target={{key: item.key, type: 'item', dropPosition: 'after'}} />
|
|
1253
|
+
}
|
|
1254
|
+
</TableRowContext.Provider>
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function TableHeaderRow({item, children, style, ...props}) {
|
|
1259
|
+
let {state, headerMenuOpen} = useTableContext();
|
|
1260
|
+
let ref = useRef();
|
|
1261
|
+
let {rowProps} = useTableHeaderRow({node: item, isVirtualized: true}, state, ref);
|
|
1262
|
+
let {hoverProps} = useHover({...props, isDisabled: headerMenuOpen});
|
|
1263
|
+
|
|
1264
|
+
return (
|
|
1265
|
+
<div {...mergeProps(rowProps, hoverProps)} ref={ref} style={style}>
|
|
1266
|
+
{children}
|
|
1267
|
+
</div>
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function TableDragCell({cell}) {
|
|
1272
|
+
let ref = useRef();
|
|
1273
|
+
let {state, isTableDraggable} = useTableContext();
|
|
1274
|
+
let isDisabled = state.disabledKeys.has(cell.parentKey);
|
|
1275
|
+
let {gridCellProps} = useTableCell({
|
|
1276
|
+
node: cell,
|
|
1277
|
+
isVirtualized: true
|
|
1278
|
+
}, state, ref);
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
return (
|
|
1282
|
+
<FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
|
|
1283
|
+
<div
|
|
1284
|
+
{...gridCellProps}
|
|
1285
|
+
ref={ref}
|
|
1286
|
+
className={
|
|
1287
|
+
classNames(
|
|
1288
|
+
styles,
|
|
1289
|
+
'spectrum-Table-cell',
|
|
1290
|
+
{
|
|
1291
|
+
'is-disabled': isDisabled
|
|
1292
|
+
},
|
|
1293
|
+
classNames(
|
|
1294
|
+
stylesOverrides,
|
|
1295
|
+
'react-spectrum-Table-cell',
|
|
1296
|
+
'react-spectrum-Table-dragButtonCell'
|
|
1297
|
+
)
|
|
1298
|
+
)
|
|
1299
|
+
}>
|
|
1300
|
+
{isTableDraggable && !isDisabled && <DragButton />}
|
|
1301
|
+
</div>
|
|
1302
|
+
</FocusRing>
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function TableCheckboxCell({cell}) {
|
|
1307
|
+
let ref = useRef();
|
|
1308
|
+
let {state} = useTableContext();
|
|
1309
|
+
let isDisabled = state.disabledKeys.has(cell.parentKey);
|
|
1310
|
+
let {gridCellProps} = useTableCell({
|
|
1311
|
+
node: cell,
|
|
1312
|
+
isVirtualized: true
|
|
1313
|
+
}, state, ref);
|
|
1314
|
+
|
|
1315
|
+
let {checkboxProps} = useTableSelectionCheckbox({key: cell.parentKey}, state);
|
|
1316
|
+
|
|
1317
|
+
return (
|
|
1318
|
+
<FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
|
|
1319
|
+
<div
|
|
1320
|
+
{...gridCellProps}
|
|
1321
|
+
ref={ref}
|
|
1322
|
+
className={
|
|
1323
|
+
classNames(
|
|
1324
|
+
styles,
|
|
1325
|
+
'spectrum-Table-cell',
|
|
1326
|
+
'spectrum-Table-checkboxCell',
|
|
1327
|
+
{
|
|
1328
|
+
'is-disabled': isDisabled
|
|
1329
|
+
},
|
|
1330
|
+
classNames(
|
|
1331
|
+
stylesOverrides,
|
|
1332
|
+
'react-spectrum-Table-cell'
|
|
1333
|
+
)
|
|
1334
|
+
)
|
|
1335
|
+
}>
|
|
1336
|
+
{state.selectionManager.selectionMode !== 'none' &&
|
|
1337
|
+
<Checkbox
|
|
1338
|
+
{...checkboxProps}
|
|
1339
|
+
isEmphasized
|
|
1340
|
+
isDisabled={isDisabled}
|
|
1341
|
+
UNSAFE_className={classNames(styles, 'spectrum-Table-checkbox')} />
|
|
1342
|
+
}
|
|
1343
|
+
</div>
|
|
1344
|
+
</FocusRing>
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function TableCell({cell}) {
|
|
1349
|
+
let {scale} = useProvider();
|
|
1350
|
+
let {state} = useTableContext();
|
|
1351
|
+
let isExpandableTable = 'expandedKeys' in state;
|
|
1352
|
+
let ref = useRef();
|
|
1353
|
+
let columnProps = cell.column.props as SpectrumColumnProps<unknown>;
|
|
1354
|
+
let isDisabled = state.disabledKeys.has(cell.parentKey);
|
|
1355
|
+
let {gridCellProps} = useTableCell({
|
|
1356
|
+
node: cell,
|
|
1357
|
+
isVirtualized: true
|
|
1358
|
+
}, state, ref);
|
|
1359
|
+
let {id, ...otherGridCellProps} = gridCellProps;
|
|
1360
|
+
let isFirstRowHeaderCell = state.collection.rowHeaderColumnKeys.keys().next().value === cell.column.key;
|
|
1361
|
+
let isRowExpandable = false;
|
|
1362
|
+
let showExpandCollapseButton = false;
|
|
1363
|
+
let levelOffset = 0;
|
|
1364
|
+
|
|
1365
|
+
if ('expandedKeys' in state) {
|
|
1366
|
+
isRowExpandable = state.keyMap.get(cell.parentKey)?.props.UNSTABLE_childItems?.length > 0 || state.keyMap.get(cell.parentKey)?.props?.children?.length > state.userColumnCount;
|
|
1367
|
+
showExpandCollapseButton = isFirstRowHeaderCell && isRowExpandable;
|
|
1368
|
+
// Offset based on level, and add additional offset if there is no expand/collapse button on a row
|
|
1369
|
+
levelOffset = (cell.level - 2) * LEVEL_OFFSET_WIDTH[scale] + (!showExpandCollapseButton ? LEVEL_OFFSET_WIDTH[scale] * 2 : 0);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
return (
|
|
1373
|
+
<FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
|
|
1374
|
+
<div
|
|
1375
|
+
{...otherGridCellProps}
|
|
1376
|
+
aria-labelledby={id}
|
|
1377
|
+
ref={ref}
|
|
1378
|
+
style={isExpandableTable && isFirstRowHeaderCell ? {paddingInlineStart: levelOffset} : {}}
|
|
1379
|
+
className={
|
|
1380
|
+
classNames(
|
|
1381
|
+
styles,
|
|
1382
|
+
'spectrum-Table-cell',
|
|
1383
|
+
{
|
|
1384
|
+
'spectrum-Table-cell--divider': columnProps.showDivider && cell.column.nextKey !== null,
|
|
1385
|
+
'spectrum-Table-cell--hideHeader': columnProps.hideHeader,
|
|
1386
|
+
'is-disabled': isDisabled
|
|
1387
|
+
},
|
|
1388
|
+
classNames(
|
|
1389
|
+
stylesOverrides,
|
|
1390
|
+
'react-spectrum-Table-cell',
|
|
1391
|
+
{
|
|
1392
|
+
'react-spectrum-Table-cell--alignStart': columnProps.align === 'start',
|
|
1393
|
+
'react-spectrum-Table-cell--alignCenter': columnProps.align === 'center',
|
|
1394
|
+
'react-spectrum-Table-cell--alignEnd': columnProps.align === 'end',
|
|
1395
|
+
'react-spectrum-Table-cell--hasExpandCollapseButton': showExpandCollapseButton
|
|
1396
|
+
}
|
|
1397
|
+
)
|
|
1398
|
+
)
|
|
1399
|
+
}>
|
|
1400
|
+
{showExpandCollapseButton && <ExpandableRowChevron cell={cell} />}
|
|
1401
|
+
<span
|
|
1402
|
+
id={id}
|
|
1403
|
+
className={
|
|
1404
|
+
classNames(
|
|
1405
|
+
styles,
|
|
1406
|
+
'spectrum-Table-cellContents'
|
|
1407
|
+
)
|
|
1408
|
+
}>
|
|
1409
|
+
{cell.rendered}
|
|
1410
|
+
</span>
|
|
1411
|
+
</div>
|
|
1412
|
+
</FocusRing>
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function ExpandableRowChevron({cell}) {
|
|
1417
|
+
// TODO: move some/all of the chevron button setup into a separate hook?
|
|
1418
|
+
let {direction} = useLocale();
|
|
1419
|
+
let {state} = useTableContext();
|
|
1420
|
+
let expandButtonRef = useRef();
|
|
1421
|
+
let stringFormatter = useLocalizedStringFormatter(intlMessages);
|
|
1422
|
+
let isExpanded;
|
|
1423
|
+
|
|
1424
|
+
if ('expandedKeys' in state) {
|
|
1425
|
+
isExpanded = state.expandedKeys === 'all' || state.expandedKeys.has(cell.parentKey);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// Will need to keep the chevron as a button for iOS VO at all times since VO doesn't focus the cell. Also keep as button if cellAction is defined by the user in the future
|
|
1429
|
+
let {buttonProps} = useButton({
|
|
1430
|
+
// Desktop and mobile both toggle expansion of a native expandable row on mouse/touch up
|
|
1431
|
+
onPress: () => {
|
|
1432
|
+
(state as TreeGridState<unknown>).toggleKey(cell.parentKey);
|
|
1433
|
+
if (!isFocusVisible()) {
|
|
1434
|
+
state.selectionManager.setFocused(true);
|
|
1435
|
+
state.selectionManager.setFocusedKey(cell.parentKey);
|
|
1436
|
+
}
|
|
1437
|
+
},
|
|
1438
|
+
elementType: 'span',
|
|
1439
|
+
'aria-label': isExpanded ? stringFormatter.format('collapse') : stringFormatter.format('expand')
|
|
1440
|
+
}, expandButtonRef);
|
|
1441
|
+
|
|
1442
|
+
return (
|
|
1443
|
+
<span
|
|
1444
|
+
{...buttonProps}
|
|
1445
|
+
ref={expandButtonRef}
|
|
1446
|
+
// Override tabindex so that grid keyboard nav skips over it. Needs -1 so android talkback can actually "focus" it
|
|
1447
|
+
tabIndex={isAndroid() ? -1 : undefined}
|
|
1448
|
+
className={
|
|
1449
|
+
classNames(
|
|
1450
|
+
styles,
|
|
1451
|
+
'spectrum-Table-expandButton',
|
|
1452
|
+
{
|
|
1453
|
+
'is-open': isExpanded
|
|
1454
|
+
}
|
|
1455
|
+
)
|
|
1456
|
+
}>
|
|
1457
|
+
{direction === 'ltr' ? <ChevronRightMedium /> : <ChevronLeftMedium />}
|
|
1458
|
+
</span>
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function CenteredWrapper({children}) {
|
|
1463
|
+
let {state} = useTableContext();
|
|
1464
|
+
let rowProps;
|
|
1465
|
+
|
|
1466
|
+
if ('expandedKeys' in state) {
|
|
1467
|
+
let topLevelRowCount = [...state.keyMap.get(state.collection.body.key).childNodes].length;
|
|
1468
|
+
rowProps = {
|
|
1469
|
+
'aria-level': 1,
|
|
1470
|
+
'aria-posinset': topLevelRowCount + 1,
|
|
1471
|
+
'aria-setsize': topLevelRowCount + 1
|
|
1472
|
+
};
|
|
1473
|
+
} else {
|
|
1474
|
+
rowProps = {
|
|
1475
|
+
'aria-rowindex': state.collection.headerRows.length + state.collection.size + 1
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
return (
|
|
1480
|
+
<div
|
|
1481
|
+
role="row"
|
|
1482
|
+
{...rowProps}
|
|
1483
|
+
className={classNames(stylesOverrides, 'react-spectrum-Table-centeredWrapper')}>
|
|
1484
|
+
<div role="rowheader" aria-colspan={state.collection.columns.length}>
|
|
1485
|
+
{children}
|
|
1486
|
+
</div>
|
|
1487
|
+
</div>
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
const _TableViewBase = React.forwardRef(TableViewBase) as <T>(props: TableBaseProps<T> & {ref?: DOMRef<HTMLDivElement>}) => ReactElement;
|
|
1492
|
+
|
|
1493
|
+
export {_TableViewBase as TableViewBase};
|