@alaarab/ogrid-react-radix 2.0.0-beta
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 +78 -0
- package/dist/esm/ColumnChooser/ColumnChooser.js +22 -0
- package/dist/esm/ColumnChooser/ColumnChooser.module.css +140 -0
- package/dist/esm/ColumnHeaderFilter/ColumnHeaderFilter.js +55 -0
- package/dist/esm/ColumnHeaderFilter/ColumnHeaderFilter.module.css +332 -0
- package/dist/esm/ColumnHeaderFilter/MultiSelectFilterPopover.js +5 -0
- package/dist/esm/ColumnHeaderFilter/PeopleFilterPopover.js +19 -0
- package/dist/esm/ColumnHeaderFilter/TextFilterPopover.js +4 -0
- package/dist/esm/ColumnHeaderFilter/index.js +1 -0
- package/dist/esm/DataGridTable/DataGridTable.js +155 -0
- package/dist/esm/DataGridTable/DataGridTable.module.css +493 -0
- package/dist/esm/DataGridTable/GridContextMenu.js +35 -0
- package/dist/esm/DataGridTable/InlineCellEditor.js +9 -0
- package/dist/esm/DataGridTable/StatusBar.js +7 -0
- package/dist/esm/OGrid/OGrid.js +16 -0
- package/dist/esm/PaginationControls/PaginationControls.js +31 -0
- package/dist/esm/PaginationControls/PaginationControls.module.css +110 -0
- package/dist/esm/index.js +8 -0
- package/dist/types/ColumnChooser/ColumnChooser.d.ts +10 -0
- package/dist/types/ColumnHeaderFilter/ColumnHeaderFilter.d.ts +22 -0
- package/dist/types/ColumnHeaderFilter/MultiSelectFilterPopover.d.ts +14 -0
- package/dist/types/ColumnHeaderFilter/PeopleFilterPopover.d.ts +13 -0
- package/dist/types/ColumnHeaderFilter/TextFilterPopover.d.ts +8 -0
- package/dist/types/ColumnHeaderFilter/index.d.ts +1 -0
- package/dist/types/DataGridTable/DataGridTable.d.ts +5 -0
- package/dist/types/DataGridTable/GridContextMenu.d.ts +10 -0
- package/dist/types/DataGridTable/InlineCellEditor.d.ts +12 -0
- package/dist/types/DataGridTable/StatusBar.d.ts +16 -0
- package/dist/types/OGrid/OGrid.d.ts +5 -0
- package/dist/types/PaginationControls/PaginationControls.d.ts +12 -0
- package/dist/types/index.d.ts +6 -0
- package/package.json +62 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { useCallback, useRef, useMemo } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import * as Popover from '@radix-ui/react-popover';
|
|
6
|
+
import * as Checkbox from '@radix-ui/react-checkbox';
|
|
7
|
+
import { ColumnHeaderFilter } from '../ColumnHeaderFilter';
|
|
8
|
+
import { InlineCellEditor } from './InlineCellEditor';
|
|
9
|
+
import { StatusBar } from './StatusBar';
|
|
10
|
+
import { GridContextMenu } from './GridContextMenu';
|
|
11
|
+
import { useDataGridState, useColumnResize, useLatestRef, getHeaderFilterConfig, getCellRenderDescriptor, buildHeaderRows, MarchingAntsOverlay, resolveCellDisplayContent, resolveCellStyle, buildInlineEditorProps, buildPopoverEditorProps, getCellInteractionProps, areGridRowPropsEqual, CellErrorBoundary, DEFAULT_MIN_COLUMN_WIDTH, } from '@alaarab/ogrid-react';
|
|
12
|
+
import styles from './DataGridTable.module.css';
|
|
13
|
+
// Module-scope stable constants (avoid per-render allocations)
|
|
14
|
+
const GRID_ROOT_STYLE = { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' };
|
|
15
|
+
const CURSOR_CELL_STYLE = { cursor: 'cell' };
|
|
16
|
+
const POPOVER_ANCHOR_STYLE = { minHeight: '100%', minWidth: 40 };
|
|
17
|
+
const STOP_PROPAGATION = (e) => e.stopPropagation();
|
|
18
|
+
const PREVENT_DEFAULT = (e) => { e.preventDefault(); };
|
|
19
|
+
const NOOP = () => { };
|
|
20
|
+
function GridRowInner(props) {
|
|
21
|
+
const { item, rowIndex, rowId, isSelected, visibleCols, columnMeta, renderCellContent, handleSingleRowClick, handleRowCheckboxChange, lastMouseShiftRef, hasCheckboxCol, } = props;
|
|
22
|
+
return (_jsxs("tr", { className: isSelected ? styles.selectedRow : '', "data-row-id": rowId, onClick: handleSingleRowClick, children: [hasCheckboxCol && (_jsx("td", { className: styles.selectionCell, children: _jsx("div", { className: styles.selectionCellInner, "data-row-index": rowIndex, "data-col-index": 0, onClick: STOP_PROPAGATION, children: _jsx(Checkbox.Root, { className: styles.rowCheckbox, checked: isSelected, onCheckedChange: (c) => handleRowCheckboxChange(rowId, !!c, rowIndex, lastMouseShiftRef.current), "aria-label": `Select row ${rowIndex + 1}`, children: _jsx(Checkbox.Indicator, { className: styles.rowCheckboxIndicator, children: "\u2713" }) }) }) })), visibleCols.map((col, colIdx) => (_jsx("td", { className: columnMeta.cellClasses[col.columnId] || undefined, style: columnMeta.cellStyles[col.columnId], children: renderCellContent(item, col, rowIndex, colIdx) }, col.columnId)))] }));
|
|
23
|
+
}
|
|
24
|
+
const GridRow = React.memo(GridRowInner, areGridRowPropsEqual);
|
|
25
|
+
function DataGridTableInner(props) {
|
|
26
|
+
const wrapperRef = useRef(null);
|
|
27
|
+
const tableContainerRef = useRef(null);
|
|
28
|
+
const state = useDataGridState({ props, wrapperRef });
|
|
29
|
+
const lastMouseShiftRef = useRef(false);
|
|
30
|
+
const { layout, rowSelection: rowSel, editing, interaction, contextMenu: ctxMenu, viewModels } = state;
|
|
31
|
+
const { visibleCols, totalColCount, hasCheckboxCol, colOffset, containerWidth, minTableWidth, desiredTableWidth, columnSizingOverrides, setColumnSizingOverrides } = layout;
|
|
32
|
+
const { selectedRowIds, updateSelection, handleRowCheckboxChange, handleSelectAll, allSelected, someSelected } = rowSel;
|
|
33
|
+
const { editingCell, setEditingCell, pendingEditorValue, setPendingEditorValue, commitCellEdit, cancelPopoverEdit, popoverAnchorEl, setPopoverAnchorEl } = editing;
|
|
34
|
+
const { setActiveCell, handleCellMouseDown, handleSelectAllCells, selectionRange, hasCellSelection, handleGridKeyDown, handleFillHandleMouseDown, handleCopy, handleCut, handlePaste, cutRange, copyRange, canUndo, canRedo, onUndo, onRedo, isDragging } = interaction;
|
|
35
|
+
const handlePasteVoid = useCallback(() => { void handlePaste(); }, [handlePaste]);
|
|
36
|
+
const { menuPosition, handleCellContextMenu, closeContextMenu } = ctxMenu;
|
|
37
|
+
const { headerFilterInput, cellDescriptorInput, statusBarConfig, showEmptyInGrid, onCellError } = viewModels;
|
|
38
|
+
const { items, columns, getRowId, emptyState, layoutMode = 'fill', rowSelection = 'none', freezeRows, freezeCols, suppressHorizontalScroll, isLoading = false, loadingMessage = 'Loading\u2026', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, visibleColumns, } = props;
|
|
39
|
+
// Memoize header rows (recursive tree traversal — avoid recomputing every render)
|
|
40
|
+
const headerRows = useMemo(() => buildHeaderRows(columns, visibleColumns), [columns, visibleColumns]);
|
|
41
|
+
const allowOverflowX = !suppressHorizontalScroll && containerWidth > 0 && (minTableWidth > containerWidth || desiredTableWidth > containerWidth);
|
|
42
|
+
const fitToContent = layoutMode === 'content';
|
|
43
|
+
const { handleResizeStart, getColumnWidth } = useColumnResize({
|
|
44
|
+
columnSizingOverrides,
|
|
45
|
+
setColumnSizingOverrides,
|
|
46
|
+
});
|
|
47
|
+
const editCallbacks = useMemo(() => ({ commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit }), [commitCellEdit, setEditingCell, setPendingEditorValue, cancelPopoverEdit]);
|
|
48
|
+
const interactionHandlers = useMemo(() => ({ handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu }), [handleCellMouseDown, setActiveCell, setEditingCell, handleCellContextMenu]);
|
|
49
|
+
// Refs for volatile state — lets renderCellContent be stable (same function ref across
|
|
50
|
+
// selection changes) so that GridRow's React.memo comparator can skip unaffected rows.
|
|
51
|
+
const cellDescriptorInputRef = useLatestRef(cellDescriptorInput);
|
|
52
|
+
const pendingEditorValueRef = useLatestRef(pendingEditorValue);
|
|
53
|
+
const popoverAnchorElRef = useLatestRef(popoverAnchorEl);
|
|
54
|
+
// Pre-compute column styles and classNames (avoids per-cell object creation in the row loop)
|
|
55
|
+
const columnMeta = useMemo(() => {
|
|
56
|
+
const cellStyles = {};
|
|
57
|
+
const cellClasses = {};
|
|
58
|
+
const hdrStyles = {};
|
|
59
|
+
const hdrClasses = {};
|
|
60
|
+
for (let i = 0; i < visibleCols.length; i++) {
|
|
61
|
+
const col = visibleCols[i];
|
|
62
|
+
const columnWidth = getColumnWidth(col);
|
|
63
|
+
const hasExplicitWidth = !!(columnSizingOverrides[col.columnId] || col.idealWidth != null || col.defaultWidth != null);
|
|
64
|
+
const isFreezeCol = freezeCols != null && freezeCols >= 1 && i < freezeCols;
|
|
65
|
+
const isPinnedLeft = col.pinned === 'left';
|
|
66
|
+
const isPinnedRight = col.pinned === 'right';
|
|
67
|
+
cellStyles[col.columnId] = {
|
|
68
|
+
minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
|
|
69
|
+
width: hasExplicitWidth ? columnWidth : undefined,
|
|
70
|
+
maxWidth: hasExplicitWidth ? columnWidth : undefined,
|
|
71
|
+
textAlign: col.type === 'numeric' ? 'right' : col.type === 'boolean' ? 'center' : undefined,
|
|
72
|
+
};
|
|
73
|
+
hdrStyles[col.columnId] = {
|
|
74
|
+
minWidth: col.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
|
|
75
|
+
width: hasExplicitWidth ? columnWidth : undefined,
|
|
76
|
+
maxWidth: hasExplicitWidth ? columnWidth : undefined,
|
|
77
|
+
};
|
|
78
|
+
const parts = [];
|
|
79
|
+
if (isFreezeCol)
|
|
80
|
+
parts.push(styles.freezeCol);
|
|
81
|
+
if (isFreezeCol && i === 0)
|
|
82
|
+
parts.push(styles.freezeColFirst);
|
|
83
|
+
if (isPinnedLeft)
|
|
84
|
+
parts.push(styles.pinnedColLeft);
|
|
85
|
+
if (isPinnedRight)
|
|
86
|
+
parts.push(styles.pinnedColRight);
|
|
87
|
+
const cn = parts.join(' ');
|
|
88
|
+
cellClasses[col.columnId] = cn;
|
|
89
|
+
hdrClasses[col.columnId] = cn;
|
|
90
|
+
}
|
|
91
|
+
return { cellStyles, cellClasses, hdrStyles, hdrClasses };
|
|
92
|
+
}, [visibleCols, getColumnWidth, columnSizingOverrides, freezeCols]);
|
|
93
|
+
// Stable row-click handler (avoids creating a new arrow function per row)
|
|
94
|
+
const selectedRowIdsRef = useLatestRef(selectedRowIds);
|
|
95
|
+
const handleSingleRowClick = useCallback((e) => {
|
|
96
|
+
if (rowSelection !== 'single')
|
|
97
|
+
return;
|
|
98
|
+
const rowId = e.currentTarget.dataset.rowId;
|
|
99
|
+
if (!rowId)
|
|
100
|
+
return;
|
|
101
|
+
const ids = selectedRowIdsRef.current;
|
|
102
|
+
updateSelection(ids.has(rowId) ? new Set() : new Set([rowId]));
|
|
103
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- selectedRowIdsRef is a stable ref
|
|
104
|
+
}, [rowSelection, updateSelection]);
|
|
105
|
+
// Stable header select-all handler
|
|
106
|
+
const handleSelectAllChecked = useCallback((c) => handleSelectAll(!!c), [handleSelectAll]);
|
|
107
|
+
// renderCellContent reads volatile state from refs — keeps function identity stable so
|
|
108
|
+
// GridRow's React.memo comparator can skip rows whose selection state hasn't changed.
|
|
109
|
+
const renderCellContent = useCallback((item, col, rowIndex, colIdx) => {
|
|
110
|
+
const descriptor = getCellRenderDescriptor(item, col, rowIndex, colIdx, cellDescriptorInputRef.current);
|
|
111
|
+
const rowId = getRowId(item);
|
|
112
|
+
let content;
|
|
113
|
+
if (descriptor.mode === 'editing-inline') {
|
|
114
|
+
content = _jsx(InlineCellEditor, { ...buildInlineEditorProps(item, col, descriptor, editCallbacks) });
|
|
115
|
+
}
|
|
116
|
+
else if (descriptor.mode === 'editing-popover' && typeof col.cellEditor === 'function') {
|
|
117
|
+
const editorProps = buildPopoverEditorProps(item, col, descriptor, pendingEditorValueRef.current, editCallbacks);
|
|
118
|
+
const CustomEditor = col.cellEditor;
|
|
119
|
+
content = (_jsxs(Popover.Root, { open: !!popoverAnchorElRef.current, onOpenChange: (open) => { if (!open)
|
|
120
|
+
cancelPopoverEdit(); }, children: [_jsx(Popover.Anchor, { asChild: true, children: _jsx("div", { ref: (el) => el && setPopoverAnchorEl(el), style: POPOVER_ANCHOR_STYLE, "aria-hidden": true }) }), _jsx(Popover.Portal, { children: _jsx(Popover.Content, { sideOffset: 4, onOpenAutoFocus: (e) => e.preventDefault(), children: _jsx(CustomEditor, { ...editorProps }) }) })] }));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const displayContent = resolveCellDisplayContent(col, item, descriptor.displayValue);
|
|
124
|
+
const cellStyle = resolveCellStyle(col, item);
|
|
125
|
+
const styledContent = cellStyle ? _jsx("span", { style: cellStyle, children: displayContent }) : displayContent;
|
|
126
|
+
const cellClassNames = `${styles.cellContent}${descriptor.isActive && !descriptor.isInRange ? ` ${styles.activeCellContent}` : ''}${descriptor.isInRange ? ` ${styles.cellInRange}` : ''}${descriptor.isInCutRange ? ` ${styles.cellCut}` : ''}${descriptor.isInCopyRange ? ` ${styles.cellCopied}` : ''}`;
|
|
127
|
+
const interactionProps = getCellInteractionProps(descriptor, col.columnId, interactionHandlers);
|
|
128
|
+
content = (_jsxs("div", { className: cellClassNames, ...interactionProps, style: descriptor.canEditAny ? CURSOR_CELL_STYLE : undefined, children: [styledContent, descriptor.canEditAny && descriptor.isSelectionEndCell && (_jsx("div", { className: styles.fillHandle, onMouseDown: handleFillHandleMouseDown, "aria-label": "Fill handle" }))] }));
|
|
129
|
+
}
|
|
130
|
+
return (_jsx(CellErrorBoundary, { onError: onCellError, children: content }, `${rowId}-${col.columnId}`));
|
|
131
|
+
},
|
|
132
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- *Ref vars are stable refs from useLatestRef
|
|
133
|
+
[editCallbacks, interactionHandlers, handleFillHandleMouseDown, setPopoverAnchorEl, cancelPopoverEdit, getRowId, onCellError]);
|
|
134
|
+
return (_jsxs("div", { style: GRID_ROOT_STYLE, children: [_jsxs("div", { ref: wrapperRef, tabIndex: 0, onMouseDown: (e) => { lastMouseShiftRef.current = e.shiftKey; }, className: `${styles.tableWrapper} ${rowSelection !== 'none' ? styles.selectableGrid : ''}`, role: "region", "aria-label": ariaLabel ?? (ariaLabelledBy ? undefined : 'Data grid'), "aria-labelledby": ariaLabelledBy, "data-empty": showEmptyInGrid ? 'true' : undefined, "data-column-count": totalColCount, "data-freeze-rows": freezeRows != null && freezeRows >= 1 ? freezeRows : undefined, "data-freeze-cols": freezeCols != null && freezeCols >= 1 ? freezeCols : undefined, "data-overflow-x": allowOverflowX ? 'true' : 'false', "data-container-width": containerWidth, "data-min-table-width": Math.round(minTableWidth), "data-has-selection": rowSelection !== 'none' ? 'true' : undefined, onContextMenu: PREVENT_DEFAULT, onKeyDown: handleGridKeyDown, style: {
|
|
135
|
+
['--data-table-column-count']: totalColCount,
|
|
136
|
+
['--data-table-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'fit-content' : fitToContent ? 'fit-content' : '100%',
|
|
137
|
+
['--data-table-min-width']: showEmptyInGrid ? '100%' : allowOverflowX ? 'max-content' : fitToContent ? 'max-content' : '100%',
|
|
138
|
+
['--data-table-total-min-width']: `${minTableWidth}px`,
|
|
139
|
+
}, children: [_jsx("div", { className: styles.tableScrollContent, children: _jsx("div", { className: isLoading && items.length > 0 ? styles.loadingDimmed : undefined, children: _jsxs("div", { className: styles.tableWidthAnchor, ref: tableContainerRef, children: [_jsxs("table", { className: styles.dataTable, children: [_jsx("thead", { className: styles.stickyHeader, children: headerRows.map((row, rowIdx) => (_jsxs("tr", { children: [rowIdx === headerRows.length - 1 && hasCheckboxCol && (_jsx("th", { className: styles.selectionHeaderCell, scope: "col", rowSpan: 1, children: _jsx("div", { className: styles.selectionHeaderCellInner, children: _jsx(Checkbox.Root, { className: styles.rowCheckbox, checked: allSelected ? true : someSelected ? 'indeterminate' : false, onCheckedChange: handleSelectAllChecked, "aria-label": "Select all rows", children: _jsx(Checkbox.Indicator, { className: styles.rowCheckboxIndicator, children: someSelected && !allSelected ? '–' : '✓' }) }) }) })), rowIdx === 0 && rowIdx < headerRows.length - 1 && hasCheckboxCol && (_jsx("th", { rowSpan: headerRows.length - 1 })), row.map((cell, cellIdx) => {
|
|
140
|
+
if (cell.isGroup) {
|
|
141
|
+
return (_jsx("th", { colSpan: cell.colSpan, className: styles.groupHeaderCell, scope: "colgroup", children: cell.label }, cellIdx));
|
|
142
|
+
}
|
|
143
|
+
// Leaf cell
|
|
144
|
+
const col = cell.columnDef;
|
|
145
|
+
const leafRowSpan = headerRows.length > 1 && rowIdx < headerRows.length - 1
|
|
146
|
+
? headerRows.length - rowIdx
|
|
147
|
+
: undefined;
|
|
148
|
+
return (_jsxs("th", { scope: "col", "data-column-id": col.columnId, rowSpan: leafRowSpan, className: columnMeta.hdrClasses[col.columnId] || undefined, style: columnMeta.hdrStyles[col.columnId], children: [_jsx(ColumnHeaderFilter, { ...getHeaderFilterConfig(col, headerFilterInput) }), _jsx("div", { className: styles.resizeHandle, onMouseDown: (e) => handleResizeStart(e, col), "aria-label": `Resize ${col.name}` })] }, col.columnId));
|
|
149
|
+
})] }, rowIdx))) }), !showEmptyInGrid && (_jsx("tbody", { children: items.map((item, rowIndex) => {
|
|
150
|
+
const rowIdStr = getRowId(item);
|
|
151
|
+
return (_jsx(GridRow, { item: item, rowIndex: rowIndex, rowId: rowIdStr, isSelected: selectedRowIds.has(rowIdStr), visibleCols: visibleCols, columnMeta: columnMeta, renderCellContent: renderCellContent, handleSingleRowClick: handleSingleRowClick, handleRowCheckboxChange: handleRowCheckboxChange, lastMouseShiftRef: lastMouseShiftRef, hasCheckboxCol: hasCheckboxCol, selectionRange: selectionRange, activeCell: interaction.activeCell, cutRange: cutRange, copyRange: copyRange, isDragging: isDragging, editingRowId: editingCell?.rowId ?? null }, rowIdStr));
|
|
152
|
+
}) }))] }), _jsx(MarchingAntsOverlay, { containerRef: tableContainerRef, selectionRange: selectionRange, copyRange: copyRange, cutRange: cutRange, colOffset: colOffset }), showEmptyInGrid && emptyState && (_jsx("div", { className: styles.emptyStateInGrid, children: _jsx("div", { children: emptyState.render ? (emptyState.render()) : (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.emptyStateInGridTitle, children: "No results found" }), _jsx("div", { className: styles.emptyStateInGridMessage, children: emptyState.message != null ? (emptyState.message) : emptyState.hasActiveFilters ? (_jsxs(_Fragment, { children: ["No items match your current filters. Try adjusting your search or", ' ', _jsx("button", { type: "button", className: styles.emptyStateInGridLink, onClick: emptyState.onClearAll, children: "clear all filters" }), ' ', "to see all items."] })) : ('There are no items available at this time.') })] })) }) }))] }) }) }), menuPosition &&
|
|
153
|
+
createPortal(_jsx(GridContextMenu, { x: menuPosition.x, y: menuPosition.y, hasSelection: hasCellSelection, canUndo: canUndo, canRedo: canRedo, onUndo: onUndo ?? NOOP, onRedo: onRedo ?? NOOP, onCopy: handleCopy, onCut: handleCut, onPaste: handlePasteVoid, onSelectAll: handleSelectAllCells, onClose: closeContextMenu }), document.body)] }), statusBarConfig && (_jsx(StatusBar, { totalCount: statusBarConfig.totalCount, filteredCount: statusBarConfig.filteredCount, selectedCount: statusBarConfig.selectedCount ?? selectedRowIds.size, selectedCellCount: selectionRange ? (Math.abs(selectionRange.endRow - selectionRange.startRow) + 1) * (Math.abs(selectionRange.endCol - selectionRange.startCol) + 1) : undefined, aggregation: statusBarConfig.aggregation, suppressRowCount: statusBarConfig.suppressRowCount })), isLoading && (_jsx("div", { className: styles.loadingOverlay, "aria-live": "polite", children: _jsxs("div", { className: styles.loadingOverlayContent, children: [_jsx("div", { className: styles.spinner }), _jsx("span", { className: styles.loadingOverlayText, children: loadingMessage })] }) }))] }));
|
|
154
|
+
}
|
|
155
|
+
export const DataGridTable = React.memo(DataGridTableInner);
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
@charset "UTF-8";
|
|
2
|
+
.tableScrollContent {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
width: 100%;
|
|
6
|
+
min-width: 0;
|
|
7
|
+
min-height: 100%;
|
|
8
|
+
background: var(--ogrid-bg, #fff);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.tableWrapper {
|
|
12
|
+
position: relative;
|
|
13
|
+
flex: 1;
|
|
14
|
+
min-height: 0;
|
|
15
|
+
overflow-x: hidden;
|
|
16
|
+
overflow-y: auto;
|
|
17
|
+
width: 100%;
|
|
18
|
+
min-width: 0;
|
|
19
|
+
max-width: 100%;
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
background: var(--ogrid-bg, #fff);
|
|
22
|
+
will-change: scroll-position;
|
|
23
|
+
}
|
|
24
|
+
.tableWrapper[data-overflow-x=true] {
|
|
25
|
+
overflow-x: auto;
|
|
26
|
+
}
|
|
27
|
+
.tableWrapper[data-empty=true] {
|
|
28
|
+
overflow-x: hidden;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.tableWidthAnchor {
|
|
32
|
+
position: relative;
|
|
33
|
+
width: max-content;
|
|
34
|
+
/* min-width uses the same CSS var as the table: 100% in fill mode, max-content otherwise.
|
|
35
|
+
Safe now that StatusBar is outside this anchor. */
|
|
36
|
+
min-width: var(--data-table-min-width, max-content);
|
|
37
|
+
background: var(--ogrid-bg, #fff);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.dataTable {
|
|
41
|
+
width: var(--data-table-width, fit-content);
|
|
42
|
+
max-width: 100%;
|
|
43
|
+
min-width: var(--data-table-min-width, max-content);
|
|
44
|
+
border-collapse: separate;
|
|
45
|
+
border-spacing: 0;
|
|
46
|
+
box-sizing: border-box;
|
|
47
|
+
table-layout: auto;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.dataTable th,
|
|
51
|
+
.dataTable td {
|
|
52
|
+
min-width: 80px;
|
|
53
|
+
box-sizing: border-box;
|
|
54
|
+
border-right: 1px solid var(--ogrid-border, #e0e0e0);
|
|
55
|
+
font-size: 13px;
|
|
56
|
+
vertical-align: middle;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.dataTable th {
|
|
60
|
+
padding: 6px 10px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.dataTable td {
|
|
64
|
+
padding: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.dataTable th:last-of-type,
|
|
68
|
+
.dataTable td:last-of-type {
|
|
69
|
+
border-right: none;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.dataTable thead {
|
|
73
|
+
background: var(--ogrid-bg-subtle, #f3f2f1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Freeze panes: sticky header when freezeRows >= 1 */
|
|
77
|
+
.dataTable thead.stickyHeader {
|
|
78
|
+
position: sticky;
|
|
79
|
+
top: 0;
|
|
80
|
+
z-index: 8;
|
|
81
|
+
background: var(--ogrid-bg-subtle, #f3f2f1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.dataTable thead.stickyHeader th {
|
|
85
|
+
background: var(--ogrid-bg-subtle, #f3f2f1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Freeze panes: sticky first column when freezeCols >= 1 */
|
|
89
|
+
.dataTable .freezeColFirst {
|
|
90
|
+
position: sticky;
|
|
91
|
+
left: 0;
|
|
92
|
+
z-index: 6;
|
|
93
|
+
background: var(--ogrid-bg, #ffffff);
|
|
94
|
+
will-change: transform;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Freeze/pinned header cells need top: 0 + highest z-index so they stick in BOTH
|
|
98
|
+
directions — left (from .freezeCol/.pinnedCol) AND top (from .stickyHeader).
|
|
99
|
+
Without explicit top, the child's own position: sticky overrides the parent's. */
|
|
100
|
+
.dataTable thead.stickyHeader .freezeColFirst,
|
|
101
|
+
.dataTable thead.stickyHeader .pinnedColLeft,
|
|
102
|
+
.dataTable thead.stickyHeader .pinnedColRight {
|
|
103
|
+
top: 0;
|
|
104
|
+
z-index: 9;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.dataTable thead .freezeColFirst {
|
|
108
|
+
background: var(--ogrid-bg-subtle, #f3f2f1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* Pinned columns: sticky positioning based on column pinned property.
|
|
112
|
+
Selectors use .dataTable qualifier to beat .dataTable thead th / .dataTable tbody td
|
|
113
|
+
which set position: relative. */
|
|
114
|
+
.dataTable .pinnedColLeft {
|
|
115
|
+
position: sticky;
|
|
116
|
+
left: 0;
|
|
117
|
+
z-index: 6;
|
|
118
|
+
background: var(--ogrid-bg, #ffffff);
|
|
119
|
+
will-change: transform;
|
|
120
|
+
}
|
|
121
|
+
.dataTable .pinnedColLeft::after {
|
|
122
|
+
content: "";
|
|
123
|
+
position: absolute;
|
|
124
|
+
top: 0;
|
|
125
|
+
right: -4px;
|
|
126
|
+
bottom: 0;
|
|
127
|
+
width: 4px;
|
|
128
|
+
background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent);
|
|
129
|
+
pointer-events: none;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.dataTable thead .pinnedColLeft {
|
|
133
|
+
background: var(--ogrid-bg-subtle, #f3f2f1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.dataTable .pinnedColRight {
|
|
137
|
+
position: sticky;
|
|
138
|
+
right: 0;
|
|
139
|
+
z-index: 6;
|
|
140
|
+
background: var(--ogrid-bg, #ffffff);
|
|
141
|
+
will-change: transform;
|
|
142
|
+
}
|
|
143
|
+
.dataTable .pinnedColRight::before {
|
|
144
|
+
content: "";
|
|
145
|
+
position: absolute;
|
|
146
|
+
top: 0;
|
|
147
|
+
left: -4px;
|
|
148
|
+
bottom: 0;
|
|
149
|
+
width: 4px;
|
|
150
|
+
background: linear-gradient(to left, rgba(0, 0, 0, 0.08), transparent);
|
|
151
|
+
pointer-events: none;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.dataTable thead .pinnedColRight {
|
|
155
|
+
background: var(--ogrid-bg-subtle, #f3f2f1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.dataTable thead th {
|
|
159
|
+
font-weight: 600;
|
|
160
|
+
font-size: 14px;
|
|
161
|
+
color: var(--ogrid-fg, #242424);
|
|
162
|
+
white-space: nowrap;
|
|
163
|
+
position: relative;
|
|
164
|
+
border-bottom: 1px solid var(--ogrid-border, #e0e0e0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.groupHeaderCell {
|
|
168
|
+
text-align: center;
|
|
169
|
+
font-weight: 600;
|
|
170
|
+
border-bottom: 2px solid var(--ogrid-border, #e0e0e0);
|
|
171
|
+
padding: 6px 10px;
|
|
172
|
+
background: var(--ogrid-header-bg, #f5f5f5);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* Column resize handle — wide hit area with narrow visual indicator */
|
|
176
|
+
.resizeHandle {
|
|
177
|
+
position: absolute;
|
|
178
|
+
top: 0;
|
|
179
|
+
right: -3px;
|
|
180
|
+
bottom: 0;
|
|
181
|
+
width: 8px;
|
|
182
|
+
cursor: col-resize;
|
|
183
|
+
user-select: none;
|
|
184
|
+
z-index: 1;
|
|
185
|
+
}
|
|
186
|
+
.resizeHandle::after {
|
|
187
|
+
content: "";
|
|
188
|
+
position: absolute;
|
|
189
|
+
top: 0;
|
|
190
|
+
right: 3px;
|
|
191
|
+
bottom: 0;
|
|
192
|
+
width: 2px;
|
|
193
|
+
}
|
|
194
|
+
.resizeHandle:hover::after {
|
|
195
|
+
background-color: var(--ogrid-accent, #0078d4);
|
|
196
|
+
}
|
|
197
|
+
.resizeHandle:active::after {
|
|
198
|
+
background-color: var(--ogrid-accent-dark, #005a9e);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.dataTable tbody td {
|
|
202
|
+
border-bottom: 1px solid var(--ogrid-border, #e8e8e8);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.dataTable tbody tr:last-child td {
|
|
206
|
+
border-bottom: none;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.dataTable tbody tr:hover td {
|
|
210
|
+
background: var(--ogrid-bg-hover, #f5f5f5);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.dataTable tbody tr.selectedRow td {
|
|
214
|
+
background: var(--ogrid-bg-selected, #e6f0fb);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.dataTable tbody tr.selectedRow:hover td {
|
|
218
|
+
background: var(--ogrid-bg-selected-hover, #dae8f8);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.dataTable tbody td {
|
|
222
|
+
position: relative;
|
|
223
|
+
height: 1px;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.cellContent {
|
|
227
|
+
width: 100%;
|
|
228
|
+
height: 100%;
|
|
229
|
+
display: flex;
|
|
230
|
+
align-items: center;
|
|
231
|
+
min-width: 0;
|
|
232
|
+
padding: 6px 10px;
|
|
233
|
+
box-sizing: border-box;
|
|
234
|
+
overflow: hidden;
|
|
235
|
+
text-overflow: ellipsis;
|
|
236
|
+
white-space: nowrap;
|
|
237
|
+
user-select: none;
|
|
238
|
+
outline: none;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.activeCellContent {
|
|
242
|
+
outline: 2px solid var(--ogrid-selection, #217346);
|
|
243
|
+
outline-offset: -1px;
|
|
244
|
+
z-index: 2;
|
|
245
|
+
position: relative;
|
|
246
|
+
overflow: visible;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.cellInRange {
|
|
250
|
+
background: var(--ogrid-bg-range, rgba(33, 115, 70, 0.12)) !important;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
:global([data-drag-range]) {
|
|
254
|
+
background: var(--ogrid-bg-range, rgba(33, 115, 70, 0.12)) !important;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.cellCut {
|
|
258
|
+
background: var(--ogrid-bg-hover, rgba(0, 0, 0, 0.04)) !important;
|
|
259
|
+
opacity: 0.7;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.fillHandle {
|
|
263
|
+
position: absolute;
|
|
264
|
+
right: -3px;
|
|
265
|
+
bottom: -3px;
|
|
266
|
+
width: 7px;
|
|
267
|
+
height: 7px;
|
|
268
|
+
background: var(--ogrid-selection, #217346);
|
|
269
|
+
border: 1px solid var(--ogrid-bg, #fff);
|
|
270
|
+
border-radius: 1px;
|
|
271
|
+
cursor: crosshair;
|
|
272
|
+
pointer-events: auto;
|
|
273
|
+
z-index: 3;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.dataTable th.selectionHeaderCell,
|
|
277
|
+
.dataTable td.selectionCell {
|
|
278
|
+
width: 48px;
|
|
279
|
+
min-width: 48px;
|
|
280
|
+
max-width: 48px;
|
|
281
|
+
padding: 4px !important;
|
|
282
|
+
text-align: center;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.selectionHeaderCellInner,
|
|
286
|
+
.selectionCellInner {
|
|
287
|
+
display: flex;
|
|
288
|
+
align-items: center;
|
|
289
|
+
justify-content: center;
|
|
290
|
+
width: 100%;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.rowCheckbox {
|
|
294
|
+
width: 18px;
|
|
295
|
+
height: 18px;
|
|
296
|
+
border: 1.5px solid var(--ogrid-border-strong, #888);
|
|
297
|
+
border-radius: 3px;
|
|
298
|
+
background: var(--ogrid-bg, #fff);
|
|
299
|
+
cursor: pointer;
|
|
300
|
+
display: inline-flex;
|
|
301
|
+
align-items: center;
|
|
302
|
+
justify-content: center;
|
|
303
|
+
flex-shrink: 0;
|
|
304
|
+
padding: 0;
|
|
305
|
+
}
|
|
306
|
+
.rowCheckbox[data-state=checked], .rowCheckbox[data-state=indeterminate] {
|
|
307
|
+
background: var(--ogrid-primary, #0066cc);
|
|
308
|
+
border-color: var(--ogrid-primary, #0066cc);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.rowCheckboxIndicator {
|
|
312
|
+
color: var(--ogrid-primary-fg, #fff);
|
|
313
|
+
font-size: 13px;
|
|
314
|
+
line-height: 1;
|
|
315
|
+
display: flex;
|
|
316
|
+
align-items: center;
|
|
317
|
+
justify-content: center;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.statusBar {
|
|
321
|
+
display: flex;
|
|
322
|
+
align-items: center;
|
|
323
|
+
gap: 16px;
|
|
324
|
+
width: 100%;
|
|
325
|
+
padding: 6px 12px;
|
|
326
|
+
box-sizing: border-box;
|
|
327
|
+
font-size: 12px;
|
|
328
|
+
color: var(--ogrid-muted, #616161);
|
|
329
|
+
background: var(--ogrid-bg-subtle, #f3f2f1);
|
|
330
|
+
border-top: 1px solid var(--ogrid-border, #e0e0e0);
|
|
331
|
+
min-height: 28px;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.statusBarItem {
|
|
335
|
+
display: inline-flex;
|
|
336
|
+
align-items: center;
|
|
337
|
+
gap: 4px;
|
|
338
|
+
}
|
|
339
|
+
.statusBarItem:not(:last-child)::after {
|
|
340
|
+
content: "";
|
|
341
|
+
width: 1px;
|
|
342
|
+
height: 14px;
|
|
343
|
+
background: var(--ogrid-border, #c4c4c4);
|
|
344
|
+
margin-left: 12px;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.statusBarLabel {
|
|
348
|
+
color: var(--ogrid-muted, #707070);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.statusBarValue {
|
|
352
|
+
color: var(--ogrid-fg, #242424);
|
|
353
|
+
font-weight: 600;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.contextMenu {
|
|
357
|
+
position: fixed;
|
|
358
|
+
z-index: 10000;
|
|
359
|
+
min-width: 160px;
|
|
360
|
+
padding: 4px 0;
|
|
361
|
+
background: var(--ogrid-bg, #fff);
|
|
362
|
+
border: 1px solid var(--ogrid-border, #e0e0e0);
|
|
363
|
+
border-radius: 6px;
|
|
364
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.contextMenuItem {
|
|
368
|
+
display: flex;
|
|
369
|
+
align-items: center;
|
|
370
|
+
justify-content: space-between;
|
|
371
|
+
gap: 24px;
|
|
372
|
+
width: 100%;
|
|
373
|
+
padding: 6px 12px;
|
|
374
|
+
border: none;
|
|
375
|
+
background: none;
|
|
376
|
+
font-size: 13px;
|
|
377
|
+
text-align: left;
|
|
378
|
+
cursor: pointer;
|
|
379
|
+
color: var(--ogrid-fg, #242424);
|
|
380
|
+
}
|
|
381
|
+
.contextMenuItem:hover:not(:disabled) {
|
|
382
|
+
background: var(--ogrid-bg-hover, #f5f5f5);
|
|
383
|
+
}
|
|
384
|
+
.contextMenuItem:disabled {
|
|
385
|
+
opacity: 0.5;
|
|
386
|
+
cursor: not-allowed;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.contextMenuItemLabel {
|
|
390
|
+
flex: 1;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.contextMenuItemShortcut {
|
|
394
|
+
color: var(--ogrid-fg-muted, rgba(0, 0, 0, 0.4));
|
|
395
|
+
font-size: 0.85em;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.contextMenuDivider {
|
|
399
|
+
height: 1px;
|
|
400
|
+
margin: 4px 0;
|
|
401
|
+
background: var(--ogrid-border, #e0e0e0);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.loadingOverlay {
|
|
405
|
+
position: absolute;
|
|
406
|
+
inset: 0;
|
|
407
|
+
z-index: 2;
|
|
408
|
+
display: flex;
|
|
409
|
+
align-items: center;
|
|
410
|
+
justify-content: center;
|
|
411
|
+
background: rgba(255, 255, 255, 0.7);
|
|
412
|
+
backdrop-filter: blur(1px);
|
|
413
|
+
pointer-events: all;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.loadingOverlayContent {
|
|
417
|
+
display: flex;
|
|
418
|
+
flex-direction: column;
|
|
419
|
+
align-items: center;
|
|
420
|
+
gap: 8px;
|
|
421
|
+
padding: 16px 24px;
|
|
422
|
+
background: var(--ogrid-bg, #fff);
|
|
423
|
+
border: 1px solid var(--ogrid-border, #c4c4c4);
|
|
424
|
+
border-radius: 6px;
|
|
425
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.14);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.loadingOverlayText {
|
|
429
|
+
font-size: 13px;
|
|
430
|
+
font-weight: 500;
|
|
431
|
+
color: var(--ogrid-muted, #616161);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.loadingDimmed {
|
|
435
|
+
opacity: 0.6;
|
|
436
|
+
pointer-events: none;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.emptyStateInGrid {
|
|
440
|
+
display: flex;
|
|
441
|
+
flex-direction: column;
|
|
442
|
+
align-items: center;
|
|
443
|
+
justify-content: center;
|
|
444
|
+
text-align: center;
|
|
445
|
+
padding: 20px 16px;
|
|
446
|
+
min-height: 88px;
|
|
447
|
+
width: 100%;
|
|
448
|
+
box-sizing: border-box;
|
|
449
|
+
border-top: 1px solid var(--ogrid-border, #e0e0e0);
|
|
450
|
+
background: var(--ogrid-bg-subtle, #fafafa);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.emptyStateInGridTitle {
|
|
454
|
+
font-size: 14px;
|
|
455
|
+
font-weight: 600;
|
|
456
|
+
color: var(--ogrid-fg, #242424);
|
|
457
|
+
margin-bottom: 4px;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.emptyStateInGridMessage {
|
|
461
|
+
font-size: 13px;
|
|
462
|
+
color: var(--ogrid-muted, #616161);
|
|
463
|
+
line-height: 1.5;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.emptyStateInGridLink {
|
|
467
|
+
background: none;
|
|
468
|
+
border: none;
|
|
469
|
+
color: var(--ogrid-primary, #0066cc);
|
|
470
|
+
text-decoration: underline;
|
|
471
|
+
cursor: pointer;
|
|
472
|
+
padding: 0;
|
|
473
|
+
font-size: inherit;
|
|
474
|
+
font-family: inherit;
|
|
475
|
+
}
|
|
476
|
+
.emptyStateInGridLink:hover {
|
|
477
|
+
color: var(--ogrid-primary-hover, #0052a3);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.spinner {
|
|
481
|
+
width: 24px;
|
|
482
|
+
height: 24px;
|
|
483
|
+
border: 2px solid var(--ogrid-border, #e0e0e0);
|
|
484
|
+
border-top-color: var(--ogrid-primary, #0066cc);
|
|
485
|
+
border-radius: 50%;
|
|
486
|
+
animation: ogrid-spin 0.8s linear infinite;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
@keyframes ogrid-spin {
|
|
490
|
+
to {
|
|
491
|
+
transform: rotate(360deg);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { GRID_CONTEXT_MENU_ITEMS, getContextMenuHandlers, formatShortcut } from '@alaarab/ogrid-react';
|
|
4
|
+
import styles from './DataGridTable.module.css';
|
|
5
|
+
export function GridContextMenu(props) {
|
|
6
|
+
const { x, y, hasSelection, canUndo, canRedo, onClose } = props;
|
|
7
|
+
const ref = React.useRef(null);
|
|
8
|
+
const handlers = React.useMemo(() => getContextMenuHandlers(props), [props]);
|
|
9
|
+
const isDisabled = React.useCallback((item) => {
|
|
10
|
+
if (item.disabledWhenNoSelection && !hasSelection)
|
|
11
|
+
return true;
|
|
12
|
+
if (item.id === 'undo' && !canUndo)
|
|
13
|
+
return true;
|
|
14
|
+
if (item.id === 'redo' && !canRedo)
|
|
15
|
+
return true;
|
|
16
|
+
return false;
|
|
17
|
+
}, [hasSelection, canUndo, canRedo]);
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
const handleClickOutside = (e) => {
|
|
20
|
+
if (ref.current && !ref.current.contains(e.target))
|
|
21
|
+
onClose();
|
|
22
|
+
};
|
|
23
|
+
const handleKeyDown = (e) => {
|
|
24
|
+
if (e.key === 'Escape')
|
|
25
|
+
onClose();
|
|
26
|
+
};
|
|
27
|
+
document.addEventListener('mousedown', handleClickOutside, true);
|
|
28
|
+
document.addEventListener('keydown', handleKeyDown, true);
|
|
29
|
+
return () => {
|
|
30
|
+
document.removeEventListener('mousedown', handleClickOutside, true);
|
|
31
|
+
document.removeEventListener('keydown', handleKeyDown, true);
|
|
32
|
+
};
|
|
33
|
+
}, [onClose]);
|
|
34
|
+
return (_jsx("div", { ref: ref, className: styles.contextMenu, role: "menu", style: { left: x, top: y }, "aria-label": "Grid context menu", children: GRID_CONTEXT_MENU_ITEMS.map((item) => (_jsxs(React.Fragment, { children: [item.dividerBefore && _jsx("div", { className: styles.contextMenuDivider }), _jsxs("button", { type: "button", className: styles.contextMenuItem, onClick: handlers[item.id], disabled: isDisabled(item), children: [_jsx("span", { className: styles.contextMenuItemLabel, children: item.label }), item.shortcut && (_jsx("span", { className: styles.contextMenuItemShortcut, children: formatShortcut(item.shortcut) }))] })] }, item.id))) }));
|
|
35
|
+
}
|