@alaarab/ogrid-core 1.3.2 → 1.4.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/dist/esm/components/OGridLayout.js +73 -7
- package/dist/esm/components/SideBar.js +98 -0
- package/dist/esm/hooks/index.js +2 -0
- package/dist/esm/hooks/useCellSelection.js +84 -0
- package/dist/esm/hooks/useColumnHeaderFilterState.js +25 -3
- package/dist/esm/hooks/useColumnResize.js +9 -2
- package/dist/esm/hooks/useDataGridState.js +43 -6
- package/dist/esm/hooks/useKeyboardNavigation.js +56 -4
- package/dist/esm/hooks/useOGrid.js +132 -4
- package/dist/esm/hooks/useRichSelectState.js +53 -0
- package/dist/esm/hooks/useSideBarState.js +34 -0
- package/dist/esm/index.js +3 -2
- package/dist/esm/types/dataGridTypes.js +9 -2
- package/dist/esm/utils/aggregationUtils.js +39 -0
- package/dist/esm/utils/columnUtils.js +97 -0
- package/dist/esm/utils/dataGridViewModel.js +105 -6
- package/dist/esm/utils/index.js +3 -2
- package/dist/esm/utils/ogridHelpers.js +4 -2
- package/dist/esm/utils/paginationHelpers.js +1 -1
- package/dist/esm/utils/statusBarHelpers.js +8 -1
- package/dist/esm/utils/valueParsers.js +15 -1
- package/dist/types/components/OGridLayout.d.ts +22 -5
- package/dist/types/components/SideBar.d.ts +34 -0
- package/dist/types/components/StatusBar.d.ts +8 -0
- package/dist/types/hooks/index.d.ts +5 -1
- package/dist/types/hooks/useColumnHeaderFilterState.d.ts +9 -1
- package/dist/types/hooks/useColumnResize.d.ts +3 -1
- package/dist/types/hooks/useDataGridState.d.ts +3 -0
- package/dist/types/hooks/useInlineCellEditorState.d.ts +1 -1
- package/dist/types/hooks/useOGrid.d.ts +7 -0
- package/dist/types/hooks/useRichSelectState.d.ts +17 -0
- package/dist/types/hooks/useSideBarState.d.ts +15 -0
- package/dist/types/index.d.ts +7 -5
- package/dist/types/types/columnTypes.d.ts +26 -2
- package/dist/types/types/dataGridTypes.d.ts +56 -6
- package/dist/types/types/index.d.ts +2 -2
- package/dist/types/utils/aggregationUtils.d.ts +15 -0
- package/dist/types/utils/columnUtils.d.ts +16 -1
- package/dist/types/utils/dataGridViewModel.d.ts +67 -2
- package/dist/types/utils/index.d.ts +5 -3
- package/dist/types/utils/ogridHelpers.d.ts +2 -2
- package/dist/types/utils/paginationHelpers.d.ts +1 -1
- package/dist/types/utils/statusBarHelpers.d.ts +8 -0
- package/package.json +1 -1
|
@@ -1,16 +1,82 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
|
|
2
|
+
import { SideBar } from './SideBar';
|
|
3
|
+
// Stable style objects (avoid re-creating on every render)
|
|
4
|
+
const borderedContainerStyle = {
|
|
5
|
+
border: '1px solid var(--ogrid-border, #e0e0e0)',
|
|
6
|
+
borderRadius: 6,
|
|
7
|
+
overflow: 'hidden',
|
|
8
|
+
display: 'flex',
|
|
9
|
+
flexDirection: 'column',
|
|
10
|
+
flex: 1,
|
|
11
|
+
minHeight: 0,
|
|
12
|
+
background: 'var(--ogrid-bg, #fff)',
|
|
13
|
+
};
|
|
14
|
+
const toolbarStripStyle = {
|
|
15
|
+
display: 'flex',
|
|
16
|
+
justifyContent: 'space-between',
|
|
17
|
+
alignItems: 'center',
|
|
18
|
+
padding: '6px 12px',
|
|
19
|
+
borderBottom: '1px solid var(--ogrid-border, #e0e0e0)',
|
|
20
|
+
background: 'var(--ogrid-header-bg, #f5f5f5)',
|
|
21
|
+
gap: 8,
|
|
22
|
+
flexWrap: 'wrap',
|
|
23
|
+
minHeight: 0,
|
|
24
|
+
};
|
|
25
|
+
const toolbarSectionStyle = {
|
|
26
|
+
display: 'flex',
|
|
27
|
+
alignItems: 'center',
|
|
28
|
+
gap: 8,
|
|
29
|
+
};
|
|
30
|
+
const footerStripStyle = {
|
|
31
|
+
borderTop: '1px solid var(--ogrid-border, #e0e0e0)',
|
|
32
|
+
background: 'var(--ogrid-header-bg, #f5f5f5)',
|
|
33
|
+
padding: '6px 12px',
|
|
34
|
+
};
|
|
35
|
+
const gridAreaFlexStyle = {
|
|
36
|
+
width: '100%',
|
|
37
|
+
minWidth: 0,
|
|
38
|
+
flex: 1,
|
|
39
|
+
display: 'flex',
|
|
40
|
+
};
|
|
41
|
+
const gridAreaSoloStyle = {
|
|
42
|
+
width: '100%',
|
|
43
|
+
minWidth: 0,
|
|
44
|
+
minHeight: 0,
|
|
45
|
+
flex: 1,
|
|
46
|
+
display: 'flex',
|
|
47
|
+
flexDirection: 'column',
|
|
48
|
+
};
|
|
49
|
+
const gridChildStyle = {
|
|
50
|
+
flex: 1,
|
|
51
|
+
minWidth: 0,
|
|
52
|
+
minHeight: 0,
|
|
53
|
+
display: 'flex',
|
|
54
|
+
flexDirection: 'column',
|
|
55
|
+
};
|
|
3
56
|
/**
|
|
4
|
-
* Renders OGrid layout
|
|
5
|
-
*
|
|
57
|
+
* Renders OGrid layout as a unified bordered container:
|
|
58
|
+
* [deprecated title above]
|
|
59
|
+
* ┌────────────────────────────────────┐
|
|
60
|
+
* │ [toolbar strip] │
|
|
61
|
+
* ├────────────────────────────────────┤
|
|
62
|
+
* │ [sidebar]? [grid] │
|
|
63
|
+
* ├────────────────────────────────────┤
|
|
64
|
+
* │ [footer strip / pagination] │
|
|
65
|
+
* └────────────────────────────────────┘
|
|
6
66
|
*/
|
|
7
67
|
export function OGridLayout(props) {
|
|
8
|
-
const { containerComponent: Container = 'div', containerProps = {}, gap =
|
|
9
|
-
|
|
68
|
+
const { containerComponent: Container = 'div', containerProps = {}, gap = 8, className, title, toolbar, columnChooser, toolbarEnd: toolbarEndProp, children, pagination, sideBar, } = props;
|
|
69
|
+
const hasSideBar = sideBar != null;
|
|
70
|
+
const sideBarPosition = sideBar?.position ?? 'right';
|
|
71
|
+
// Backward compat: columnChooser prop → toolbarEnd
|
|
72
|
+
const toolbarEnd = toolbarEndProp ?? columnChooser;
|
|
73
|
+
const hasToolbar = toolbar != null || toolbarEnd != null;
|
|
74
|
+
// Root styles: flex column, fill parent height, gap for deprecated title spacing
|
|
10
75
|
const rootStyle = {
|
|
11
76
|
display: 'flex',
|
|
12
77
|
flexDirection: 'column',
|
|
13
|
-
|
|
78
|
+
height: '100%',
|
|
79
|
+
gap: title != null ? (typeof gap === 'number' ? `${gap}px` : gap) : undefined,
|
|
14
80
|
};
|
|
15
|
-
return (_jsxs(Container, { className: className, style: rootStyle, ...containerProps, children: [
|
|
81
|
+
return (_jsxs(Container, { className: className, style: rootStyle, ...containerProps, children: [title != null && _jsx("div", { style: { margin: 0 }, children: title }), _jsxs("div", { style: borderedContainerStyle, children: [hasToolbar && (_jsxs("div", { style: toolbarStripStyle, children: [_jsx("div", { style: toolbarSectionStyle, children: toolbar }), _jsx("div", { style: toolbarSectionStyle, children: toolbarEnd })] })), hasSideBar ? (_jsxs("div", { style: gridAreaFlexStyle, children: [sideBarPosition === 'left' && _jsx(SideBar, { ...sideBar }), _jsx("div", { style: gridChildStyle, children: children }), sideBarPosition !== 'left' && _jsx(SideBar, { ...sideBar })] })) : (_jsx("div", { style: gridAreaSoloStyle, children: children })), pagination && (_jsx("div", { style: footerStripStyle, children: pagination }))] })] }));
|
|
16
82
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
const PANEL_WIDTH = 240;
|
|
3
|
+
const TAB_WIDTH = 36;
|
|
4
|
+
const PANEL_LABELS = {
|
|
5
|
+
columns: 'Columns',
|
|
6
|
+
filters: 'Filters',
|
|
7
|
+
};
|
|
8
|
+
export function SideBar(props) {
|
|
9
|
+
const { activePanel, onPanelChange, panels, position, columns, visibleColumns, onVisibilityChange, onSetVisibleColumns, filterableColumns, multiSelectFilters, textFilters, onMultiSelectFilterChange, onTextFilterChange, dateFilters, onDateFilterChange, filterOptions, } = props;
|
|
10
|
+
const isOpen = activePanel !== null;
|
|
11
|
+
const handleTabClick = (panel) => {
|
|
12
|
+
onPanelChange(activePanel === panel ? null : panel);
|
|
13
|
+
};
|
|
14
|
+
const tabStrip = (_jsx("div", { style: {
|
|
15
|
+
display: 'flex',
|
|
16
|
+
flexDirection: 'column',
|
|
17
|
+
width: TAB_WIDTH,
|
|
18
|
+
borderLeft: position === 'right' ? '1px solid var(--ogrid-border, #e0e0e0)' : undefined,
|
|
19
|
+
borderRight: position === 'left' ? '1px solid var(--ogrid-border, #e0e0e0)' : undefined,
|
|
20
|
+
background: 'var(--ogrid-header-bg, #f5f5f5)',
|
|
21
|
+
}, role: "tablist", "aria-label": "Side bar tabs", children: panels.map((panel) => (_jsx("button", { role: "tab", "aria-selected": activePanel === panel, "aria-label": PANEL_LABELS[panel], onClick: () => handleTabClick(panel), title: PANEL_LABELS[panel], style: {
|
|
22
|
+
width: TAB_WIDTH,
|
|
23
|
+
height: TAB_WIDTH,
|
|
24
|
+
border: 'none',
|
|
25
|
+
cursor: 'pointer',
|
|
26
|
+
background: activePanel === panel ? 'var(--ogrid-bg, #fff)' : 'transparent',
|
|
27
|
+
color: 'var(--ogrid-fg, #242424)',
|
|
28
|
+
fontWeight: activePanel === panel ? 'bold' : 'normal',
|
|
29
|
+
fontSize: 14,
|
|
30
|
+
display: 'flex',
|
|
31
|
+
alignItems: 'center',
|
|
32
|
+
justifyContent: 'center',
|
|
33
|
+
}, children: panel === 'columns' ? '\u2261' : '\u2A65' }, panel))) }));
|
|
34
|
+
const panelContent = isOpen ? (_jsxs("div", { role: "tabpanel", "aria-label": PANEL_LABELS[activePanel], style: {
|
|
35
|
+
width: PANEL_WIDTH,
|
|
36
|
+
display: 'flex',
|
|
37
|
+
flexDirection: 'column',
|
|
38
|
+
borderLeft: position === 'right' ? '1px solid var(--ogrid-border, #e0e0e0)' : undefined,
|
|
39
|
+
borderRight: position === 'left' ? '1px solid var(--ogrid-border, #e0e0e0)' : undefined,
|
|
40
|
+
overflow: 'hidden',
|
|
41
|
+
background: 'var(--ogrid-bg, #fff)',
|
|
42
|
+
color: 'var(--ogrid-fg, #242424)',
|
|
43
|
+
}, children: [_jsxs("div", { style: {
|
|
44
|
+
display: 'flex',
|
|
45
|
+
justifyContent: 'space-between',
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
padding: '8px 12px',
|
|
48
|
+
borderBottom: '1px solid var(--ogrid-border, #e0e0e0)',
|
|
49
|
+
fontWeight: 600,
|
|
50
|
+
}, children: [_jsx("span", { children: PANEL_LABELS[activePanel] }), _jsx("button", { onClick: () => onPanelChange(null), style: { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: 16, color: 'var(--ogrid-fg, #242424)' }, "aria-label": "Close panel", children: "\u00D7" })] }), _jsxs("div", { style: { flex: 1, overflowY: 'auto', padding: '8px 12px' }, children: [activePanel === 'columns' && (_jsx(ColumnsPanel, { columns: columns, visibleColumns: visibleColumns, onVisibilityChange: onVisibilityChange, onSetVisibleColumns: onSetVisibleColumns })), activePanel === 'filters' && (_jsx(FiltersPanel, { filterableColumns: filterableColumns, multiSelectFilters: multiSelectFilters, textFilters: textFilters, onMultiSelectFilterChange: onMultiSelectFilterChange, onTextFilterChange: onTextFilterChange, dateFilters: dateFilters, onDateFilterChange: onDateFilterChange, filterOptions: filterOptions }))] })] })) : null;
|
|
51
|
+
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'row', flexShrink: 0 }, role: "complementary", "aria-label": "Side bar", children: [position === 'left' && tabStrip, position === 'left' && panelContent, position === 'right' && panelContent, position === 'right' && tabStrip] }));
|
|
52
|
+
}
|
|
53
|
+
// --- Internal sub-components ---
|
|
54
|
+
function ColumnsPanel(props) {
|
|
55
|
+
const { columns, visibleColumns, onVisibilityChange, onSetVisibleColumns } = props;
|
|
56
|
+
const allVisible = columns.every((c) => visibleColumns.has(c.columnId));
|
|
57
|
+
const handleSelectAll = () => {
|
|
58
|
+
const next = new Set(visibleColumns);
|
|
59
|
+
columns.forEach((c) => next.add(c.columnId));
|
|
60
|
+
onSetVisibleColumns(next);
|
|
61
|
+
};
|
|
62
|
+
const handleClearAll = () => {
|
|
63
|
+
const next = new Set();
|
|
64
|
+
columns.forEach((c) => {
|
|
65
|
+
if (c.required && visibleColumns.has(c.columnId))
|
|
66
|
+
next.add(c.columnId);
|
|
67
|
+
});
|
|
68
|
+
onSetVisibleColumns(next);
|
|
69
|
+
};
|
|
70
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { style: { display: 'flex', gap: 8, marginBottom: 8 }, children: [_jsx("button", { onClick: handleSelectAll, disabled: allVisible, style: { flex: 1, cursor: 'pointer', background: 'var(--ogrid-bg-subtle, #f3f2f1)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: 4, padding: '4px 8px' }, children: "Select All" }), _jsx("button", { onClick: handleClearAll, style: { flex: 1, cursor: 'pointer', background: 'var(--ogrid-bg-subtle, #f3f2f1)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: 4, padding: '4px 8px' }, children: "Clear All" })] }), columns.map((col) => (_jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6, padding: '2px 0', cursor: 'pointer' }, children: [_jsx("input", { type: "checkbox", checked: visibleColumns.has(col.columnId), onChange: (e) => onVisibilityChange(col.columnId, e.target.checked), disabled: col.required }), _jsx("span", { children: col.name })] }, col.columnId)))] }));
|
|
71
|
+
}
|
|
72
|
+
function FiltersPanel(props) {
|
|
73
|
+
const { filterableColumns, multiSelectFilters, textFilters, onMultiSelectFilterChange, onTextFilterChange, dateFilters, onDateFilterChange, filterOptions } = props;
|
|
74
|
+
if (filterableColumns.length === 0) {
|
|
75
|
+
return _jsx("div", { style: { color: 'var(--ogrid-muted, #999)', fontStyle: 'italic' }, children: "No filterable columns" });
|
|
76
|
+
}
|
|
77
|
+
return (_jsx(_Fragment, { children: filterableColumns.map((col) => {
|
|
78
|
+
const filterKey = col.filterField;
|
|
79
|
+
return (_jsxs("div", { style: { marginBottom: 12 }, children: [_jsx("div", { style: { fontWeight: 500, marginBottom: 4, fontSize: 13 }, children: col.name }), col.filterType === 'text' && (_jsx("input", { type: "text", value: textFilters[filterKey] ?? '', onChange: (e) => onTextFilterChange(filterKey, e.target.value), placeholder: `Filter ${col.name}...`, "aria-label": `Filter ${col.name}`, style: { width: '100%', boxSizing: 'border-box', padding: '4px 6px', background: 'var(--ogrid-bg, #fff)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: 4 } })), col.filterType === 'date' && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [_jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }, children: ["From:", _jsx("input", { type: "date", value: dateFilters[filterKey]?.from ?? '', onChange: (e) => {
|
|
80
|
+
const from = e.target.value || undefined;
|
|
81
|
+
const to = dateFilters[filterKey]?.to;
|
|
82
|
+
onDateFilterChange(filterKey, from || to ? { from, to } : undefined);
|
|
83
|
+
}, "aria-label": `${col.name} from date`, style: { flex: 1, padding: '2px 4px', background: 'var(--ogrid-bg, #fff)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: 4 } })] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }, children: ["To:", _jsx("input", { type: "date", value: dateFilters[filterKey]?.to ?? '', onChange: (e) => {
|
|
84
|
+
const to = e.target.value || undefined;
|
|
85
|
+
const from = dateFilters[filterKey]?.from;
|
|
86
|
+
onDateFilterChange(filterKey, from || to ? { from, to } : undefined);
|
|
87
|
+
}, "aria-label": `${col.name} to date`, style: { flex: 1, padding: '2px 4px', background: 'var(--ogrid-bg, #fff)', color: 'var(--ogrid-fg, #242424)', border: '1px solid var(--ogrid-border, #e0e0e0)', borderRadius: 4 } })] })] })), col.filterType === 'multiSelect' && (_jsx("div", { style: { maxHeight: 120, overflowY: 'auto' }, role: "group", "aria-label": `${col.name} options`, children: (filterOptions[filterKey] ?? []).map((opt) => {
|
|
88
|
+
const selected = (multiSelectFilters[filterKey] ?? []).includes(opt);
|
|
89
|
+
return (_jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 4, padding: '1px 0', cursor: 'pointer', fontSize: 13 }, children: [_jsx("input", { type: "checkbox", checked: selected, onChange: (e) => {
|
|
90
|
+
const current = multiSelectFilters[filterKey] ?? [];
|
|
91
|
+
const next = e.target.checked
|
|
92
|
+
? [...current, opt]
|
|
93
|
+
: current.filter((v) => v !== opt);
|
|
94
|
+
onMultiSelectFilterChange(filterKey, next);
|
|
95
|
+
} }), _jsx("span", { children: opt })] }, opt));
|
|
96
|
+
}) }))] }, col.columnId));
|
|
97
|
+
}) }));
|
|
98
|
+
}
|
package/dist/esm/hooks/index.js
CHANGED
|
@@ -15,3 +15,5 @@ export { useColumnHeaderFilterState } from './useColumnHeaderFilterState';
|
|
|
15
15
|
export { useColumnChooserState } from './useColumnChooserState';
|
|
16
16
|
export { useInlineCellEditorState } from './useInlineCellEditorState';
|
|
17
17
|
export { useColumnResize } from './useColumnResize';
|
|
18
|
+
export { useRichSelectState } from './useRichSelectState';
|
|
19
|
+
export { useSideBarState } from './useSideBarState';
|
|
@@ -2,6 +2,16 @@ import { useState, useCallback, useRef, useEffect } from 'react';
|
|
|
2
2
|
import { normalizeSelectionRange } from '../types';
|
|
3
3
|
/** DOM attribute name used for drag-range highlighting (bypasses React). */
|
|
4
4
|
const DRAG_ATTR = 'data-drag-range';
|
|
5
|
+
/** Auto-scroll config */
|
|
6
|
+
const AUTO_SCROLL_EDGE = 40; // px from wrapper edge to trigger
|
|
7
|
+
const AUTO_SCROLL_MIN_SPEED = 2;
|
|
8
|
+
const AUTO_SCROLL_MAX_SPEED = 20;
|
|
9
|
+
const AUTO_SCROLL_INTERVAL = 16; // ~60fps
|
|
10
|
+
/** Compute scroll speed proportional to distance past the edge, capped. */
|
|
11
|
+
function autoScrollSpeed(distance) {
|
|
12
|
+
const t = Math.min(distance / AUTO_SCROLL_EDGE, 1);
|
|
13
|
+
return AUTO_SCROLL_MIN_SPEED + t * (AUTO_SCROLL_MAX_SPEED - AUTO_SCROLL_MIN_SPEED);
|
|
14
|
+
}
|
|
5
15
|
export function useCellSelection(params) {
|
|
6
16
|
const { colOffset, rowCount, visibleColCount, setActiveCell, wrapperRef } = params;
|
|
7
17
|
const [selectionRange, setSelectionRange] = useState(null);
|
|
@@ -11,6 +21,8 @@ export function useCellSelection(params) {
|
|
|
11
21
|
const rafRef = useRef(0);
|
|
12
22
|
/** Live drag range kept in a ref — only committed to React state on mouseup. */
|
|
13
23
|
const liveDragRangeRef = useRef(null);
|
|
24
|
+
/** Auto-scroll interval during drag. */
|
|
25
|
+
const autoScrollRef = useRef(null);
|
|
14
26
|
const handleCellMouseDown = useCallback((e, rowIndex, globalColIndex) => {
|
|
15
27
|
// Only handle primary (left) button — let middle-click scroll and right-click context menu work natively
|
|
16
28
|
if (e.button !== 0)
|
|
@@ -116,11 +128,81 @@ export function useCellSelection(params) {
|
|
|
116
128
|
endCol: dataCol,
|
|
117
129
|
});
|
|
118
130
|
};
|
|
131
|
+
/** Start or update auto-scroll interval based on mouse position relative to wrapper edges. */
|
|
132
|
+
const updateAutoScroll = () => {
|
|
133
|
+
const wrapper = wrapperRef.current;
|
|
134
|
+
const pos = lastMousePosRef.current;
|
|
135
|
+
if (!wrapper || !pos || !isDraggingRef.current) {
|
|
136
|
+
stopAutoScroll();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const rect = wrapper.getBoundingClientRect();
|
|
140
|
+
let dx = 0;
|
|
141
|
+
let dy = 0;
|
|
142
|
+
if (pos.cy < rect.top + AUTO_SCROLL_EDGE) {
|
|
143
|
+
dy = -autoScrollSpeed(rect.top + AUTO_SCROLL_EDGE - pos.cy);
|
|
144
|
+
}
|
|
145
|
+
else if (pos.cy > rect.bottom - AUTO_SCROLL_EDGE) {
|
|
146
|
+
dy = autoScrollSpeed(pos.cy - (rect.bottom - AUTO_SCROLL_EDGE));
|
|
147
|
+
}
|
|
148
|
+
if (pos.cx < rect.left + AUTO_SCROLL_EDGE) {
|
|
149
|
+
dx = -autoScrollSpeed(rect.left + AUTO_SCROLL_EDGE - pos.cx);
|
|
150
|
+
}
|
|
151
|
+
else if (pos.cx > rect.right - AUTO_SCROLL_EDGE) {
|
|
152
|
+
dx = autoScrollSpeed(pos.cx - (rect.right - AUTO_SCROLL_EDGE));
|
|
153
|
+
}
|
|
154
|
+
if (dx === 0 && dy === 0) {
|
|
155
|
+
stopAutoScroll();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// Start interval if not already running
|
|
159
|
+
if (!autoScrollRef.current) {
|
|
160
|
+
autoScrollRef.current = setInterval(() => {
|
|
161
|
+
const w = wrapperRef.current;
|
|
162
|
+
const p = lastMousePosRef.current;
|
|
163
|
+
if (!w || !p || !isDraggingRef.current) {
|
|
164
|
+
stopAutoScroll();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const r = w.getBoundingClientRect();
|
|
168
|
+
let sdx = 0;
|
|
169
|
+
let sdy = 0;
|
|
170
|
+
if (p.cy < r.top + AUTO_SCROLL_EDGE)
|
|
171
|
+
sdy = -autoScrollSpeed(r.top + AUTO_SCROLL_EDGE - p.cy);
|
|
172
|
+
else if (p.cy > r.bottom - AUTO_SCROLL_EDGE)
|
|
173
|
+
sdy = autoScrollSpeed(p.cy - (r.bottom - AUTO_SCROLL_EDGE));
|
|
174
|
+
if (p.cx < r.left + AUTO_SCROLL_EDGE)
|
|
175
|
+
sdx = -autoScrollSpeed(r.left + AUTO_SCROLL_EDGE - p.cx);
|
|
176
|
+
else if (p.cx > r.right - AUTO_SCROLL_EDGE)
|
|
177
|
+
sdx = autoScrollSpeed(p.cx - (r.right - AUTO_SCROLL_EDGE));
|
|
178
|
+
if (sdx === 0 && sdy === 0) {
|
|
179
|
+
stopAutoScroll();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
w.scrollTop += sdy;
|
|
183
|
+
w.scrollLeft += sdx;
|
|
184
|
+
// After scrolling, re-resolve the cell under the mouse and update drag range
|
|
185
|
+
const newRange = resolveRange(p.cx, p.cy);
|
|
186
|
+
if (newRange) {
|
|
187
|
+
liveDragRangeRef.current = newRange;
|
|
188
|
+
applyDragAttrs(newRange);
|
|
189
|
+
}
|
|
190
|
+
}, AUTO_SCROLL_INTERVAL);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const stopAutoScroll = () => {
|
|
194
|
+
if (autoScrollRef.current) {
|
|
195
|
+
clearInterval(autoScrollRef.current);
|
|
196
|
+
autoScrollRef.current = null;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
119
199
|
const onMove = (e) => {
|
|
120
200
|
if (!isDraggingRef.current || !dragStartRef.current)
|
|
121
201
|
return;
|
|
122
202
|
// Always store latest position so mouseUp can flush if RAF hasn't executed
|
|
123
203
|
lastMousePosRef.current = { cx: e.clientX, cy: e.clientY };
|
|
204
|
+
// Update auto-scroll based on mouse proximity to edges
|
|
205
|
+
updateAutoScroll();
|
|
124
206
|
// Cancel previous pending frame
|
|
125
207
|
if (rafRef.current)
|
|
126
208
|
cancelAnimationFrame(rafRef.current);
|
|
@@ -149,6 +231,7 @@ export function useCellSelection(params) {
|
|
|
149
231
|
const onUp = () => {
|
|
150
232
|
if (!isDraggingRef.current)
|
|
151
233
|
return;
|
|
234
|
+
stopAutoScroll();
|
|
152
235
|
if (rafRef.current) {
|
|
153
236
|
cancelAnimationFrame(rafRef.current);
|
|
154
237
|
rafRef.current = 0;
|
|
@@ -185,6 +268,7 @@ export function useCellSelection(params) {
|
|
|
185
268
|
window.removeEventListener('mouseup', onUp, true);
|
|
186
269
|
if (rafRef.current)
|
|
187
270
|
cancelAnimationFrame(rafRef.current);
|
|
271
|
+
stopAutoScroll();
|
|
188
272
|
};
|
|
189
273
|
}, [colOffset, setActiveCell, wrapperRef]);
|
|
190
274
|
return {
|
|
@@ -7,7 +7,7 @@ import { useDebounce } from './useDebounce';
|
|
|
7
7
|
const SEARCH_DEBOUNCE_MS = 150;
|
|
8
8
|
const EMPTY_OPTIONS = [];
|
|
9
9
|
export function useColumnHeaderFilterState(params) {
|
|
10
|
-
const { filterType, onSort, selectedValues, onFilterChange, options, textValue = '', onTextChange, selectedUser, onUserChange, peopleSearch, } = params;
|
|
10
|
+
const { filterType, onSort, selectedValues, onFilterChange, options, textValue = '', onTextChange, selectedUser, onUserChange, peopleSearch, dateValue, onDateChange, } = params;
|
|
11
11
|
const safeSelectedValues = selectedValues ?? EMPTY_OPTIONS;
|
|
12
12
|
const safeOptions = options ?? EMPTY_OPTIONS;
|
|
13
13
|
const headerRef = useRef(null);
|
|
@@ -22,12 +22,16 @@ export function useColumnHeaderFilterState(params) {
|
|
|
22
22
|
const [peopleSuggestions, setPeopleSuggestions] = useState([]);
|
|
23
23
|
const [isPeopleLoading, setIsPeopleLoading] = useState(false);
|
|
24
24
|
const [peopleSearchText, setPeopleSearchText] = useState('');
|
|
25
|
+
const [tempDateFrom, setTempDateFrom] = useState(dateValue?.from ?? '');
|
|
26
|
+
const [tempDateTo, setTempDateTo] = useState(dateValue?.to ?? '');
|
|
25
27
|
const [popoverPosition, setPopoverPosition] = useState(null);
|
|
26
28
|
// Sync temp state when popover opens
|
|
27
29
|
useEffect(() => {
|
|
28
30
|
if (isFilterOpen) {
|
|
29
31
|
setTempSelected(new Set(safeSelectedValues));
|
|
30
32
|
setTempTextValue(textValue);
|
|
33
|
+
setTempDateFrom(dateValue?.from ?? '');
|
|
34
|
+
setTempDateTo(dateValue?.to ?? '');
|
|
31
35
|
setSearchText('');
|
|
32
36
|
setPeopleSearchText('');
|
|
33
37
|
setPeopleSuggestions([]);
|
|
@@ -38,7 +42,7 @@ export function useColumnHeaderFilterState(params) {
|
|
|
38
42
|
else {
|
|
39
43
|
setPopoverPosition(null);
|
|
40
44
|
}
|
|
41
|
-
}, [isFilterOpen, filterType, safeSelectedValues, textValue]);
|
|
45
|
+
}, [isFilterOpen, filterType, safeSelectedValues, textValue, dateValue]);
|
|
42
46
|
// Click outside and Escape to close
|
|
43
47
|
useEffect(() => {
|
|
44
48
|
if (!isFilterOpen)
|
|
@@ -144,6 +148,16 @@ export function useColumnHeaderFilterState(params) {
|
|
|
144
148
|
onUserChange?.(user);
|
|
145
149
|
setFilterOpen(false);
|
|
146
150
|
}, [onUserChange]);
|
|
151
|
+
const handleDateApply = useCallback(() => {
|
|
152
|
+
const from = tempDateFrom || undefined;
|
|
153
|
+
const to = tempDateTo || undefined;
|
|
154
|
+
onDateChange?.(from || to ? { from, to } : undefined);
|
|
155
|
+
setFilterOpen(false);
|
|
156
|
+
}, [onDateChange, tempDateFrom, tempDateTo]);
|
|
157
|
+
const handleDateClear = useCallback(() => {
|
|
158
|
+
setTempDateFrom('');
|
|
159
|
+
setTempDateTo('');
|
|
160
|
+
}, []);
|
|
147
161
|
const handleClearUser = useCallback(() => {
|
|
148
162
|
onUserChange?.(undefined);
|
|
149
163
|
setFilterOpen(false);
|
|
@@ -163,8 +177,10 @@ export function useColumnHeaderFilterState(params) {
|
|
|
163
177
|
return !!textValue.trim();
|
|
164
178
|
if (filterType === 'people')
|
|
165
179
|
return !!selectedUser;
|
|
180
|
+
if (filterType === 'date')
|
|
181
|
+
return !!(dateValue?.from || dateValue?.to);
|
|
166
182
|
return false;
|
|
167
|
-
}, [filterType, safeSelectedValues, textValue, selectedUser]);
|
|
183
|
+
}, [filterType, safeSelectedValues, textValue, selectedUser, dateValue]);
|
|
168
184
|
return {
|
|
169
185
|
headerRef,
|
|
170
186
|
popoverRef,
|
|
@@ -183,6 +199,10 @@ export function useColumnHeaderFilterState(params) {
|
|
|
183
199
|
isPeopleLoading,
|
|
184
200
|
peopleSearchText,
|
|
185
201
|
setPeopleSearchText,
|
|
202
|
+
tempDateFrom,
|
|
203
|
+
setTempDateFrom,
|
|
204
|
+
tempDateTo,
|
|
205
|
+
setTempDateTo,
|
|
186
206
|
hasActiveFilter,
|
|
187
207
|
popoverPosition,
|
|
188
208
|
handlers: {
|
|
@@ -192,6 +212,8 @@ export function useColumnHeaderFilterState(params) {
|
|
|
192
212
|
handleTextClear,
|
|
193
213
|
handleUserSelect,
|
|
194
214
|
handleClearUser,
|
|
215
|
+
handleDateApply,
|
|
216
|
+
handleDateClear,
|
|
195
217
|
handleCheckboxChange,
|
|
196
218
|
handleSelectAll,
|
|
197
219
|
handleClearSelection,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useRef, useEffect } from 'react';
|
|
2
|
-
export function useColumnResize({ columnSizingOverrides, setColumnSizingOverrides, minWidth = 80, defaultWidth = 120, }) {
|
|
2
|
+
export function useColumnResize({ columnSizingOverrides, setColumnSizingOverrides, minWidth = 80, defaultWidth = 120, onColumnResized, }) {
|
|
3
3
|
const resizingRef = useRef(null);
|
|
4
4
|
const handleResizeStart = useCallback((e, col) => {
|
|
5
5
|
e.preventDefault();
|
|
@@ -26,8 +26,15 @@ export function useColumnResize({ columnSizingOverrides, setColumnSizingOverride
|
|
|
26
26
|
}));
|
|
27
27
|
}, [setColumnSizingOverrides, minWidth]);
|
|
28
28
|
const handleResizeEnd = useCallback(() => {
|
|
29
|
+
if (resizingRef.current && onColumnResized) {
|
|
30
|
+
const { columnId } = resizingRef.current;
|
|
31
|
+
const width = columnSizingOverrides[columnId]?.widthPx;
|
|
32
|
+
if (width != null) {
|
|
33
|
+
onColumnResized(columnId, width);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
29
36
|
resizingRef.current = null;
|
|
30
|
-
}, []);
|
|
37
|
+
}, [onColumnResized, columnSizingOverrides]);
|
|
31
38
|
useEffect(() => {
|
|
32
39
|
const handleMouseMove = (e) => handleResizeMove(e);
|
|
33
40
|
const handleMouseUp = () => handleResizeEnd();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMemo, useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import { flattenColumns, getDataGridStatusBarConfig } from '../utils';
|
|
3
3
|
import { parseValue } from '../utils/valueParsers';
|
|
4
|
+
import { computeAggregations } from '../utils/aggregationUtils';
|
|
4
5
|
import { useRowSelection } from './useRowSelection';
|
|
5
6
|
import { useCellEditing } from './useCellEditing';
|
|
6
7
|
import { useActiveCell } from './useActiveCell';
|
|
@@ -22,7 +23,7 @@ const NOOP_CTX = (_e) => { };
|
|
|
22
23
|
*/
|
|
23
24
|
export function useDataGridState(params) {
|
|
24
25
|
const { props, wrapperRef } = params;
|
|
25
|
-
const { items, columns, getRowId, visibleColumns, columnOrder, rowSelection = 'none', selectedRows: controlledSelectedRows, onSelectionChange, statusBar, emptyState, editable, cellSelection: cellSelectionProp, onCellValueChanged: onCellValueChangedProp, } = props;
|
|
26
|
+
const { items, columns, getRowId, visibleColumns, columnOrder, rowSelection = 'none', selectedRows: controlledSelectedRows, onSelectionChange, statusBar, emptyState, editable, cellSelection: cellSelectionProp, onCellValueChanged: onCellValueChangedProp, initialColumnWidths, onColumnResized, } = props;
|
|
26
27
|
const cellSelection = cellSelectionProp !== false;
|
|
27
28
|
// Wrap onCellValueChanged with undo/redo tracking — all edits are recorded automatically
|
|
28
29
|
const undoRedo = useUndoRedo({ onCellValueChanged: onCellValueChangedProp });
|
|
@@ -146,12 +147,25 @@ export function useDataGridState(params) {
|
|
|
146
147
|
measure();
|
|
147
148
|
return () => ro.disconnect();
|
|
148
149
|
}, [wrapperRef]);
|
|
150
|
+
const [columnSizingOverrides, setColumnSizingOverrides] = useState(() => {
|
|
151
|
+
if (!initialColumnWidths)
|
|
152
|
+
return {};
|
|
153
|
+
const result = {};
|
|
154
|
+
for (const [id, width] of Object.entries(initialColumnWidths)) {
|
|
155
|
+
result[id] = { widthPx: width };
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
});
|
|
149
159
|
const minTableWidth = useMemo(() => {
|
|
150
160
|
const PADDING = 16;
|
|
151
161
|
const checkboxW = hasCheckboxCol ? 48 : 0;
|
|
152
|
-
return visibleCols.reduce((sum, c) =>
|
|
153
|
-
|
|
154
|
-
|
|
162
|
+
return visibleCols.reduce((sum, c) => {
|
|
163
|
+
// Use the widest explicit width: resize override > idealWidth > defaultWidth > minWidth > 80
|
|
164
|
+
const override = columnSizingOverrides[c.columnId];
|
|
165
|
+
const w = override?.widthPx ?? c.idealWidth ?? c.defaultWidth ?? c.minWidth ?? 80;
|
|
166
|
+
return sum + w + PADDING;
|
|
167
|
+
}, checkboxW);
|
|
168
|
+
}, [visibleCols, hasCheckboxCol, columnSizingOverrides]);
|
|
155
169
|
useEffect(() => {
|
|
156
170
|
const colIds = new Set(flatColumns.map((c) => c.columnId));
|
|
157
171
|
setColumnSizingOverrides((prev) => {
|
|
@@ -166,11 +180,28 @@ export function useDataGridState(params) {
|
|
|
166
180
|
return changed ? next : prev;
|
|
167
181
|
});
|
|
168
182
|
}, [flatColumns]);
|
|
169
|
-
const
|
|
183
|
+
const desiredTableWidth = useMemo(() => {
|
|
184
|
+
const PADDING = 16;
|
|
185
|
+
const checkboxW = hasCheckboxCol ? 48 : 0;
|
|
186
|
+
return visibleCols.reduce((sum, c) => {
|
|
187
|
+
const override = columnSizingOverrides[c.columnId];
|
|
188
|
+
const w = override
|
|
189
|
+
? override.widthPx
|
|
190
|
+
: (c.idealWidth ?? c.defaultWidth ?? c.minWidth ?? 80);
|
|
191
|
+
return sum + Math.max(c.minWidth ?? 80, w) + PADDING;
|
|
192
|
+
}, checkboxW);
|
|
193
|
+
}, [visibleCols, columnSizingOverrides, hasCheckboxCol]);
|
|
194
|
+
const aggregation = useMemo(() => computeAggregations(items, visibleCols, cellSelection ? selectionRange : null), [items, visibleCols, selectionRange, cellSelection]);
|
|
195
|
+
const statusBarConfig = useMemo(() => {
|
|
196
|
+
const base = getDataGridStatusBarConfig(statusBar, items.length, selectedRowIds.size);
|
|
197
|
+
if (!base)
|
|
198
|
+
return null;
|
|
199
|
+
return { ...base, aggregation: aggregation ?? undefined };
|
|
200
|
+
}, [statusBar, items.length, selectedRowIds.size, aggregation]);
|
|
170
201
|
const showEmptyInGrid = items.length === 0 && !!emptyState;
|
|
171
202
|
const hasCellSelection = selectionRange != null || activeCell != null;
|
|
172
203
|
// --- View-model inputs (shared across all 3 DataGridTables) ---
|
|
173
|
-
const { sortBy, sortDirection, onColumnSort, textFilters = {}, onTextFilterChange, peopleFilters = {}, onPeopleFilterChange, peopleSearch, filterOptions, loadingFilterOptions, multiSelectFilters, onMultiSelectFilterChange, } = props;
|
|
204
|
+
const { sortBy, sortDirection, onColumnSort, textFilters = {}, onTextFilterChange, peopleFilters = {}, onPeopleFilterChange, peopleSearch, filterOptions, loadingFilterOptions, multiSelectFilters, onMultiSelectFilterChange, dateFilters = {}, onDateFilterChange, } = props;
|
|
174
205
|
const headerFilterInput = useMemo(() => ({
|
|
175
206
|
sortBy,
|
|
176
207
|
sortDirection,
|
|
@@ -184,6 +215,8 @@ export function useDataGridState(params) {
|
|
|
184
215
|
loadingFilterOptions,
|
|
185
216
|
multiSelectFilters,
|
|
186
217
|
onMultiSelectFilterChange,
|
|
218
|
+
dateFilters,
|
|
219
|
+
onDateFilterChange,
|
|
187
220
|
}), [
|
|
188
221
|
sortBy,
|
|
189
222
|
sortDirection,
|
|
@@ -197,6 +230,8 @@ export function useDataGridState(params) {
|
|
|
197
230
|
loadingFilterOptions,
|
|
198
231
|
multiSelectFilters,
|
|
199
232
|
onMultiSelectFilterChange,
|
|
233
|
+
dateFilters,
|
|
234
|
+
onDateFilterChange,
|
|
200
235
|
]);
|
|
201
236
|
const cellDescriptorInput = useMemo(() => ({
|
|
202
237
|
editingCell,
|
|
@@ -302,8 +337,10 @@ export function useDataGridState(params) {
|
|
|
302
337
|
handleFillHandleMouseDown: cellSelection ? handleFillHandleMouseDown : NOOP,
|
|
303
338
|
containerWidth,
|
|
304
339
|
minTableWidth,
|
|
340
|
+
desiredTableWidth,
|
|
305
341
|
columnSizingOverrides,
|
|
306
342
|
setColumnSizingOverrides,
|
|
343
|
+
onColumnResized,
|
|
307
344
|
headerFilterInput,
|
|
308
345
|
cellDescriptorInput,
|
|
309
346
|
commitCellEdit,
|
|
@@ -2,6 +2,32 @@ import { useCallback } from 'react';
|
|
|
2
2
|
import { normalizeSelectionRange } from '../types';
|
|
3
3
|
import { getCellValue } from '../utils';
|
|
4
4
|
import { parseValue } from '../utils/valueParsers';
|
|
5
|
+
/**
|
|
6
|
+
* Excel-style Ctrl+Arrow: find the target position along a 1D axis.
|
|
7
|
+
* - Non-empty current + non-empty next → scan through non-empties, stop at last before empty/edge.
|
|
8
|
+
* - Otherwise → skip empties, land on next non-empty or edge.
|
|
9
|
+
*/
|
|
10
|
+
function findCtrlTarget(pos, edge, step, isEmpty) {
|
|
11
|
+
if (pos === edge)
|
|
12
|
+
return pos;
|
|
13
|
+
const next = pos + step;
|
|
14
|
+
if (!isEmpty(pos) && !isEmpty(next)) {
|
|
15
|
+
let p = next;
|
|
16
|
+
while (p !== edge) {
|
|
17
|
+
if (isEmpty(p + step))
|
|
18
|
+
return p;
|
|
19
|
+
p += step;
|
|
20
|
+
}
|
|
21
|
+
return edge;
|
|
22
|
+
}
|
|
23
|
+
let p = next;
|
|
24
|
+
while (p !== edge) {
|
|
25
|
+
if (!isEmpty(p))
|
|
26
|
+
return p;
|
|
27
|
+
p += step;
|
|
28
|
+
}
|
|
29
|
+
return edge;
|
|
30
|
+
}
|
|
5
31
|
export function useKeyboardNavigation(params) {
|
|
6
32
|
const { items, visibleCols, colOffset, hasCheckboxCol, visibleColumnCount, activeCell, setActiveCell, selectionRange, setSelectionRange, editable, onCellValueChanged, getRowId, editingCell, setEditingCell, rowSelection, selectedRowIds, handleRowCheckboxChange, handleCopy, handleCut, handlePaste, setContextMenu, wrapperRef, onUndo, onRedo, clearClipboardRanges, } = params;
|
|
7
33
|
const maxRowIndex = items.length - 1;
|
|
@@ -28,6 +54,12 @@ export function useKeyboardNavigation(params) {
|
|
|
28
54
|
const { rowIndex, columnIndex } = activeCell;
|
|
29
55
|
const dataColIndex = columnIndex - colOffset;
|
|
30
56
|
const shift = e.shiftKey;
|
|
57
|
+
const isEmptyAt = (r, c) => {
|
|
58
|
+
if (r < 0 || r >= items.length || c < 0 || c >= visibleCols.length)
|
|
59
|
+
return true;
|
|
60
|
+
const v = getCellValue(items[r], visibleCols[c]);
|
|
61
|
+
return v == null || v === '';
|
|
62
|
+
};
|
|
31
63
|
switch (e.key) {
|
|
32
64
|
case 'c':
|
|
33
65
|
if (e.ctrlKey || e.metaKey) {
|
|
@@ -55,7 +87,10 @@ export function useKeyboardNavigation(params) {
|
|
|
55
87
|
break;
|
|
56
88
|
case 'ArrowDown': {
|
|
57
89
|
e.preventDefault();
|
|
58
|
-
const
|
|
90
|
+
const ctrl = e.ctrlKey || e.metaKey;
|
|
91
|
+
const newRow = ctrl
|
|
92
|
+
? findCtrlTarget(rowIndex, maxRowIndex, 1, (r) => isEmptyAt(r, Math.max(0, dataColIndex)))
|
|
93
|
+
: Math.min(rowIndex + 1, maxRowIndex);
|
|
59
94
|
if (shift) {
|
|
60
95
|
setSelectionRange(normalizeSelectionRange({
|
|
61
96
|
startRow: selectionRange?.startRow ?? rowIndex,
|
|
@@ -77,7 +112,10 @@ export function useKeyboardNavigation(params) {
|
|
|
77
112
|
}
|
|
78
113
|
case 'ArrowUp': {
|
|
79
114
|
e.preventDefault();
|
|
80
|
-
const
|
|
115
|
+
const ctrl = e.ctrlKey || e.metaKey;
|
|
116
|
+
const newRowUp = ctrl
|
|
117
|
+
? findCtrlTarget(rowIndex, 0, -1, (r) => isEmptyAt(r, Math.max(0, dataColIndex)))
|
|
118
|
+
: Math.max(rowIndex - 1, 0);
|
|
81
119
|
if (shift) {
|
|
82
120
|
setSelectionRange(normalizeSelectionRange({
|
|
83
121
|
startRow: selectionRange?.startRow ?? rowIndex,
|
|
@@ -99,7 +137,14 @@ export function useKeyboardNavigation(params) {
|
|
|
99
137
|
}
|
|
100
138
|
case 'ArrowRight': {
|
|
101
139
|
e.preventDefault();
|
|
102
|
-
const
|
|
140
|
+
const ctrl = e.ctrlKey || e.metaKey;
|
|
141
|
+
let newCol;
|
|
142
|
+
if (ctrl && dataColIndex >= 0) {
|
|
143
|
+
newCol = findCtrlTarget(dataColIndex, visibleCols.length - 1, 1, (c) => isEmptyAt(rowIndex, c)) + colOffset;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
newCol = Math.min(columnIndex + 1, maxColIndex);
|
|
147
|
+
}
|
|
103
148
|
const newDataCol = newCol - colOffset;
|
|
104
149
|
if (shift) {
|
|
105
150
|
setSelectionRange(normalizeSelectionRange({
|
|
@@ -122,7 +167,14 @@ export function useKeyboardNavigation(params) {
|
|
|
122
167
|
}
|
|
123
168
|
case 'ArrowLeft': {
|
|
124
169
|
e.preventDefault();
|
|
125
|
-
const
|
|
170
|
+
const ctrl = e.ctrlKey || e.metaKey;
|
|
171
|
+
let newColLeft;
|
|
172
|
+
if (ctrl && dataColIndex >= 0) {
|
|
173
|
+
newColLeft = findCtrlTarget(dataColIndex, 0, -1, (c) => isEmptyAt(rowIndex, c)) + colOffset;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
newColLeft = Math.max(columnIndex - 1, colOffset);
|
|
177
|
+
}
|
|
126
178
|
const newDataColLeft = newColLeft - colOffset;
|
|
127
179
|
if (shift) {
|
|
128
180
|
setSelectionRange(normalizeSelectionRange({
|