@jielga/tmdatagrid 0.1.0
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/LICENSE +21 -0
- package/README.md +162 -0
- package/dist/index.d.ts +592 -0
- package/dist/index.js +1590 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +2 -0
- package/package.json +111 -0
- package/skills/columns/SKILL.md +195 -0
- package/skills/features/SKILL.md +214 -0
- package/skills/getting-started/SKILL.md +209 -0
- package/skills/options/SKILL.md +208 -0
- package/skills/server-side/SKILL.md +150 -0
- package/src/tmdatagrid/TMDataGrid.module.css +410 -0
- package/src/tmdatagrid/TMDataGrid.tsx +101 -0
- package/src/tmdatagrid/TMDataGridColumnsPanel.tsx +91 -0
- package/src/tmdatagrid/TMDataGridContext.ts +32 -0
- package/src/tmdatagrid/TMDataGridFilterPanel.tsx +182 -0
- package/src/tmdatagrid/TMDataGridFooter.tsx +74 -0
- package/src/tmdatagrid/TMDataGridHeaderCell.tsx +466 -0
- package/src/tmdatagrid/TMDataGridTable.tsx +297 -0
- package/src/tmdatagrid/TMDataGridToolbar.tsx +131 -0
- package/src/tmdatagrid/capabilities.ts +117 -0
- package/src/tmdatagrid/columnOrdering.ts +140 -0
- package/src/tmdatagrid/columnUtils.ts +43 -0
- package/src/tmdatagrid/filterOperators.ts +171 -0
- package/src/tmdatagrid/icons.ts +25 -0
- package/src/tmdatagrid/index.ts +68 -0
- package/src/tmdatagrid/persistence.ts +187 -0
- package/src/tmdatagrid/sizes.ts +29 -0
- package/src/tmdatagrid/useTMDataGrid.tsx +378 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { ActionIcon, Menu, Tooltip } from "@mantine/core";
|
|
2
|
+
import { flexRender, type Header } from "@tanstack/react-table";
|
|
3
|
+
import { useSelector } from "@tanstack/react-store";
|
|
4
|
+
import { shallow } from "@tanstack/store";
|
|
5
|
+
import { type DragEvent, type ReactNode, useRef, useState } from "react";
|
|
6
|
+
import classes from "./TMDataGrid.module.css";
|
|
7
|
+
import {
|
|
8
|
+
type TMDataGridRowData,
|
|
9
|
+
useTMDataGridContext,
|
|
10
|
+
} from "./TMDataGridContext";
|
|
11
|
+
import type { TMDataGridColumnLayout } from "./TMDataGridTable";
|
|
12
|
+
import { getColumnCapabilities, getGridCapabilities } from "./capabilities";
|
|
13
|
+
import {
|
|
14
|
+
getColumnRegion,
|
|
15
|
+
getStepTargetColumn,
|
|
16
|
+
moveColumn,
|
|
17
|
+
moveColumnByStep,
|
|
18
|
+
type TMDataGridDropSide,
|
|
19
|
+
} from "./columnOrdering";
|
|
20
|
+
import { getColumnAlign, getColumnLabel } from "./columnUtils";
|
|
21
|
+
import { isFilterActive } from "./filterOperators";
|
|
22
|
+
import {
|
|
23
|
+
ArrowDownIcon,
|
|
24
|
+
ArrowUpIcon,
|
|
25
|
+
ColumnsIcon,
|
|
26
|
+
DotsVerticalIcon,
|
|
27
|
+
EyeOffIcon,
|
|
28
|
+
FilterIcon,
|
|
29
|
+
MoveLeftIcon,
|
|
30
|
+
MoveRightIcon,
|
|
31
|
+
PinLeftIcon,
|
|
32
|
+
PinOffIcon,
|
|
33
|
+
PinRightIcon,
|
|
34
|
+
} from "./icons";
|
|
35
|
+
import {
|
|
36
|
+
type TMDataGridFeatures,
|
|
37
|
+
openColumnFilter,
|
|
38
|
+
SELECT_COLUMN_ID,
|
|
39
|
+
} from "./useTMDataGrid";
|
|
40
|
+
|
|
41
|
+
export type TMDataGridHeader = Header<
|
|
42
|
+
TMDataGridFeatures,
|
|
43
|
+
TMDataGridRowData,
|
|
44
|
+
unknown
|
|
45
|
+
>;
|
|
46
|
+
|
|
47
|
+
export function TMDataGridHeaderCell({
|
|
48
|
+
header,
|
|
49
|
+
layout,
|
|
50
|
+
}: {
|
|
51
|
+
header: TMDataGridHeader;
|
|
52
|
+
layout: TMDataGridColumnLayout;
|
|
53
|
+
}) {
|
|
54
|
+
const api = useTMDataGridContext();
|
|
55
|
+
const { table, features } = api;
|
|
56
|
+
const column = header.column;
|
|
57
|
+
const [menuOpened, setMenuOpened] = useState(false);
|
|
58
|
+
const [dropSide, setDropSide] = useState<TMDataGridDropSide | null>(null);
|
|
59
|
+
const cellRef = useRef<HTMLDivElement>(null);
|
|
60
|
+
// A resize drag ends with a click that bubbles to the header — without this
|
|
61
|
+
// every resize would also toggle the column's sort order.
|
|
62
|
+
const suppressSortRef = useRef(false);
|
|
63
|
+
|
|
64
|
+
// Subscribing to the slices this cell renders from keeps it live even when the
|
|
65
|
+
// surrounding tree is memoized (React Compiler) or the table lives elsewhere.
|
|
66
|
+
// `columnOrder` is in the list because the move menu items are derived from
|
|
67
|
+
// the column's neighbours, which is exactly what an order change invalidates.
|
|
68
|
+
const { sorting, columnFilters, resizingColumnId, columnPinning } =
|
|
69
|
+
useSelector(
|
|
70
|
+
table.store,
|
|
71
|
+
(state) => ({
|
|
72
|
+
sorting: state.sorting,
|
|
73
|
+
columnFilters: state.columnFilters,
|
|
74
|
+
resizingColumnId: state.columnResizing.isResizingColumn,
|
|
75
|
+
columnPinning: state.columnPinning,
|
|
76
|
+
columnOrder: state.columnOrder,
|
|
77
|
+
}),
|
|
78
|
+
{ compare: shallow },
|
|
79
|
+
);
|
|
80
|
+
const draggedColumnId = useSelector(api.ui, (state) => state.draggedColumnId);
|
|
81
|
+
|
|
82
|
+
const isLeaf = header.subHeaders.length === 0;
|
|
83
|
+
const label = getColumnLabel(column);
|
|
84
|
+
const align = getColumnAlign(column);
|
|
85
|
+
const sortDirection = sorting.find((sort) => sort.id === column.id)?.desc;
|
|
86
|
+
const isSorted = sortDirection !== undefined;
|
|
87
|
+
const isFiltered = columnFilters.some(
|
|
88
|
+
(filter) => filter.id === column.id && isFilterActive(filter.value),
|
|
89
|
+
);
|
|
90
|
+
const isResizing = resizingColumnId === column.id;
|
|
91
|
+
const pinnedAt = layout.pinnedAt;
|
|
92
|
+
|
|
93
|
+
// Every affordance below mirrors a TanStack capability check, so switching a
|
|
94
|
+
// feature off through the table options removes the markup with it.
|
|
95
|
+
const capabilities = getColumnCapabilities(column, features);
|
|
96
|
+
const canSort = isLeaf && capabilities.canSort;
|
|
97
|
+
const canFilter = isLeaf && capabilities.canFilter;
|
|
98
|
+
const canHide = isLeaf && capabilities.canHide;
|
|
99
|
+
const canPin = isLeaf && capabilities.canPin;
|
|
100
|
+
const canResize = capabilities.canResize;
|
|
101
|
+
const canReorder = isLeaf && capabilities.canReorder;
|
|
102
|
+
const canManageColumns =
|
|
103
|
+
isLeaf && getGridCapabilities(table, features).canHideAny;
|
|
104
|
+
|
|
105
|
+
// A column only moves inside its own pinned lane, so a header in another one
|
|
106
|
+
// never lights up as a target. See TMDataGridColumnRegion.
|
|
107
|
+
const isDragged = draggedColumnId === column.id;
|
|
108
|
+
const isDropTarget =
|
|
109
|
+
canReorder &&
|
|
110
|
+
draggedColumnId !== null &&
|
|
111
|
+
!isDragged &&
|
|
112
|
+
getColumnRegion(columnPinning, draggedColumnId) ===
|
|
113
|
+
getColumnRegion(columnPinning, column.id);
|
|
114
|
+
|
|
115
|
+
function handleDragStart(event: DragEvent<HTMLDivElement>) {
|
|
116
|
+
event.dataTransfer.effectAllowed = "move";
|
|
117
|
+
// Some browsers refuse to start a drag with an empty payload.
|
|
118
|
+
event.dataTransfer.setData("text/plain", column.id);
|
|
119
|
+
api.ui.actions.startColumnDrag(column.id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function handleDragOver(event: DragEvent<HTMLDivElement>) {
|
|
123
|
+
if (!isDropTarget) return;
|
|
124
|
+
// Only a prevented dragover marks an element as a drop target.
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
event.dataTransfer.dropEffect = "move";
|
|
127
|
+
const bounds = cellRef.current?.getBoundingClientRect();
|
|
128
|
+
if (!bounds) return;
|
|
129
|
+
setDropSide(
|
|
130
|
+
event.clientX < bounds.left + bounds.width / 2 ? "before" : "after",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function handleDragLeave(event: DragEvent<HTMLDivElement>) {
|
|
135
|
+
// dragleave also fires when the pointer crosses into a child element.
|
|
136
|
+
const next = event.relatedTarget;
|
|
137
|
+
if (next instanceof Node && event.currentTarget.contains(next)) return;
|
|
138
|
+
setDropSide(null);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function handleDrop(event: DragEvent<HTMLDivElement>) {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
setDropSide(null);
|
|
144
|
+
api.ui.actions.endColumnDrag();
|
|
145
|
+
if (!isDropTarget || !dropSide || !draggedColumnId) return;
|
|
146
|
+
moveColumn({
|
|
147
|
+
table,
|
|
148
|
+
columnId: draggedColumnId,
|
|
149
|
+
targetId: column.id,
|
|
150
|
+
side: dropSide,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function handleDragEnd() {
|
|
155
|
+
setDropSide(null);
|
|
156
|
+
api.ui.actions.endColumnDrag();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Pinned columns are positioned with `getStart()` / `getAfter()`, which sum
|
|
161
|
+
* `column.getSize()`. Fluid (`fr`) columns don't report their rendered width
|
|
162
|
+
* there, so freeze the measured width into `columnSizing` as we pin — that
|
|
163
|
+
* keeps the column looking the same and keeps the offsets exact.
|
|
164
|
+
*/
|
|
165
|
+
function setPinned(position: "left" | "right" | false) {
|
|
166
|
+
if (position && !(column.id in table.store.state.columnSizing)) {
|
|
167
|
+
const width = cellRef.current?.getBoundingClientRect().width;
|
|
168
|
+
if (width) {
|
|
169
|
+
table.setColumnSizing((prev) => ({
|
|
170
|
+
...prev,
|
|
171
|
+
[column.id]: Math.round(width),
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
column.pin(position);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const menuItems: ReactNode[] = [];
|
|
179
|
+
if (canSort) {
|
|
180
|
+
menuItems.push(
|
|
181
|
+
<Menu.Item
|
|
182
|
+
key="sort-asc"
|
|
183
|
+
leftSection={<ArrowUpIcon size={16} stroke={1.6} />}
|
|
184
|
+
onClick={() => column.toggleSorting(false)}
|
|
185
|
+
>
|
|
186
|
+
Sort by ASC
|
|
187
|
+
</Menu.Item>,
|
|
188
|
+
<Menu.Item
|
|
189
|
+
key="sort-desc"
|
|
190
|
+
leftSection={<ArrowDownIcon size={16} stroke={1.6} />}
|
|
191
|
+
onClick={() => column.toggleSorting(true)}
|
|
192
|
+
>
|
|
193
|
+
Sort by DESC
|
|
194
|
+
</Menu.Item>,
|
|
195
|
+
<Menu.Divider key="sort-divider" />,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
if (canFilter) {
|
|
199
|
+
menuItems.push(
|
|
200
|
+
<Menu.Item
|
|
201
|
+
key="filter"
|
|
202
|
+
leftSection={<FilterIcon size={16} stroke={1.6} />}
|
|
203
|
+
onClick={() => openColumnFilter(api, column.id)}
|
|
204
|
+
>
|
|
205
|
+
Filter
|
|
206
|
+
</Menu.Item>,
|
|
207
|
+
<Menu.Divider key="filter-divider" />,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (canPin) {
|
|
211
|
+
menuItems.push(
|
|
212
|
+
<Menu.Item
|
|
213
|
+
key="pin-left"
|
|
214
|
+
className={pinnedAt === "left" ? classes.menuItemActive : undefined}
|
|
215
|
+
leftSection={<PinLeftIcon size={16} stroke={1.6} />}
|
|
216
|
+
onClick={() => setPinned(pinnedAt === "left" ? false : "left")}
|
|
217
|
+
>
|
|
218
|
+
Pin to left
|
|
219
|
+
</Menu.Item>,
|
|
220
|
+
<Menu.Item
|
|
221
|
+
key="pin-right"
|
|
222
|
+
className={pinnedAt === "right" ? classes.menuItemActive : undefined}
|
|
223
|
+
leftSection={<PinRightIcon size={16} stroke={1.6} />}
|
|
224
|
+
onClick={() => setPinned(pinnedAt === "right" ? false : "right")}
|
|
225
|
+
>
|
|
226
|
+
Pin to right
|
|
227
|
+
</Menu.Item>,
|
|
228
|
+
);
|
|
229
|
+
if (pinnedAt) {
|
|
230
|
+
menuItems.push(
|
|
231
|
+
<Menu.Item
|
|
232
|
+
key="unpin"
|
|
233
|
+
leftSection={<PinOffIcon size={16} stroke={1.6} />}
|
|
234
|
+
onClick={() => setPinned(false)}
|
|
235
|
+
>
|
|
236
|
+
Unpin
|
|
237
|
+
</Menu.Item>,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
menuItems.push(<Menu.Divider key="pin-divider" />);
|
|
241
|
+
}
|
|
242
|
+
if (canReorder) {
|
|
243
|
+
// The keyboard path to reordering, and the discoverable one — a header you
|
|
244
|
+
// can drag looks no different from one you cannot.
|
|
245
|
+
const previous = getStepTargetColumn({
|
|
246
|
+
table,
|
|
247
|
+
columnId: column.id,
|
|
248
|
+
direction: -1,
|
|
249
|
+
});
|
|
250
|
+
const next = getStepTargetColumn({
|
|
251
|
+
table,
|
|
252
|
+
columnId: column.id,
|
|
253
|
+
direction: 1,
|
|
254
|
+
});
|
|
255
|
+
if (previous || next) {
|
|
256
|
+
menuItems.push(
|
|
257
|
+
<Menu.Item
|
|
258
|
+
key="move-left"
|
|
259
|
+
disabled={!previous}
|
|
260
|
+
leftSection={<MoveLeftIcon size={16} stroke={1.6} />}
|
|
261
|
+
onClick={() =>
|
|
262
|
+
moveColumnByStep({ table, columnId: column.id, direction: -1 })
|
|
263
|
+
}
|
|
264
|
+
>
|
|
265
|
+
Move left
|
|
266
|
+
</Menu.Item>,
|
|
267
|
+
<Menu.Item
|
|
268
|
+
key="move-right"
|
|
269
|
+
disabled={!next}
|
|
270
|
+
leftSection={<MoveRightIcon size={16} stroke={1.6} />}
|
|
271
|
+
onClick={() =>
|
|
272
|
+
moveColumnByStep({ table, columnId: column.id, direction: 1 })
|
|
273
|
+
}
|
|
274
|
+
>
|
|
275
|
+
Move right
|
|
276
|
+
</Menu.Item>,
|
|
277
|
+
<Menu.Divider key="move-divider" />,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (canHide) {
|
|
282
|
+
menuItems.push(
|
|
283
|
+
<Menu.Item
|
|
284
|
+
key="hide"
|
|
285
|
+
leftSection={<EyeOffIcon size={16} stroke={1.6} />}
|
|
286
|
+
onClick={() => column.toggleVisibility(false)}
|
|
287
|
+
>
|
|
288
|
+
Hide column
|
|
289
|
+
</Menu.Item>,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
if (canManageColumns) {
|
|
293
|
+
menuItems.push(
|
|
294
|
+
<Menu.Item
|
|
295
|
+
key="manage"
|
|
296
|
+
leftSection={<ColumnsIcon size={16} stroke={1.6} />}
|
|
297
|
+
onClick={() => api.ui.actions.setColumnsPanelOpen(true)}
|
|
298
|
+
>
|
|
299
|
+
Manage columns
|
|
300
|
+
</Menu.Item>,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
// A trailing divider reads as a dropped item — drop the divider instead.
|
|
304
|
+
while (menuItems.length > 0 && isDivider(menuItems.at(-1))) {
|
|
305
|
+
menuItems.pop();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const cellClass = [
|
|
309
|
+
classes.headerCell,
|
|
310
|
+
canSort ? classes.headerCellSortable : "",
|
|
311
|
+
isDragged ? classes.headerCellDragging : "",
|
|
312
|
+
dropSide === "before" ? classes.headerCellDropBefore : "",
|
|
313
|
+
dropSide === "after" ? classes.headerCellDropAfter : "",
|
|
314
|
+
layout.isBoundary && pinnedAt === "left" ? classes.stickyLeft : "",
|
|
315
|
+
layout.isBoundary && pinnedAt === "right" ? classes.stickyRight : "",
|
|
316
|
+
]
|
|
317
|
+
.filter(Boolean)
|
|
318
|
+
.join(" ");
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<div
|
|
322
|
+
ref={cellRef}
|
|
323
|
+
role="columnheader"
|
|
324
|
+
data-testid={`dg-header-${column.id}`}
|
|
325
|
+
data-active={isSorted || isFiltered}
|
|
326
|
+
data-align={align}
|
|
327
|
+
className={cellClass}
|
|
328
|
+
// Dropped while a resize is running, so that dragging the separator
|
|
329
|
+
// resizes the column instead of starting to move it. The resize handler
|
|
330
|
+
// records the column on mousedown, before the first pointer move.
|
|
331
|
+
draggable={canReorder && !isResizing}
|
|
332
|
+
onDragStart={canReorder ? handleDragStart : undefined}
|
|
333
|
+
onDragEnd={canReorder ? handleDragEnd : undefined}
|
|
334
|
+
onDragOver={isDropTarget ? handleDragOver : undefined}
|
|
335
|
+
onDragLeave={isDropTarget ? handleDragLeave : undefined}
|
|
336
|
+
onDrop={isDropTarget ? handleDrop : undefined}
|
|
337
|
+
style={{
|
|
338
|
+
left: pinnedAt === "left" ? layout.offset : undefined,
|
|
339
|
+
right: pinnedAt === "right" ? layout.offset : undefined,
|
|
340
|
+
position: pinnedAt ? "sticky" : undefined,
|
|
341
|
+
zIndex: pinnedAt ? 4 : undefined,
|
|
342
|
+
}}
|
|
343
|
+
onClick={
|
|
344
|
+
canSort
|
|
345
|
+
? (event) => {
|
|
346
|
+
if (suppressSortRef.current) {
|
|
347
|
+
suppressSortRef.current = false;
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
column.getToggleSortingHandler()?.(event);
|
|
351
|
+
}
|
|
352
|
+
: undefined
|
|
353
|
+
}
|
|
354
|
+
>
|
|
355
|
+
{header.isPlaceholder ? null : (
|
|
356
|
+
<Tooltip label={label} openDelay={600} withinPortal disabled={!isLeaf}>
|
|
357
|
+
<span className={classes.headerTitle}>
|
|
358
|
+
{flexRender(column.columnDef.header, header.getContext())}
|
|
359
|
+
</span>
|
|
360
|
+
</Tooltip>
|
|
361
|
+
)}
|
|
362
|
+
|
|
363
|
+
{isLeaf && (
|
|
364
|
+
<div className={classes.headerActions}>
|
|
365
|
+
{isFiltered && (
|
|
366
|
+
<ActionIcon
|
|
367
|
+
className={classes.headerAction}
|
|
368
|
+
data-pinned-visible="true"
|
|
369
|
+
variant="subtle"
|
|
370
|
+
color="gray"
|
|
371
|
+
size="xs"
|
|
372
|
+
aria-label={`Filter on ${label}`}
|
|
373
|
+
onClick={(event) => {
|
|
374
|
+
event.stopPropagation();
|
|
375
|
+
openColumnFilter(api, column.id);
|
|
376
|
+
}}
|
|
377
|
+
>
|
|
378
|
+
<FilterIcon size={14} stroke={1.6} />
|
|
379
|
+
</ActionIcon>
|
|
380
|
+
)}
|
|
381
|
+
|
|
382
|
+
{canSort && (
|
|
383
|
+
<ActionIcon
|
|
384
|
+
className={classes.headerAction}
|
|
385
|
+
data-pinned-visible={isSorted}
|
|
386
|
+
variant="subtle"
|
|
387
|
+
color="gray"
|
|
388
|
+
size="xs"
|
|
389
|
+
aria-label={`Sort ${label}`}
|
|
390
|
+
onClick={column.getToggleSortingHandler()}
|
|
391
|
+
>
|
|
392
|
+
{sortDirection ? (
|
|
393
|
+
<ArrowDownIcon size={14} stroke={1.6} />
|
|
394
|
+
) : (
|
|
395
|
+
<ArrowUpIcon size={14} stroke={1.6} />
|
|
396
|
+
)}
|
|
397
|
+
</ActionIcon>
|
|
398
|
+
)}
|
|
399
|
+
|
|
400
|
+
{menuItems.length > 0 && column.id !== SELECT_COLUMN_ID && (
|
|
401
|
+
<Menu
|
|
402
|
+
opened={menuOpened}
|
|
403
|
+
onChange={setMenuOpened}
|
|
404
|
+
position="bottom-start"
|
|
405
|
+
shadow="md"
|
|
406
|
+
width={220}
|
|
407
|
+
withinPortal
|
|
408
|
+
>
|
|
409
|
+
<Menu.Target>
|
|
410
|
+
<ActionIcon
|
|
411
|
+
className={classes.headerAction}
|
|
412
|
+
data-pinned-visible={menuOpened}
|
|
413
|
+
variant="subtle"
|
|
414
|
+
color="gray"
|
|
415
|
+
size="xs"
|
|
416
|
+
aria-label={`${label} column menu`}
|
|
417
|
+
onClick={(event) => {
|
|
418
|
+
event.stopPropagation();
|
|
419
|
+
setMenuOpened((opened) => !opened);
|
|
420
|
+
}}
|
|
421
|
+
>
|
|
422
|
+
<DotsVerticalIcon size={14} stroke={1.6} />
|
|
423
|
+
</ActionIcon>
|
|
424
|
+
</Menu.Target>
|
|
425
|
+
<Menu.Dropdown onClick={(event) => event.stopPropagation()}>
|
|
426
|
+
{menuItems}
|
|
427
|
+
</Menu.Dropdown>
|
|
428
|
+
</Menu>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
|
|
433
|
+
<div
|
|
434
|
+
className={[
|
|
435
|
+
classes.columnSeparator,
|
|
436
|
+
canResize ? classes.columnSeparatorResizable : "",
|
|
437
|
+
isResizing ? classes.columnSeparatorActive : "",
|
|
438
|
+
]
|
|
439
|
+
.filter(Boolean)
|
|
440
|
+
.join(" ")}
|
|
441
|
+
// Keeps a drag that starts on the separator from being picked up by the
|
|
442
|
+
// draggable header around it.
|
|
443
|
+
draggable={false}
|
|
444
|
+
onMouseDown={
|
|
445
|
+
canResize
|
|
446
|
+
? (event) => {
|
|
447
|
+
suppressSortRef.current = true;
|
|
448
|
+
header.getResizeHandler()(event);
|
|
449
|
+
}
|
|
450
|
+
: undefined
|
|
451
|
+
}
|
|
452
|
+
onTouchStart={canResize ? header.getResizeHandler() : undefined}
|
|
453
|
+
onClick={(event) => event.stopPropagation()}
|
|
454
|
+
/>
|
|
455
|
+
</div>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function isDivider(node: ReactNode): boolean {
|
|
460
|
+
return (
|
|
461
|
+
typeof node === "object" &&
|
|
462
|
+
node !== null &&
|
|
463
|
+
"type" in node &&
|
|
464
|
+
node.type === Menu.Divider
|
|
465
|
+
);
|
|
466
|
+
}
|