gbs-add-block 1.2.7 → 1.2.8
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/README.md +3 -3
- package/index.cjs +9 -9
- package/package.json +1 -1
- package/source/components/datagridbeta/README.md +232 -0
- package/source/components/datagridbeta/__tests__/core.test.ts +341 -0
- package/source/components/datagridbeta/__tests__/export.test.ts +70 -0
- package/source/components/datagridbeta/core/columnHelper.ts +21 -0
- package/source/components/datagridbeta/core/columns.ts +288 -0
- package/source/components/datagridbeta/core/filtering.ts +237 -0
- package/source/components/datagridbeta/core/grid.ts +616 -0
- package/source/components/datagridbeta/core/index.ts +14 -0
- package/source/components/datagridbeta/core/rows.ts +64 -0
- package/source/components/datagridbeta/core/sorting.ts +111 -0
- package/source/components/datagridbeta/core/state.ts +82 -0
- package/source/components/datagridbeta/core/store.ts +52 -0
- package/source/components/datagridbeta/core/types.ts +266 -0
- package/source/components/datagridbeta/core/values.ts +98 -0
- package/source/components/datagridbeta/core/virtual.ts +85 -0
- package/source/components/datagridbeta/export/csv.ts +32 -0
- package/source/components/datagridbeta/export/download.ts +12 -0
- package/source/components/datagridbeta/export/pdf.ts +61 -0
- package/source/components/datagridbeta/export/table.ts +73 -0
- package/source/components/datagridbeta/export/xlsx.ts +145 -0
- package/source/components/datagridbeta/export/zip.ts +87 -0
- package/source/components/datagridbeta/index.ts +7 -0
- package/source/components/datagridbeta/react/Cell.tsx +282 -0
- package/source/components/datagridbeta/react/ColumnMenu.tsx +135 -0
- package/source/components/datagridbeta/react/DataGrid.tsx +296 -0
- package/source/components/datagridbeta/react/FilterForm.tsx +142 -0
- package/source/components/datagridbeta/react/HeaderRow.tsx +306 -0
- package/source/components/datagridbeta/react/Pagination.tsx +107 -0
- package/source/components/datagridbeta/react/Popover.tsx +79 -0
- package/source/components/datagridbeta/react/Row.tsx +85 -0
- package/source/components/datagridbeta/react/Toolbar.tsx +266 -0
- package/source/components/datagridbeta/react/Viewport.tsx +167 -0
- package/source/components/datagridbeta/react/context.ts +57 -0
- package/source/components/datagridbeta/react/hooks.ts +80 -0
- package/source/components/datagridbeta/react/icons.tsx +72 -0
- package/source/components/datagridbeta/react/keyboard.ts +110 -0
- package/source/components/datagridbeta/react/locale.ts +128 -0
- package/source/components/datagridbeta/styles.css +717 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
type DragEvent,
|
|
7
|
+
type KeyboardEvent,
|
|
8
|
+
type MouseEvent,
|
|
9
|
+
type PointerEvent,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { isFilterActive } from "../core/filtering";
|
|
13
|
+
import type { ColumnFilter, ColumnLayout, ColumnLayoutItem, GridRow, RowId, SortItem } from "../core/types";
|
|
14
|
+
import type { Range } from "../core/virtual";
|
|
15
|
+
import { cx, useGridContext } from "./context";
|
|
16
|
+
import { useFocusWhenActive, useSelectionState, useStoreSelector } from "./hooks";
|
|
17
|
+
import { ArrowDownIcon, ArrowUpIcon, MoreIcon } from "./icons";
|
|
18
|
+
|
|
19
|
+
const NO_ACTIVE_CELL = -2;
|
|
20
|
+
|
|
21
|
+
interface HeaderRowProps<T> {
|
|
22
|
+
layout: ColumnLayout<T>;
|
|
23
|
+
colRange: Range;
|
|
24
|
+
sorting: SortItem[];
|
|
25
|
+
filters: ColumnFilter[];
|
|
26
|
+
/** Rows the select-all checkbox acts on. */
|
|
27
|
+
rows: GridRow<T>[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function HeaderRow<T>({ layout, colRange, sorting, filters, rows }: HeaderRowProps<T>) {
|
|
31
|
+
const { engine, classNames } = useGridContext<T>();
|
|
32
|
+
const activeCol = useStoreSelector(engine.store, (s) => {
|
|
33
|
+
const active = s.ui.activeCell;
|
|
34
|
+
return active === null ? NO_ACTIVE_CELL : active.rowIndex === -1 ? active.colIndex : -1;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const render = (item: ColumnLayoutItem<T>): ReactNode => {
|
|
38
|
+
const active = activeCol === item.visibleIndex;
|
|
39
|
+
// Before anything is focused, the first header cell is the grid's tab stop.
|
|
40
|
+
const tabbable = active || (activeCol === NO_ACTIVE_CELL && item.visibleIndex === 0);
|
|
41
|
+
if (item.column.internal) {
|
|
42
|
+
return (
|
|
43
|
+
<SelectAllHeaderCell key={item.column.id} item={item} rows={rows} active={active} tabbable={tabbable} />
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const sortIndex = sorting.findIndex((s) => s.columnId === item.column.id);
|
|
47
|
+
return (
|
|
48
|
+
<HeaderCell
|
|
49
|
+
key={item.column.id}
|
|
50
|
+
item={item}
|
|
51
|
+
active={active}
|
|
52
|
+
tabbable={tabbable}
|
|
53
|
+
sort={sortIndex === -1 ? undefined : sorting[sortIndex]}
|
|
54
|
+
sortOrder={sorting.length > 1 ? sortIndex + 1 : 0}
|
|
55
|
+
filtered={filters.some((f) => f.columnId === item.column.id && isFilterActive(f))}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div role="rowgroup" className="dg-header-group">
|
|
62
|
+
<div role="row" aria-rowindex={1} className={cx("dg-header", classNames.header)} data-row-index={-1}>
|
|
63
|
+
{layout.left.map(render)}
|
|
64
|
+
{colRange.start > 0 && (
|
|
65
|
+
<div className="dg-spacer" style={{ width: layout.center[colRange.start].offset }} aria-hidden="true" />
|
|
66
|
+
)}
|
|
67
|
+
{layout.center.slice(colRange.start, colRange.end).map(render)}
|
|
68
|
+
<div className="dg-spacer dg-spacer-fill" aria-hidden="true" />
|
|
69
|
+
{layout.right.map(render)}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface HeaderCellProps<T> {
|
|
76
|
+
item: ColumnLayoutItem<T>;
|
|
77
|
+
active: boolean;
|
|
78
|
+
tabbable: boolean;
|
|
79
|
+
sort: SortItem | undefined;
|
|
80
|
+
/** 1-based position in a multi-column sort, 0 otherwise. */
|
|
81
|
+
sortOrder: number;
|
|
82
|
+
filtered: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function HeaderCell<T>({ item, active, tabbable, sort, sortOrder, filtered }: HeaderCellProps<T>) {
|
|
86
|
+
const { engine, locale, classNames, features, openColumnMenu } = useGridContext<T>();
|
|
87
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
88
|
+
useFocusWhenActive(ref, active);
|
|
89
|
+
|
|
90
|
+
const { column } = item;
|
|
91
|
+
const sortable = features.sorting && column.sortable;
|
|
92
|
+
const resizable = features.resizing && column.resizable;
|
|
93
|
+
const reorderable = features.reordering && column.reorderable;
|
|
94
|
+
const hasMenu =
|
|
95
|
+
sortable ||
|
|
96
|
+
reorderable ||
|
|
97
|
+
(features.filtering && column.filterable) ||
|
|
98
|
+
(features.pinning && column.pinnable) ||
|
|
99
|
+
(features.hiding && column.hideable);
|
|
100
|
+
|
|
101
|
+
const onClick = (event: MouseEvent<HTMLDivElement>) => {
|
|
102
|
+
engine.setActiveCell(-1, item.visibleIndex, { focus: false, scroll: false });
|
|
103
|
+
const fromControl = (event.target as HTMLElement).closest("button, .dg-resize-handle");
|
|
104
|
+
if (sortable && !fromControl) engine.api.toggleSort(column.id, event.shiftKey);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
108
|
+
if (event.target !== event.currentTarget || !hasMenu) return;
|
|
109
|
+
if ((event.altKey && event.key === "ArrowDown") || event.key === "ContextMenu") {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
event.stopPropagation();
|
|
112
|
+
openColumnMenu(column.id, event.currentTarget);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const startResize = (event: PointerEvent<HTMLDivElement>) => {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
event.stopPropagation();
|
|
119
|
+
const handle = event.currentTarget;
|
|
120
|
+
const root = handle.closest<HTMLElement>(".dg-root");
|
|
121
|
+
const direction = getComputedStyle(handle).direction === "rtl" ? -1 : 1;
|
|
122
|
+
const startX = event.clientX;
|
|
123
|
+
let width = item.width;
|
|
124
|
+
|
|
125
|
+
handle.setPointerCapture(event.pointerId);
|
|
126
|
+
root?.setAttribute("data-resizing", "");
|
|
127
|
+
|
|
128
|
+
const onMove = (move: globalThis.PointerEvent) => {
|
|
129
|
+
const next = item.width + (move.clientX - startX) * direction;
|
|
130
|
+
width = Math.round(Math.min(column.maxWidth, Math.max(column.minWidth, next)));
|
|
131
|
+
engine.previewColumnWidth(column.id, width);
|
|
132
|
+
};
|
|
133
|
+
const onEnd = () => {
|
|
134
|
+
handle.removeEventListener("pointermove", onMove);
|
|
135
|
+
handle.removeEventListener("pointerup", onEnd);
|
|
136
|
+
handle.removeEventListener("pointercancel", onEnd);
|
|
137
|
+
root?.removeAttribute("data-resizing");
|
|
138
|
+
if (width !== item.width) engine.api.setColumnWidth(column.id, width);
|
|
139
|
+
};
|
|
140
|
+
handle.addEventListener("pointermove", onMove);
|
|
141
|
+
handle.addEventListener("pointerup", onEnd);
|
|
142
|
+
handle.addEventListener("pointercancel", onEnd);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const onDragStart = (event: DragEvent<HTMLDivElement>) => {
|
|
146
|
+
if ((event.target as HTMLElement).closest(".dg-resize-handle")) {
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
engine.setDraggingColumn(column.id);
|
|
151
|
+
event.dataTransfer.effectAllowed = "move";
|
|
152
|
+
event.dataTransfer.setData("text/plain", column.header);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const onDragOver = (event: DragEvent<HTMLDivElement>) => {
|
|
156
|
+
const dragging = engine.getDraggingColumn();
|
|
157
|
+
if (!dragging || dragging === column.id) return;
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
event.dataTransfer.dropEffect = "move";
|
|
160
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
161
|
+
const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
|
|
162
|
+
const firstHalf = event.clientX < rect.left + rect.width / 2;
|
|
163
|
+
event.currentTarget.dataset.drop = firstHalf !== rtl ? "before" : "after";
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const onDragLeave = (event: DragEvent<HTMLDivElement>) => {
|
|
167
|
+
delete event.currentTarget.dataset.drop;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const onDrop = (event: DragEvent<HTMLDivElement>) => {
|
|
171
|
+
const dragging = engine.getDraggingColumn();
|
|
172
|
+
const placement = event.currentTarget.dataset.drop;
|
|
173
|
+
delete event.currentTarget.dataset.drop;
|
|
174
|
+
if (!dragging || (placement !== "before" && placement !== "after")) return;
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
engine.api.moveColumn(dragging, column.id, placement);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div
|
|
181
|
+
ref={ref}
|
|
182
|
+
role="columnheader"
|
|
183
|
+
aria-colindex={item.visibleIndex + 1}
|
|
184
|
+
aria-sort={sort ? (sort.desc ? "descending" : "ascending") : sortable ? "none" : undefined}
|
|
185
|
+
tabIndex={tabbable ? 0 : -1}
|
|
186
|
+
className={cx("dg-header-cell", classNames.headerCell, column.def.headerClassName)}
|
|
187
|
+
style={item.style}
|
|
188
|
+
data-col-index={item.visibleIndex}
|
|
189
|
+
data-pinned={item.pinned || undefined}
|
|
190
|
+
data-pin-edge={item.pinEdge || undefined}
|
|
191
|
+
data-align={column.align}
|
|
192
|
+
data-sortable={sortable || undefined}
|
|
193
|
+
data-active={active || undefined}
|
|
194
|
+
draggable={reorderable}
|
|
195
|
+
onClick={onClick}
|
|
196
|
+
onKeyDown={onKeyDown}
|
|
197
|
+
onDragStart={reorderable ? onDragStart : undefined}
|
|
198
|
+
onDragOver={reorderable ? onDragOver : undefined}
|
|
199
|
+
onDragLeave={reorderable ? onDragLeave : undefined}
|
|
200
|
+
onDrop={reorderable ? onDrop : undefined}
|
|
201
|
+
onDragEnd={reorderable ? () => engine.setDraggingColumn(null) : undefined}
|
|
202
|
+
>
|
|
203
|
+
<span className="dg-header-label" title={column.header}>
|
|
204
|
+
{column.header}
|
|
205
|
+
</span>
|
|
206
|
+
{sort && (
|
|
207
|
+
<span className="dg-sort-indicator" aria-hidden="true">
|
|
208
|
+
{sort.desc ? <ArrowDownIcon width={14} height={14} /> : <ArrowUpIcon width={14} height={14} />}
|
|
209
|
+
{sortOrder > 0 && <span className="dg-sort-order">{sortOrder}</span>}
|
|
210
|
+
</span>
|
|
211
|
+
)}
|
|
212
|
+
{filtered && (
|
|
213
|
+
<>
|
|
214
|
+
<span className="dg-filter-dot" aria-hidden="true" />
|
|
215
|
+
<span className="dg-sr-only">{locale.filtered}</span>
|
|
216
|
+
</>
|
|
217
|
+
)}
|
|
218
|
+
{hasMenu && (
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
tabIndex={-1}
|
|
222
|
+
className="dg-icon-button dg-header-menu"
|
|
223
|
+
aria-label={locale.columnMenu(column.header)}
|
|
224
|
+
aria-haspopup="dialog"
|
|
225
|
+
onClick={(event) => {
|
|
226
|
+
event.stopPropagation();
|
|
227
|
+
openColumnMenu(column.id, event.currentTarget);
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
<MoreIcon />
|
|
231
|
+
</button>
|
|
232
|
+
)}
|
|
233
|
+
{resizable && (
|
|
234
|
+
<div
|
|
235
|
+
className="dg-resize-handle"
|
|
236
|
+
aria-hidden="true"
|
|
237
|
+
draggable={false}
|
|
238
|
+
onPointerDown={startResize}
|
|
239
|
+
onClick={(event) => event.stopPropagation()}
|
|
240
|
+
onDoubleClick={(event) => {
|
|
241
|
+
event.stopPropagation();
|
|
242
|
+
engine.api.setColumnWidth(column.id, null);
|
|
243
|
+
}}
|
|
244
|
+
/>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
interface SelectAllHeaderCellProps<T> {
|
|
251
|
+
item: ColumnLayoutItem<T>;
|
|
252
|
+
rows: GridRow<T>[];
|
|
253
|
+
active: boolean;
|
|
254
|
+
tabbable: boolean;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function SelectAllHeaderCell<T>({ item, rows, active, tabbable }: SelectAllHeaderCellProps<T>) {
|
|
258
|
+
const { engine, locale, classNames, features, isRowSelectable } = useGridContext<T>();
|
|
259
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
260
|
+
useFocusWhenActive(ref, active);
|
|
261
|
+
|
|
262
|
+
const selection = useSelectionState<T, Record<RowId, boolean>>((s) => s);
|
|
263
|
+
const status = useMemo(() => {
|
|
264
|
+
let selectable = 0;
|
|
265
|
+
let selected = 0;
|
|
266
|
+
for (const row of rows) {
|
|
267
|
+
if (!isRowSelectable(row.original)) continue;
|
|
268
|
+
selectable++;
|
|
269
|
+
if (selection[row.id]) selected++;
|
|
270
|
+
}
|
|
271
|
+
return selected === 0 ? "none" : selected === selectable ? "all" : "some";
|
|
272
|
+
}, [rows, selection, isRowSelectable]);
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<div
|
|
276
|
+
ref={ref}
|
|
277
|
+
role="columnheader"
|
|
278
|
+
aria-colindex={item.visibleIndex + 1}
|
|
279
|
+
tabIndex={tabbable ? 0 : -1}
|
|
280
|
+
className={cx("dg-header-cell", classNames.headerCell)}
|
|
281
|
+
style={item.style}
|
|
282
|
+
data-col-index={item.visibleIndex}
|
|
283
|
+
data-pinned="left"
|
|
284
|
+
data-pin-edge={item.pinEdge || undefined}
|
|
285
|
+
data-align="center"
|
|
286
|
+
data-active={active || undefined}
|
|
287
|
+
onClick={() => engine.setActiveCell(-1, item.visibleIndex, { focus: false, scroll: false })}
|
|
288
|
+
>
|
|
289
|
+
{features.selectionMode === "multiple" ? (
|
|
290
|
+
<input
|
|
291
|
+
type="checkbox"
|
|
292
|
+
className="dg-checkbox"
|
|
293
|
+
tabIndex={-1}
|
|
294
|
+
aria-label={locale.selectAllRows}
|
|
295
|
+
checked={status === "all"}
|
|
296
|
+
ref={(el) => {
|
|
297
|
+
if (el) el.indeterminate = status === "some";
|
|
298
|
+
}}
|
|
299
|
+
onChange={() => engine.api.toggleAllRowsSelected(status !== "all")}
|
|
300
|
+
/>
|
|
301
|
+
) : (
|
|
302
|
+
<span className="dg-sr-only">{locale.selectRow}</span>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { KeyboardEvent, FocusEvent } from "react";
|
|
4
|
+
import type { PageResult } from "../core/rows";
|
|
5
|
+
import { cx, useGridContext } from "./context";
|
|
6
|
+
import { ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon } from "./icons";
|
|
7
|
+
|
|
8
|
+
interface PaginationProps<T> {
|
|
9
|
+
page: PageResult<T>;
|
|
10
|
+
pageSize: number;
|
|
11
|
+
pageSizeOptions: number[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Pagination<T>({ page, pageSize, pageSizeOptions }: PaginationProps<T>) {
|
|
15
|
+
const { engine, locale, classNames, numberFormat } = useGridContext<T>();
|
|
16
|
+
const { api } = engine;
|
|
17
|
+
const { pageIndex, pageCount, rowCount, pageOffset } = page;
|
|
18
|
+
const format = (n: number) => numberFormat.format(n);
|
|
19
|
+
|
|
20
|
+
const from = rowCount === 0 ? 0 : pageOffset + 1;
|
|
21
|
+
const to = Math.min(rowCount, pageOffset + page.rows.length);
|
|
22
|
+
const sizes = pageSizeOptions.includes(pageSize)
|
|
23
|
+
? pageSizeOptions
|
|
24
|
+
: [...pageSizeOptions, pageSize].sort((a, b) => a - b);
|
|
25
|
+
|
|
26
|
+
const goToInput = (input: HTMLInputElement) => {
|
|
27
|
+
const target = Math.min(pageCount, Math.max(1, Math.round(Number(input.value)) || 1));
|
|
28
|
+
input.value = String(target);
|
|
29
|
+
api.setPageIndex(target - 1);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<nav className={cx("dg-pagination", classNames.pagination)} aria-label={locale.pagination}>
|
|
34
|
+
<label className="dg-page-size">
|
|
35
|
+
<span>{locale.rowsPerPage}</span>
|
|
36
|
+
<select className="dg-select" value={pageSize} onChange={(e) => api.setPageSize(Number(e.target.value))}>
|
|
37
|
+
{sizes.map((size) => (
|
|
38
|
+
<option key={size} value={size}>
|
|
39
|
+
{size}
|
|
40
|
+
</option>
|
|
41
|
+
))}
|
|
42
|
+
</select>
|
|
43
|
+
</label>
|
|
44
|
+
|
|
45
|
+
<span className="dg-page-range" aria-live="polite">
|
|
46
|
+
{locale.pageRange(format(from), format(to), format(rowCount))}
|
|
47
|
+
</span>
|
|
48
|
+
|
|
49
|
+
<div className="dg-page-controls">
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
className="dg-icon-button"
|
|
53
|
+
aria-label={locale.firstPage}
|
|
54
|
+
disabled={pageIndex === 0}
|
|
55
|
+
onClick={() => api.setPageIndex(0)}
|
|
56
|
+
>
|
|
57
|
+
<ChevronsLeftIcon className="dg-flip-rtl" />
|
|
58
|
+
</button>
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
className="dg-icon-button"
|
|
62
|
+
aria-label={locale.previousPage}
|
|
63
|
+
disabled={pageIndex === 0}
|
|
64
|
+
onClick={() => api.setPageIndex(pageIndex - 1)}
|
|
65
|
+
>
|
|
66
|
+
<ChevronLeftIcon className="dg-flip-rtl" />
|
|
67
|
+
</button>
|
|
68
|
+
|
|
69
|
+
<label className="dg-page-input">
|
|
70
|
+
<span className="dg-sr-only">{locale.page}</span>
|
|
71
|
+
<input
|
|
72
|
+
key={pageIndex}
|
|
73
|
+
className="dg-input"
|
|
74
|
+
type="number"
|
|
75
|
+
min={1}
|
|
76
|
+
max={pageCount}
|
|
77
|
+
defaultValue={pageIndex + 1}
|
|
78
|
+
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
|
|
79
|
+
if (e.key === "Enter") goToInput(e.currentTarget);
|
|
80
|
+
}}
|
|
81
|
+
onBlur={(e: FocusEvent<HTMLInputElement>) => goToInput(e.currentTarget)}
|
|
82
|
+
/>
|
|
83
|
+
</label>
|
|
84
|
+
<span className="dg-page-count">{locale.pageOf(format(pageCount))}</span>
|
|
85
|
+
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
className="dg-icon-button"
|
|
89
|
+
aria-label={locale.nextPage}
|
|
90
|
+
disabled={pageIndex >= pageCount - 1}
|
|
91
|
+
onClick={() => api.setPageIndex(pageIndex + 1)}
|
|
92
|
+
>
|
|
93
|
+
<ChevronRightIcon className="dg-flip-rtl" />
|
|
94
|
+
</button>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
className="dg-icon-button"
|
|
98
|
+
aria-label={locale.lastPage}
|
|
99
|
+
disabled={pageIndex >= pageCount - 1}
|
|
100
|
+
onClick={() => api.setPageIndex(pageCount - 1)}
|
|
101
|
+
>
|
|
102
|
+
<ChevronsRightIcon className="dg-flip-rtl" />
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
</nav>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useLayoutEffect, useRef, type ReactNode, type ToggleEvent } from "react";
|
|
4
|
+
import { cx } from "./context";
|
|
5
|
+
|
|
6
|
+
interface PopoverProps {
|
|
7
|
+
anchor: HTMLElement;
|
|
8
|
+
onClose(): void;
|
|
9
|
+
label: string;
|
|
10
|
+
align?: "start" | "end";
|
|
11
|
+
className?: string;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Anchored popover on the native Popover API: rendered in the top layer (never
|
|
17
|
+
* clipped by the grid's scroll containers), with light dismiss and Escape.
|
|
18
|
+
* Mount it to open, unmount it to close.
|
|
19
|
+
*/
|
|
20
|
+
export function Popover({ anchor, onClose, label, align = "start", className, children }: PopoverProps) {
|
|
21
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
22
|
+
|
|
23
|
+
useLayoutEffect(() => {
|
|
24
|
+
const el = ref.current;
|
|
25
|
+
if (!el) return;
|
|
26
|
+
|
|
27
|
+
const position = () => {
|
|
28
|
+
const rect = anchor.getBoundingClientRect();
|
|
29
|
+
const rtl = getComputedStyle(anchor).direction === "rtl";
|
|
30
|
+
const alignEnd = (align === "end") !== rtl;
|
|
31
|
+
const { offsetWidth: width, offsetHeight: height } = el;
|
|
32
|
+
const left = Math.min(
|
|
33
|
+
Math.max(8, alignEnd ? rect.right - width : rect.left),
|
|
34
|
+
window.innerWidth - width - 8,
|
|
35
|
+
);
|
|
36
|
+
let top = rect.bottom + 4;
|
|
37
|
+
if (top + height > window.innerHeight - 8 && rect.top - height - 4 >= 8) {
|
|
38
|
+
top = rect.top - height - 4;
|
|
39
|
+
}
|
|
40
|
+
el.style.left = `${Math.max(8, left)}px`;
|
|
41
|
+
el.style.top = `${Math.max(8, top)}px`;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
el.showPopover();
|
|
45
|
+
position();
|
|
46
|
+
el.querySelector<HTMLElement>("[data-autofocus], button, input, select, textarea")?.focus({
|
|
47
|
+
preventScroll: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
window.addEventListener("resize", position);
|
|
51
|
+
window.addEventListener("scroll", position, true);
|
|
52
|
+
return () => {
|
|
53
|
+
window.removeEventListener("resize", position);
|
|
54
|
+
window.removeEventListener("scroll", position, true);
|
|
55
|
+
const hadFocus = el.contains(document.activeElement);
|
|
56
|
+
if (el.matches(":popover-open")) el.hidePopover();
|
|
57
|
+
if ((hadFocus || document.activeElement === document.body) && anchor.isConnected) {
|
|
58
|
+
anchor.focus({ preventScroll: true });
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}, [anchor, align]);
|
|
62
|
+
|
|
63
|
+
const onToggle = (event: ToggleEvent<HTMLDivElement>) => {
|
|
64
|
+
if (event.newState === "closed") onClose();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
ref={ref}
|
|
70
|
+
popover="auto"
|
|
71
|
+
role="dialog"
|
|
72
|
+
aria-label={label}
|
|
73
|
+
className={cx("dg-popover", className)}
|
|
74
|
+
onToggle={onToggle}
|
|
75
|
+
>
|
|
76
|
+
{children}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { memo, type MouseEvent, type ReactNode } from "react";
|
|
4
|
+
import type { ColumnLayout, ColumnLayoutItem, GridRow } from "../core/types";
|
|
5
|
+
import type { Range } from "../core/virtual";
|
|
6
|
+
import { Cell } from "./Cell";
|
|
7
|
+
import { cx, NO_PENDING, useGridContext } from "./context";
|
|
8
|
+
import { useSelectionState, useStoreSelector } from "./hooks";
|
|
9
|
+
|
|
10
|
+
interface RowProps<T> {
|
|
11
|
+
row: GridRow<T>;
|
|
12
|
+
/** Index within the displayed rows. */
|
|
13
|
+
rowIndex: number;
|
|
14
|
+
ariaRowIndex: number;
|
|
15
|
+
top: number;
|
|
16
|
+
layout: ColumnLayout<T>;
|
|
17
|
+
colRange: Range;
|
|
18
|
+
className: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const INTERACTIVE = "button, a, input, select, textarea, label, [contenteditable], [data-dg-interactive], .dg-editor";
|
|
22
|
+
|
|
23
|
+
function RowView<T>({ row, rowIndex, ariaRowIndex, top, layout, colRange, className }: RowProps<T>) {
|
|
24
|
+
const { engine, classNames, features, isRowSelectable } = useGridContext<T>();
|
|
25
|
+
|
|
26
|
+
// Each subscription returns a primitive or a stable reference, so moving the
|
|
27
|
+
// active cell or selecting a row only re-renders the rows involved.
|
|
28
|
+
const activeCol = useStoreSelector(engine.store, (s) =>
|
|
29
|
+
s.ui.activeCell?.rowIndex === rowIndex ? s.ui.activeCell.colIndex : -1,
|
|
30
|
+
);
|
|
31
|
+
const editingColumn = useStoreSelector(engine.store, (s) =>
|
|
32
|
+
s.ui.editing?.rowId === row.id ? s.ui.editing.columnId : null,
|
|
33
|
+
);
|
|
34
|
+
const pending = useStoreSelector(engine.store, (s) => s.ui.pending[row.id]);
|
|
35
|
+
const errors = useStoreSelector(engine.store, (s) => s.ui.errors[row.id]);
|
|
36
|
+
const selected = useSelectionState<T, boolean>((selection) => selection[row.id] === true);
|
|
37
|
+
const selectable = features.selection && isRowSelectable(row.original);
|
|
38
|
+
|
|
39
|
+
const handleClick = (event: MouseEvent<HTMLDivElement>, kind: "click" | "double") => {
|
|
40
|
+
const options = engine.getOptions();
|
|
41
|
+
const handler = kind === "click" ? options.onRowClick : options.onRowDoubleClick;
|
|
42
|
+
if (!handler || (event.target as HTMLElement).closest(INTERACTIVE)) return;
|
|
43
|
+
handler(row.original, event);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const renderCell = (item: ColumnLayoutItem<T>): ReactNode => (
|
|
47
|
+
<Cell
|
|
48
|
+
key={item.column.id}
|
|
49
|
+
row={row}
|
|
50
|
+
rowIndex={rowIndex}
|
|
51
|
+
item={item}
|
|
52
|
+
active={activeCol === item.visibleIndex}
|
|
53
|
+
editing={editingColumn === item.column.id}
|
|
54
|
+
pendingValue={pending && item.column.id in pending ? pending[item.column.id] : NO_PENDING}
|
|
55
|
+
error={errors?.[item.column.id]}
|
|
56
|
+
selected={selected}
|
|
57
|
+
selectable={selectable}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
role="row"
|
|
64
|
+
aria-rowindex={ariaRowIndex}
|
|
65
|
+
aria-selected={features.selection ? selected : undefined}
|
|
66
|
+
className={cx("dg-row", classNames.row, className)}
|
|
67
|
+
style={{ transform: `translateY(${top}px)` }}
|
|
68
|
+
data-row-index={rowIndex}
|
|
69
|
+
data-odd={rowIndex % 2 === 1 || undefined}
|
|
70
|
+
data-selected={selected || undefined}
|
|
71
|
+
onClick={(event) => handleClick(event, "click")}
|
|
72
|
+
onDoubleClick={(event) => handleClick(event, "double")}
|
|
73
|
+
>
|
|
74
|
+
{layout.left.map(renderCell)}
|
|
75
|
+
{colRange.start > 0 && (
|
|
76
|
+
<div className="dg-spacer" style={{ width: layout.center[colRange.start].offset }} aria-hidden="true" />
|
|
77
|
+
)}
|
|
78
|
+
{layout.center.slice(colRange.start, colRange.end).map(renderCell)}
|
|
79
|
+
<div className="dg-spacer dg-spacer-fill" aria-hidden="true" />
|
|
80
|
+
{layout.right.map(renderCell)}
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const Row = memo(RowView) as typeof RowView;
|