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.
Files changed (41) hide show
  1. package/README.md +3 -3
  2. package/index.cjs +9 -9
  3. package/package.json +1 -1
  4. package/source/components/datagridbeta/README.md +232 -0
  5. package/source/components/datagridbeta/__tests__/core.test.ts +341 -0
  6. package/source/components/datagridbeta/__tests__/export.test.ts +70 -0
  7. package/source/components/datagridbeta/core/columnHelper.ts +21 -0
  8. package/source/components/datagridbeta/core/columns.ts +288 -0
  9. package/source/components/datagridbeta/core/filtering.ts +237 -0
  10. package/source/components/datagridbeta/core/grid.ts +616 -0
  11. package/source/components/datagridbeta/core/index.ts +14 -0
  12. package/source/components/datagridbeta/core/rows.ts +64 -0
  13. package/source/components/datagridbeta/core/sorting.ts +111 -0
  14. package/source/components/datagridbeta/core/state.ts +82 -0
  15. package/source/components/datagridbeta/core/store.ts +52 -0
  16. package/source/components/datagridbeta/core/types.ts +266 -0
  17. package/source/components/datagridbeta/core/values.ts +98 -0
  18. package/source/components/datagridbeta/core/virtual.ts +85 -0
  19. package/source/components/datagridbeta/export/csv.ts +32 -0
  20. package/source/components/datagridbeta/export/download.ts +12 -0
  21. package/source/components/datagridbeta/export/pdf.ts +61 -0
  22. package/source/components/datagridbeta/export/table.ts +73 -0
  23. package/source/components/datagridbeta/export/xlsx.ts +145 -0
  24. package/source/components/datagridbeta/export/zip.ts +87 -0
  25. package/source/components/datagridbeta/index.ts +7 -0
  26. package/source/components/datagridbeta/react/Cell.tsx +282 -0
  27. package/source/components/datagridbeta/react/ColumnMenu.tsx +135 -0
  28. package/source/components/datagridbeta/react/DataGrid.tsx +296 -0
  29. package/source/components/datagridbeta/react/FilterForm.tsx +142 -0
  30. package/source/components/datagridbeta/react/HeaderRow.tsx +306 -0
  31. package/source/components/datagridbeta/react/Pagination.tsx +107 -0
  32. package/source/components/datagridbeta/react/Popover.tsx +79 -0
  33. package/source/components/datagridbeta/react/Row.tsx +85 -0
  34. package/source/components/datagridbeta/react/Toolbar.tsx +266 -0
  35. package/source/components/datagridbeta/react/Viewport.tsx +167 -0
  36. package/source/components/datagridbeta/react/context.ts +57 -0
  37. package/source/components/datagridbeta/react/hooks.ts +80 -0
  38. package/source/components/datagridbeta/react/icons.tsx +72 -0
  39. package/source/components/datagridbeta/react/keyboard.ts +110 -0
  40. package/source/components/datagridbeta/react/locale.ts +128 -0
  41. 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;