@opencorestack/opengridx 3.4.0 → 3.5.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/CHANGELOG.md +33 -0
- package/README.md +6 -0
- package/dist/ai.cjs +2 -1
- package/dist/ai.cjs.map +1 -1
- package/dist/ai.d.ts +203 -3
- package/dist/ai.es.js +2 -1
- package/dist/ai.es.js.map +1 -1
- package/dist/index.d.ts +248 -4
- package/dist/opengridx.css +1 -1
- package/dist/opengridx.es.js +7104 -5726
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +16 -16
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +88 -7
- package/docs/README.md +3 -1
- package/docs/architecture/datagrid-orchestration.md +9 -4
- package/docs/components/filter-panel.md +1 -0
- package/docs/components/header.md +4 -1
- package/docs/components/toolbar.md +7 -0
- package/docs/customization/theming.md +3 -1
- package/docs/features/ai-toolkit.md +99 -5
- package/docs/features/cell-selection.md +8 -2
- package/docs/features/fill-handle.md +125 -0
- package/docs/features/filtering.md +3 -0
- package/docs/features/header-filters.md +138 -0
- package/docs/features/keyboard-navigation.md +20 -2
- package/docs/roadmap.md +2 -3
- package/lib/ai/agentTools.ts +160 -0
- package/lib/ai/index.ts +19 -2
- package/lib/ai/promptHandler.ts +109 -0
- package/lib/ai/types.ts +89 -25
- package/lib/ai/validate.ts +10 -0
- package/lib/components/AiAssistant/GridAiAssistantArea.tsx +26 -0
- package/lib/components/AiAssistant/GridAiAssistantPanel.css +171 -0
- package/lib/components/AiAssistant/GridAiAssistantPanel.tsx +145 -0
- package/lib/components/AiAssistant/useSpeechInput.ts +74 -0
- package/lib/components/Cell/Cell.css +28 -0
- package/lib/components/Cell/Cell.tsx +6 -2
- package/lib/components/DataGrid/DataGrid.tsx +76 -3
- package/lib/components/DataGrid/GridLiveRegion.tsx +5 -2
- package/lib/components/DataGrid/GridPinnedRows.tsx +5 -1
- package/lib/components/DataGrid/GridVirtualRows.tsx +5 -1
- package/lib/components/FilterPanel/FilterPanel.tsx +2 -9
- package/lib/components/Header/Header.tsx +44 -23
- package/lib/components/Header/headerColumnStyle.ts +28 -0
- package/lib/components/HeaderFilter/HeaderFilter.css +160 -0
- package/lib/components/HeaderFilter/HeaderFilterCell.tsx +370 -0
- package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +124 -0
- package/lib/components/HeaderFilter/HeaderFilterRow.tsx +157 -0
- package/lib/components/Toolbar/GridToolbar.tsx +51 -0
- package/lib/hooks/core/gridApiPlaceholder.ts +8 -0
- package/lib/hooks/core/gridToolbarHostContext.ts +5 -0
- package/lib/hooks/core/useGridApiMethods.ts +39 -0
- package/lib/hooks/core/useGridAriaRows.ts +5 -3
- package/lib/hooks/core/useGridCellSelection.ts +77 -11
- package/lib/hooks/core/useGridColumnsPanel.ts +24 -1
- package/lib/hooks/core/useGridControlledState.ts +27 -0
- package/lib/hooks/core/useGridFillHandle.ts +323 -0
- package/lib/hooks/core/useGridKeyboardNavigation.ts +29 -10
- package/lib/hooks/core/useGridToolbarProps.ts +19 -0
- package/lib/hooks/features/useGridAiAssistant.ts +284 -0
- package/lib/hooks/features/useGridBatchEdit.ts +2 -2
- package/lib/index.ts +3 -0
- package/lib/styles/opengridx.css +2 -0
- package/lib/theme/DataGridThemeProvider.tsx +2 -0
- package/lib/theme/types.ts +2 -0
- package/lib/types/index.ts +219 -1
- package/lib/utils/aiAssistant.ts +205 -0
- package/lib/utils/aiBrand.ts +17 -0
- package/lib/utils/cellSelection.ts +25 -5
- package/lib/utils/editing/fill.ts +238 -0
- package/lib/utils/filtering/index.ts +14 -0
- package/lib/utils/focus/index.ts +9 -3
- package/lib/utils/headerFilters.ts +189 -0
- package/llms.txt +44 -4
- package/package.json +1 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef } from 'react';
|
|
2
|
+
import type { GridColDef, GridColumnPinning, GridFilterItem, GridFilterModel } from '../../types';
|
|
3
|
+
import { isColumnPinned, SYSTEM_COLUMN_WIDTH } from '../../utils/pinning';
|
|
4
|
+
import { CHECKBOX_FIELD, EXPAND_FIELD, REORDER_FIELD } from '../../utils/focus';
|
|
5
|
+
import { getHeaderFilterState, hasHeaderFilter, setHeaderFilterItem } from '../../utils/headerFilters';
|
|
6
|
+
import { getHeaderColumnStyle } from '../Header/headerColumnStyle';
|
|
7
|
+
import { HeaderFilterCell } from './HeaderFilterCell';
|
|
8
|
+
|
|
9
|
+
export interface HeaderFilterRowProps {
|
|
10
|
+
/** The rendered columns of the column header row (render window with spacers), in render order. */
|
|
11
|
+
columns: GridColDef[];
|
|
12
|
+
columnWidths: Record<string, number>;
|
|
13
|
+
pinnedColumns?: GridColumnPinning;
|
|
14
|
+
/** Sticky offsets of the pinned columns (`calculatePinnedPositions`). */
|
|
15
|
+
pinnedPositions: Record<string, number>;
|
|
16
|
+
pinnedEdges: { lastLeft: string | null; firstRight: string | null };
|
|
17
|
+
rowReordering: boolean;
|
|
18
|
+
hasDetailPanel: boolean;
|
|
19
|
+
checkboxSelection: boolean;
|
|
20
|
+
pinExpandColumn: boolean;
|
|
21
|
+
pinCheckboxColumn: boolean;
|
|
22
|
+
columnIndexMap?: Map<string, number>;
|
|
23
|
+
/** `aria-rowindex` of the row: right after the column header row. */
|
|
24
|
+
ariaRowIndex: number;
|
|
25
|
+
filterModel?: GridFilterModel;
|
|
26
|
+
onFilterModelChange?: (model: GridFilterModel) => void;
|
|
27
|
+
onOpenFilterPanel?: () => void;
|
|
28
|
+
/** Field of the focused filter cell, or null. */
|
|
29
|
+
focusedField: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function SystemFilterCell({ field, ariaColIndex, sticky, left, focused }: {
|
|
33
|
+
field: string;
|
|
34
|
+
ariaColIndex: number;
|
|
35
|
+
sticky: boolean;
|
|
36
|
+
left: number;
|
|
37
|
+
focused: boolean;
|
|
38
|
+
}) {
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
className={[
|
|
42
|
+
'ogx__header-filter-cell',
|
|
43
|
+
'ogx__header-filter-cell--system',
|
|
44
|
+
sticky && 'ogx__header-filter-cell--pinned ogx__header-filter-cell--pinned-left',
|
|
45
|
+
focused && 'ogx__header-filter-cell--focused',
|
|
46
|
+
].filter(Boolean).join(' ')}
|
|
47
|
+
style={{
|
|
48
|
+
width: SYSTEM_COLUMN_WIDTH,
|
|
49
|
+
minWidth: SYSTEM_COLUMN_WIDTH,
|
|
50
|
+
maxWidth: SYSTEM_COLUMN_WIDTH,
|
|
51
|
+
position: sticky ? 'sticky' : undefined,
|
|
52
|
+
left: sticky ? left : undefined,
|
|
53
|
+
}}
|
|
54
|
+
role="columnheader"
|
|
55
|
+
aria-colindex={ariaColIndex}
|
|
56
|
+
data-field={field}
|
|
57
|
+
tabIndex={-1}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The header filter row (`headerFilters`): one cell per rendered column, laid out like the column
|
|
64
|
+
* header row above it, so pinned and virtualized columns line up. It edits the shared filter model
|
|
65
|
+
* through the root-level items it owns (`header:<field>`).
|
|
66
|
+
*/
|
|
67
|
+
export function HeaderFilterRow(props: HeaderFilterRowProps) {
|
|
68
|
+
const {
|
|
69
|
+
columns, columnWidths, pinnedColumns, pinnedPositions, pinnedEdges, rowReordering, hasDetailPanel,
|
|
70
|
+
checkboxSelection, pinExpandColumn, pinCheckboxColumn, columnIndexMap, ariaRowIndex, filterModel,
|
|
71
|
+
onFilterModelChange, onOpenFilterPanel, focusedField,
|
|
72
|
+
} = props;
|
|
73
|
+
|
|
74
|
+
// Cells commit from timers and unmount cleanups: they read the newest model, and two commits
|
|
75
|
+
// before the next render build on each other.
|
|
76
|
+
const latestRef = useRef({ filterModel, onFilterModelChange });
|
|
77
|
+
useLayoutEffect(() => {
|
|
78
|
+
latestRef.current = { filterModel, onFilterModelChange };
|
|
79
|
+
});
|
|
80
|
+
const handleItemChange = useCallback((field: string, item: GridFilterItem | null) => {
|
|
81
|
+
const { filterModel: current, onFilterModelChange: emit } = latestRef.current;
|
|
82
|
+
if (!emit) return;
|
|
83
|
+
const next = setHeaderFilterItem(current, field, item);
|
|
84
|
+
if (next === current) return;
|
|
85
|
+
latestRef.current = { filterModel: next, onFilterModelChange: emit };
|
|
86
|
+
emit(next);
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
const reorderColIndex = rowReordering ? 1 : 0;
|
|
90
|
+
const expandColIndex = hasDetailPanel ? reorderColIndex + 1 : reorderColIndex;
|
|
91
|
+
const checkboxColIndex = checkboxSelection ? expandColIndex + 1 : expandColIndex;
|
|
92
|
+
const systemColumnCount = checkboxColIndex;
|
|
93
|
+
const expandLeft = rowReordering ? SYSTEM_COLUMN_WIDTH : 0;
|
|
94
|
+
const checkboxLeft = expandLeft + (hasDetailPanel && pinExpandColumn ? SYSTEM_COLUMN_WIDTH : 0);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="ogx__header-filter-row" role="row" aria-rowindex={ariaRowIndex}>
|
|
98
|
+
{rowReordering && (
|
|
99
|
+
<SystemFilterCell field={REORDER_FIELD} ariaColIndex={reorderColIndex} sticky left={0} focused={focusedField === REORDER_FIELD} />
|
|
100
|
+
)}
|
|
101
|
+
{hasDetailPanel && (
|
|
102
|
+
<SystemFilterCell field={EXPAND_FIELD} ariaColIndex={expandColIndex} sticky={pinExpandColumn} left={expandLeft} focused={focusedField === EXPAND_FIELD} />
|
|
103
|
+
)}
|
|
104
|
+
{checkboxSelection && (
|
|
105
|
+
<SystemFilterCell field={CHECKBOX_FIELD} ariaColIndex={checkboxColIndex} sticky={pinCheckboxColumn} left={checkboxLeft} focused={focusedField === CHECKBOX_FIELD} />
|
|
106
|
+
)}
|
|
107
|
+
{columns.map((colDef, index) => {
|
|
108
|
+
if (colDef.isSpacer) {
|
|
109
|
+
return (
|
|
110
|
+
<div
|
|
111
|
+
key={colDef.field}
|
|
112
|
+
style={{ width: colDef.width, minWidth: colDef.width, flexShrink: 0 }}
|
|
113
|
+
aria-hidden="true"
|
|
114
|
+
/>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const pinnedPosition = isColumnPinned(colDef.field, pinnedColumns);
|
|
118
|
+
const style = getHeaderColumnStyle(colDef, columnWidths, pinnedPosition, pinnedPosition ? pinnedPositions[colDef.field] : undefined);
|
|
119
|
+
const ariaColIndex = systemColumnCount + (columnIndexMap?.get(colDef.field) ?? index) + 1;
|
|
120
|
+
const className = [
|
|
121
|
+
pinnedPosition && 'ogx__header-filter-cell--pinned',
|
|
122
|
+
pinnedPosition && `ogx__header-filter-cell--pinned-${pinnedPosition}`,
|
|
123
|
+
pinnedPosition === 'left' && colDef.field === pinnedEdges.lastLeft && 'ogx__header-filter-cell--pinned-left-last',
|
|
124
|
+
pinnedPosition === 'right' && colDef.field === pinnedEdges.firstRight && 'ogx__header-filter-cell--pinned-right-first',
|
|
125
|
+
focusedField === colDef.field && 'ogx__header-filter-cell--focused',
|
|
126
|
+
].filter(Boolean).join(' ');
|
|
127
|
+
|
|
128
|
+
if (!hasHeaderFilter(colDef)) {
|
|
129
|
+
return (
|
|
130
|
+
<div
|
|
131
|
+
key={colDef.field}
|
|
132
|
+
className={`ogx__header-filter-cell ogx__header-filter-cell--empty ${className}`}
|
|
133
|
+
style={style}
|
|
134
|
+
role="columnheader"
|
|
135
|
+
aria-label={`No filter for ${colDef.headerName || colDef.field}`}
|
|
136
|
+
aria-colindex={ariaColIndex}
|
|
137
|
+
data-field={colDef.field}
|
|
138
|
+
tabIndex={-1}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return (
|
|
143
|
+
<HeaderFilterCell
|
|
144
|
+
key={colDef.field}
|
|
145
|
+
colDef={colDef}
|
|
146
|
+
state={getHeaderFilterState(filterModel, colDef.field)}
|
|
147
|
+
style={style}
|
|
148
|
+
className={className}
|
|
149
|
+
ariaColIndex={ariaColIndex}
|
|
150
|
+
onItemChange={handleItemChange}
|
|
151
|
+
onOpenFilterPanel={onOpenFilterPanel}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
})}
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -10,6 +10,7 @@ import { GlobalSearch } from './GlobalSearch';
|
|
|
10
10
|
import { ColumnVisibilityPanel } from '../ColumnVisibilityPanel/ColumnVisibilityPanel';
|
|
11
11
|
import { FilterPanel } from '../FilterPanel/FilterPanel';
|
|
12
12
|
import { GridTooltip } from '../Tooltip/Tooltip';
|
|
13
|
+
import { AiSparkleIcon } from '../AiAssistant/GridAiAssistantPanel';
|
|
13
14
|
|
|
14
15
|
/** Props passed to a custom toolbar button renderer. */
|
|
15
16
|
export interface ToolbarButtonRenderProps {
|
|
@@ -48,6 +49,10 @@ export interface GridToolbarProps {
|
|
|
48
49
|
onColumnOrderReset?: () => void;
|
|
49
50
|
forceColumnsOpen?: boolean;
|
|
50
51
|
onColumnsPanelClose?: () => void;
|
|
52
|
+
/** Opens the filter panel when it turns true (the grid passes it for the header filter row). @since v3.5 */
|
|
53
|
+
forceFiltersOpen?: boolean;
|
|
54
|
+
/** Called whenever the filter panel closes. @since v3.5 */
|
|
55
|
+
onFiltersPanelClose?: () => void;
|
|
51
56
|
children?: React.ReactNode;
|
|
52
57
|
rightContent?: React.ReactNode;
|
|
53
58
|
style?: React.CSSProperties;
|
|
@@ -80,6 +85,16 @@ export interface GridToolbarProps {
|
|
|
80
85
|
className?: string;
|
|
81
86
|
/** Show columns with `hideable: false` in the Columns panel as disabled rows. Default: false. */
|
|
82
87
|
showNonHideableColumns?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Opens or closes the `aiAssistant` panel. The grid passes it when `aiAssistant` is set; the
|
|
90
|
+
* **Ask AI** button is shown only with it. Receives the button, which gets focus back on close.
|
|
91
|
+
* @since v3.5
|
|
92
|
+
*/
|
|
93
|
+
onAiAssistantToggle?: (trigger: HTMLElement | null) => void;
|
|
94
|
+
/** Whether the `aiAssistant` panel is open. @since v3.5 */
|
|
95
|
+
aiAssistantOpen?: boolean;
|
|
96
|
+
/** Label of the Ask AI button (`localeText.aiAssistantButton`). Default: `"Ask AI"`. @since v3.5 */
|
|
97
|
+
aiAssistantLabel?: string;
|
|
83
98
|
}
|
|
84
99
|
|
|
85
100
|
const BUILT_IN_AGGREGATION_FUNCTIONS = Object.keys(AGGREGATION_FUNCTIONS) as BuiltInAggFn[];
|
|
@@ -492,6 +507,8 @@ export function GridToolbar({
|
|
|
492
507
|
onColumnOrderReset,
|
|
493
508
|
forceColumnsOpen,
|
|
494
509
|
onColumnsPanelClose,
|
|
510
|
+
forceFiltersOpen,
|
|
511
|
+
onFiltersPanelClose,
|
|
495
512
|
children,
|
|
496
513
|
rightContent,
|
|
497
514
|
style,
|
|
@@ -502,6 +519,9 @@ export function GridToolbar({
|
|
|
502
519
|
renderQuickFilter,
|
|
503
520
|
className,
|
|
504
521
|
showNonHideableColumns,
|
|
522
|
+
onAiAssistantToggle,
|
|
523
|
+
aiAssistantOpen = false,
|
|
524
|
+
aiAssistantLabel = 'Ask AI',
|
|
505
525
|
}: GridToolbarProps) {
|
|
506
526
|
// At most one panel is open. Every way the Columns panel closes (its button, a custom
|
|
507
527
|
// button, another panel opening, click-outside, Escape) reports onColumnsPanelClose, so the
|
|
@@ -514,6 +534,7 @@ export function GridToolbar({
|
|
|
514
534
|
|
|
515
535
|
const switchPanel = (next: ToolbarPanel | null) => {
|
|
516
536
|
if (openPanel === 'columns' && next !== 'columns') onColumnsPanelClose?.();
|
|
537
|
+
if (openPanel === 'filters' && next !== 'filters') onFiltersPanelClose?.();
|
|
517
538
|
setOpenPanel(next);
|
|
518
539
|
};
|
|
519
540
|
const togglePanel = (panel: ToolbarPanel) => switchPanel(openPanel === panel ? null : panel);
|
|
@@ -526,6 +547,12 @@ export function GridToolbar({
|
|
|
526
547
|
setSeenForceColumnsOpen(forceColumnsOpen);
|
|
527
548
|
if (forceColumnsOpen) setOpenPanel('columns');
|
|
528
549
|
}
|
|
550
|
+
// forceFiltersOpen turning true opens the filter panel, the same way.
|
|
551
|
+
const [seenForceFiltersOpen, setSeenForceFiltersOpen] = useState(forceFiltersOpen);
|
|
552
|
+
if (seenForceFiltersOpen !== forceFiltersOpen) {
|
|
553
|
+
setSeenForceFiltersOpen(forceFiltersOpen);
|
|
554
|
+
if (forceFiltersOpen && onFilterModelChange) setOpenPanel('filters');
|
|
555
|
+
}
|
|
529
556
|
|
|
530
557
|
// Tell the grid this toolbar shows the Columns panel for the column menu; it can only do so
|
|
531
558
|
// when the grid's forceColumnsOpen reaches it and the Columns panel is enabled.
|
|
@@ -535,8 +562,14 @@ export function GridToolbar({
|
|
|
535
562
|
if (!toolbarHost || !hostsColumnsPanel) return undefined;
|
|
536
563
|
return toolbarHost.registerColumnsPanel();
|
|
537
564
|
}, [toolbarHost, hostsColumnsPanel]);
|
|
565
|
+
const hostsFiltersPanel = forceFiltersOpen !== undefined && Boolean(onFilterModelChange);
|
|
566
|
+
useEffect(() => {
|
|
567
|
+
if (!toolbarHost || !hostsFiltersPanel) return undefined;
|
|
568
|
+
return toolbarHost.registerFiltersPanel();
|
|
569
|
+
}, [toolbarHost, hostsFiltersPanel]);
|
|
538
570
|
|
|
539
571
|
const aggButtonRef = useRef<HTMLButtonElement>(null);
|
|
572
|
+
const aiButtonRef = useRef<HTMLButtonElement>(null);
|
|
540
573
|
const pivotButtonRef = useRef<HTMLButtonElement>(null);
|
|
541
574
|
const colsButtonRef = useRef<HTMLButtonElement>(null);
|
|
542
575
|
const filterButtonRef = useRef<HTMLButtonElement>(null);
|
|
@@ -642,6 +675,24 @@ export function GridToolbar({
|
|
|
642
675
|
|
|
643
676
|
{/* Right: built-in buttons */}
|
|
644
677
|
<div className="ogx-toolbar__actions">
|
|
678
|
+
{onAiAssistantToggle && (
|
|
679
|
+
<div className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
|
|
680
|
+
<button
|
|
681
|
+
type="button"
|
|
682
|
+
ref={aiButtonRef}
|
|
683
|
+
className={`ogx-toolbar__icon-btn ogx-toolbar__ai-btn${aiAssistantOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
|
|
684
|
+
aria-haspopup="dialog"
|
|
685
|
+
aria-expanded={aiAssistantOpen}
|
|
686
|
+
onClick={() => {
|
|
687
|
+
switchPanel(null);
|
|
688
|
+
onAiAssistantToggle(aiButtonRef.current);
|
|
689
|
+
}}
|
|
690
|
+
>
|
|
691
|
+
<AiSparkleIcon />
|
|
692
|
+
{aiAssistantLabel}
|
|
693
|
+
</button>
|
|
694
|
+
</div>
|
|
695
|
+
)}
|
|
645
696
|
{onFilterModelChange && (
|
|
646
697
|
<div className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
|
|
647
698
|
{renderQuickFilter
|
|
@@ -45,5 +45,13 @@ export function createGridApiPlaceholder<R extends GridValidRowModel = GridRowMo
|
|
|
45
45
|
canUndo: () => false,
|
|
46
46
|
canRedo: () => false,
|
|
47
47
|
clearHistory: noop,
|
|
48
|
+
setSortModel: noop,
|
|
49
|
+
setRowGroupingModel: noop,
|
|
50
|
+
setAggregationModel: noop,
|
|
51
|
+
setColumnVisibilityModel: noop,
|
|
52
|
+
setPivotModel: noop,
|
|
53
|
+
getGridAiState: () => ({}),
|
|
54
|
+
openAiAssistant: noop,
|
|
55
|
+
closeAiAssistant: noop,
|
|
48
56
|
};
|
|
49
57
|
}
|
|
@@ -9,6 +9,11 @@ import { createContext } from 'react';
|
|
|
9
9
|
export interface GridToolbarHost {
|
|
10
10
|
/** Registers a toolbar that can show the Columns panel. Returns the function that unregisters it. */
|
|
11
11
|
registerColumnsPanel: () => () => void;
|
|
12
|
+
/**
|
|
13
|
+
* Registers a toolbar that can show the filter panel when the header filter row asks for it
|
|
14
|
+
* (through `forceFiltersOpen`). Returns the function that unregisters it.
|
|
15
|
+
*/
|
|
16
|
+
registerFiltersPanel: () => () => void;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
export const GridToolbarHostContext = createContext<GridToolbarHost | null>(null);
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { useLayoutEffect, useRef } from 'react';
|
|
2
2
|
import type {
|
|
3
|
+
GridAggregationModel,
|
|
3
4
|
GridApi,
|
|
4
5
|
GridColDef,
|
|
6
|
+
GridColumnVisibilityModel,
|
|
5
7
|
GridFilterModel,
|
|
6
8
|
GridPaginationModel,
|
|
9
|
+
GridPivotModel,
|
|
10
|
+
GridRowGroupingModel,
|
|
7
11
|
GridRowId,
|
|
8
12
|
GridRowModel,
|
|
9
13
|
GridSortItem,
|
|
@@ -45,6 +49,16 @@ export interface UseGridApiMethodsParams {
|
|
|
45
49
|
getAllFilteredRows: () => GridRowModel[];
|
|
46
50
|
/** Columns on screen, in display order. */
|
|
47
51
|
visibleColumns: GridColDef[];
|
|
52
|
+
|
|
53
|
+
// v3.5: the remaining models, for the setters and getGridAiState.
|
|
54
|
+
rowGroupingModel: GridRowGroupingModel;
|
|
55
|
+
onRowGroupingModelChange: (model: GridRowGroupingModel) => void;
|
|
56
|
+
aggregationModel: GridAggregationModel;
|
|
57
|
+
onAggregationModelChange: (model: GridAggregationModel) => void;
|
|
58
|
+
columnVisibilityModel: GridColumnVisibilityModel;
|
|
59
|
+
onColumnVisibilityModelChange: (model: GridColumnVisibilityModel) => void;
|
|
60
|
+
pivotModel: GridPivotModel;
|
|
61
|
+
onPivotModelChange: (model: GridPivotModel) => void;
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
/**
|
|
@@ -111,5 +125,30 @@ export function useGridApiMethods(params: UseGridApiMethodsParams): void {
|
|
|
111
125
|
api.getVisibleRows = () => latest().getVisibleRows();
|
|
112
126
|
api.getAllFilteredRows = () => latest().getAllFilteredRows();
|
|
113
127
|
api.getVisibleColumns = () => latest().visibleColumns;
|
|
128
|
+
|
|
129
|
+
api.setSortModel = (model) => {
|
|
130
|
+
latestRef.current = { ...latest(), sortModel: model };
|
|
131
|
+
latest().onSortModelChange(model);
|
|
132
|
+
};
|
|
133
|
+
api.setRowGroupingModel = (model) => {
|
|
134
|
+
latestRef.current = { ...latest(), rowGroupingModel: model };
|
|
135
|
+
latest().onRowGroupingModelChange(model);
|
|
136
|
+
};
|
|
137
|
+
api.setAggregationModel = (model) => {
|
|
138
|
+
latestRef.current = { ...latest(), aggregationModel: model };
|
|
139
|
+
latest().onAggregationModelChange(model);
|
|
140
|
+
};
|
|
141
|
+
api.setColumnVisibilityModel = (model) => {
|
|
142
|
+
latestRef.current = { ...latest(), columnVisibilityModel: model };
|
|
143
|
+
latest().onColumnVisibilityModelChange(model);
|
|
144
|
+
};
|
|
145
|
+
api.setPivotModel = (model) => {
|
|
146
|
+
latestRef.current = { ...latest(), pivotModel: model };
|
|
147
|
+
latest().onPivotModelChange(model);
|
|
148
|
+
};
|
|
149
|
+
api.getGridAiState = () => {
|
|
150
|
+
const { filterModel, sortModel, rowGroupingModel, aggregationModel, pivotModel, columnVisibilityModel } = latest();
|
|
151
|
+
return { filterModel, sortModel, rowGroupingModel, aggregationModel, pivotModel, columnVisibilityModel };
|
|
152
|
+
};
|
|
114
153
|
}, [apiRef]);
|
|
115
154
|
}
|
|
@@ -5,6 +5,8 @@ import type { GridColumnGroupingModel, GridPaginationModel } from '../../types';
|
|
|
5
5
|
|
|
6
6
|
export interface UseGridAriaRowsParams {
|
|
7
7
|
columnGroupingModel: GridColumnGroupingModel | undefined;
|
|
8
|
+
/** Whether the header filter row (`headerFilters`) is rendered: one more header row. */
|
|
9
|
+
headerFilterRow?: boolean;
|
|
8
10
|
pinnedTopCount: number;
|
|
9
11
|
pinnedBottomCount: number;
|
|
10
12
|
pagination: boolean;
|
|
@@ -19,13 +21,13 @@ export interface UseGridAriaRowsParams {
|
|
|
19
21
|
/** `aria-rowcount` and the `aria-rowindex` bases of the pinned and centre rows. */
|
|
20
22
|
export function useGridAriaRows(params: UseGridAriaRowsParams): GridAriaRowLayout {
|
|
21
23
|
const {
|
|
22
|
-
columnGroupingModel, pinnedTopCount, pinnedBottomCount, pagination, paginationMode, paginationModel,
|
|
24
|
+
columnGroupingModel, headerFilterRow = false, pinnedTopCount, pinnedBottomCount, pagination, paginationMode, paginationModel,
|
|
23
25
|
paginationRowCount, paginatedUnpinnedRowCount, sortedUnpinnedRowCount,
|
|
24
26
|
} = params;
|
|
25
27
|
const { page, pageSize } = paginationModel;
|
|
26
28
|
|
|
27
29
|
return useMemo(() => getAriaRowLayout({
|
|
28
|
-
headerRowCount: 1 + getColumnGroupDepth(columnGroupingModel),
|
|
30
|
+
headerRowCount: 1 + getColumnGroupDepth(columnGroupingModel) + (headerFilterRow ? 1 : 0),
|
|
29
31
|
pinnedTopCount,
|
|
30
32
|
pinnedBottomCount,
|
|
31
33
|
pageRowCount: pagination ? paginatedUnpinnedRowCount : sortedUnpinnedRowCount,
|
|
@@ -33,6 +35,6 @@ export function useGridAriaRows(params: UseGridAriaRowsParams): GridAriaRowLayou
|
|
|
33
35
|
? Math.max(paginationRowCount, paginatedUnpinnedRowCount)
|
|
34
36
|
: sortedUnpinnedRowCount,
|
|
35
37
|
pageOffset: pagination ? page * pageSize : 0,
|
|
36
|
-
}), [columnGroupingModel, pinnedTopCount, pinnedBottomCount, pagination, paginatedUnpinnedRowCount,
|
|
38
|
+
}), [columnGroupingModel, headerFilterRow, pinnedTopCount, pinnedBottomCount, pagination, paginatedUnpinnedRowCount,
|
|
37
39
|
sortedUnpinnedRowCount, paginationMode, paginationRowCount, page, pageSize]);
|
|
38
40
|
}
|
|
@@ -121,6 +121,8 @@ export interface UseGridCellSelectionReturn {
|
|
|
121
121
|
* address the renderable rows and the range columns of `navigationColumns` (`isRangeColumnField`).
|
|
122
122
|
*/
|
|
123
123
|
getCurrentRange: () => { range: GridCellSelectionModel[number] | null; resolved: ResolvedCellRange | null };
|
|
124
|
+
/** Starts a tracked pointer drag with the range drag's auto-scroll (fill handle). Stable. @since v3.5 */
|
|
125
|
+
startTrackedDrag: (options: GridTrackedDragOptions) => void;
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
interface DragState {
|
|
@@ -130,7 +132,24 @@ interface DragState {
|
|
|
130
132
|
y: number;
|
|
131
133
|
frame: number;
|
|
132
134
|
moved: boolean;
|
|
133
|
-
|
|
135
|
+
/** Ends the drag; `cancelled` unless the button was released normally (tracked drags: Escape cancels). */
|
|
136
|
+
stop: (cancelled?: boolean, altKey?: boolean) => void;
|
|
137
|
+
/** A tracked drag (fill handle): told the cell under the pointer each frame instead of moving the range. */
|
|
138
|
+
onTrack?: (head: GridCellCoordinates, x: number, y: number) => void;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** A drag that reuses the range drag's hit testing and auto-scroll without selecting (the fill handle, v3.5). */
|
|
142
|
+
export interface GridTrackedDragOptions {
|
|
143
|
+
/** The cell the drag starts on. */
|
|
144
|
+
start: GridCellCoordinates;
|
|
145
|
+
x: number;
|
|
146
|
+
y: number;
|
|
147
|
+
/** Pointer events of this pointer are followed (touch included), not mouse events. */
|
|
148
|
+
pointerId: number;
|
|
149
|
+
/** Each frame: the cell under the pointer (or nearest it) and the pointer position. */
|
|
150
|
+
onTrack: (head: GridCellCoordinates, x: number, y: number) => void;
|
|
151
|
+
/** Once: released (`cancelled: false`, with the Alt key state) or cancelled. */
|
|
152
|
+
onEnd: (details: { cancelled: boolean; altKey: boolean }) => void;
|
|
134
153
|
}
|
|
135
154
|
|
|
136
155
|
/** The body cell of `viewport` holding `target`: its row index and field. Not cells of nested grids or detail panels. */
|
|
@@ -451,8 +470,10 @@ export function useGridCellSelection<R extends GridRowModel>(params: UseGridCell
|
|
|
451
470
|
latestRef.current.containerRef.current?.classList.add(RANGE_DRAGGING_CLASS);
|
|
452
471
|
clearTextSelection(viewport.ownerDocument);
|
|
453
472
|
}
|
|
454
|
-
setRange(drag.anchor, head, 'pointer');
|
|
473
|
+
if (!drag.onTrack) setRange(drag.anchor, head, 'pointer');
|
|
455
474
|
}
|
|
475
|
+
// A tracked drag hears every frame: its target can change while the head stays in one cell.
|
|
476
|
+
if (drag.onTrack) drag.onTrack(drag.head, drag.x, drag.y);
|
|
456
477
|
|
|
457
478
|
if (!allowScroll) return;
|
|
458
479
|
const outside = drag.y < bodyTop || drag.y > bodyBottom || drag.x < box.left || drag.x > box.left + width;
|
|
@@ -475,11 +496,22 @@ export function useGridCellSelection<R extends GridRowModel>(params: UseGridCell
|
|
|
475
496
|
runDragFrameRef.current = runDragFrame;
|
|
476
497
|
}, [runDragFrame]);
|
|
477
498
|
|
|
478
|
-
const startDrag = useCallback((
|
|
499
|
+
const startDrag = useCallback((
|
|
500
|
+
anchor: GridCellCoordinates,
|
|
501
|
+
head: GridCellCoordinates,
|
|
502
|
+
x: number,
|
|
503
|
+
y: number,
|
|
504
|
+
doc: Document,
|
|
505
|
+
tracked?: Pick<GridTrackedDragOptions, 'pointerId' | 'onTrack' | 'onEnd'>,
|
|
506
|
+
) => {
|
|
479
507
|
dragRef.current?.stop();
|
|
508
|
+
// A range drag follows the mouse; a tracked drag (fill handle) follows its pointer, touch included.
|
|
509
|
+
const moveType = tracked ? 'pointermove' : 'mousemove';
|
|
510
|
+
const upType = tracked ? 'pointerup' : 'mouseup';
|
|
511
|
+
const isOwnEvent = (event: MouseEvent) => !tracked || (event as PointerEvent).pointerId === tracked.pointerId;
|
|
480
512
|
const onMove = (event: MouseEvent) => {
|
|
481
513
|
const drag = dragRef.current;
|
|
482
|
-
if (!drag) return;
|
|
514
|
+
if (!drag || !isOwnEvent(event)) return;
|
|
483
515
|
if (event.buttons === 0) {
|
|
484
516
|
// Released outside the window.
|
|
485
517
|
drag.stop();
|
|
@@ -497,12 +529,12 @@ export function useGridCellSelection<R extends GridRowModel>(params: UseGridCell
|
|
|
497
529
|
};
|
|
498
530
|
const onUp = (event: MouseEvent) => {
|
|
499
531
|
const drag = dragRef.current;
|
|
500
|
-
if (!drag) return;
|
|
532
|
+
if (!drag || !isOwnEvent(event)) return;
|
|
501
533
|
drag.x = event.clientX;
|
|
502
534
|
drag.y = event.clientY;
|
|
503
535
|
runDragFrame(false);
|
|
504
536
|
const moved = drag.moved;
|
|
505
|
-
drag.stop();
|
|
537
|
+
drag.stop(false, event.altKey);
|
|
506
538
|
if (moved) clearTextSelection(doc);
|
|
507
539
|
if (moved) {
|
|
508
540
|
// The click that follows lands on the common ancestor of both cells (often the row):
|
|
@@ -511,26 +543,57 @@ export function useGridCellSelection<R extends GridRowModel>(params: UseGridCell
|
|
|
511
543
|
setTimeout(() => { suppressClickRef.current = false; }, 0);
|
|
512
544
|
}
|
|
513
545
|
};
|
|
546
|
+
const onCancel = (event: Event) => {
|
|
547
|
+
if (isOwnEvent(event as MouseEvent)) dragRef.current?.stop();
|
|
548
|
+
};
|
|
549
|
+
// Escape abandons a tracked drag (the fill writes nothing).
|
|
550
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
551
|
+
if (event.key !== 'Escape' || !dragRef.current) return;
|
|
552
|
+
event.preventDefault();
|
|
553
|
+
event.stopPropagation();
|
|
554
|
+
dragRef.current.stop();
|
|
555
|
+
};
|
|
514
556
|
const view = doc.defaultView;
|
|
515
557
|
const onBlur = () => dragRef.current?.stop();
|
|
558
|
+
let ended = false;
|
|
516
559
|
const drag: DragState = {
|
|
517
560
|
anchor, head, x, y, frame: 0, moved: false,
|
|
518
|
-
|
|
561
|
+
onTrack: tracked?.onTrack,
|
|
562
|
+
stop: (cancelled = true, altKey = false) => {
|
|
519
563
|
if (drag.frame) cancelAnimationFrame(drag.frame);
|
|
520
564
|
drag.frame = 0;
|
|
521
|
-
doc.removeEventListener(
|
|
522
|
-
doc.removeEventListener(
|
|
565
|
+
doc.removeEventListener(moveType, onMove, true);
|
|
566
|
+
doc.removeEventListener(upType, onUp, true);
|
|
567
|
+
if (tracked) {
|
|
568
|
+
doc.removeEventListener('pointercancel', onCancel, true);
|
|
569
|
+
doc.removeEventListener('keydown', onKeyDown, true);
|
|
570
|
+
}
|
|
523
571
|
view?.removeEventListener('blur', onBlur);
|
|
524
572
|
latestRef.current.containerRef.current?.classList.remove(RANGE_DRAGGING_CLASS);
|
|
525
573
|
if (dragRef.current === drag) dragRef.current = null;
|
|
574
|
+
if (!ended) {
|
|
575
|
+
ended = true;
|
|
576
|
+
if (tracked) tracked.onEnd({ cancelled, altKey });
|
|
577
|
+
}
|
|
526
578
|
},
|
|
527
579
|
};
|
|
528
580
|
dragRef.current = drag;
|
|
529
|
-
doc.addEventListener(
|
|
530
|
-
doc.addEventListener(
|
|
581
|
+
doc.addEventListener(moveType, onMove, true);
|
|
582
|
+
doc.addEventListener(upType, onUp, true);
|
|
583
|
+
if (tracked) {
|
|
584
|
+
doc.addEventListener('pointercancel', onCancel, true);
|
|
585
|
+
doc.addEventListener('keydown', onKeyDown, true);
|
|
586
|
+
}
|
|
531
587
|
view?.addEventListener('blur', onBlur);
|
|
532
588
|
}, [runDragFrame]);
|
|
533
589
|
|
|
590
|
+
const startTrackedDrag = useCallback((options: GridTrackedDragOptions) => {
|
|
591
|
+
const viewport = latestRef.current.viewportRef.current;
|
|
592
|
+
if (!viewport || !latestRef.current.enabled) return;
|
|
593
|
+
const { start, x, y, pointerId, onTrack, onEnd } = options;
|
|
594
|
+
startDrag(start, start, x, y, viewport.ownerDocument, { pointerId, onTrack, onEnd });
|
|
595
|
+
}, [startDrag]);
|
|
596
|
+
|
|
534
597
|
const handleMouseDown = useCallback((event: React.MouseEvent<HTMLElement>) => {
|
|
535
598
|
if (event.button !== 0 || event.defaultPrevented) return;
|
|
536
599
|
const { context: ctx } = latestRef.current;
|
|
@@ -538,6 +601,8 @@ export function useGridCellSelection<R extends GridRowModel>(params: UseGridCell
|
|
|
538
601
|
const viewport = event.currentTarget;
|
|
539
602
|
const hit = cellFromTarget(event.target, viewport);
|
|
540
603
|
if (!hit || !isRangeColumnField(hit.field)) return;
|
|
604
|
+
// The fill handle starts its own drag (useGridFillHandle).
|
|
605
|
+
if ((event.target as Element).closest?.('.ogx__cell-fill-handle')) return;
|
|
541
606
|
// Typing targets and open editors keep the pointer to themselves.
|
|
542
607
|
if (hit.element.classList.contains('ogx__cell--editing') || isTextEntryElement(event.target as Element)) return;
|
|
543
608
|
const id = ctx.rowIds[hit.rowIndex];
|
|
@@ -626,6 +691,7 @@ export function useGridCellSelection<R extends GridRowModel>(params: UseGridCell
|
|
|
626
691
|
reportFocus,
|
|
627
692
|
setModel,
|
|
628
693
|
getCurrentRange: currentRange,
|
|
694
|
+
startTrackedDrag,
|
|
629
695
|
};
|
|
630
696
|
}
|
|
631
697
|
|
|
@@ -16,26 +16,46 @@ export interface UseGridColumnsPanelResult {
|
|
|
16
16
|
standalonePanelRef: RefObject<HTMLDivElement | null>;
|
|
17
17
|
/** The column menu's **Manage columns** action. */
|
|
18
18
|
openColumnsPanel: () => void;
|
|
19
|
+
/** Passed to the toolbar as `forceFiltersOpen`. */
|
|
20
|
+
toolbarFiltersRequested: boolean;
|
|
21
|
+
/** Passed to the toolbar as `onFiltersPanelClose`. */
|
|
22
|
+
closeToolbarFiltersPanel: () => void;
|
|
23
|
+
/**
|
|
24
|
+
* Opens the toolbar's filter panel (a header filter cell's "Custom filter"); undefined while no
|
|
25
|
+
* `GridToolbar` that can show it is rendered.
|
|
26
|
+
*/
|
|
27
|
+
openFiltersPanel: (() => void) | undefined;
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
/**
|
|
22
31
|
* Opens the Columns panel for the column menu: in the toolbar when a `GridToolbar` has registered
|
|
23
32
|
* through `GridToolbarHostContext`, otherwise as the standalone panel, which closes on Escape or a
|
|
24
|
-
* mousedown outside it.
|
|
33
|
+
* mousedown outside it. Also opens the toolbar's filter panel for the header filter row, when a
|
|
34
|
+
* `GridToolbar` has registered that it can show it.
|
|
25
35
|
*/
|
|
26
36
|
export function useGridColumnsPanel(): UseGridColumnsPanelResult {
|
|
27
37
|
const [toolbarPanelRequested, setToolbarPanelRequested] = useState(false);
|
|
28
38
|
const [standalonePanelOpen, setStandalonePanelOpen] = useState(false);
|
|
29
39
|
const standalonePanelRef = useRef<HTMLDivElement>(null);
|
|
30
40
|
const toolbarHostCountRef = useRef(0);
|
|
41
|
+
// State, not a ref: whether "Custom filter" can open the panel decides how it renders.
|
|
42
|
+
const [filtersHostCount, setFiltersHostCount] = useState(0);
|
|
43
|
+
const [toolbarFiltersRequested, setToolbarFiltersRequested] = useState(false);
|
|
31
44
|
|
|
32
45
|
const toolbarHost = useMemo<GridToolbarHost>(() => ({
|
|
33
46
|
registerColumnsPanel: () => {
|
|
34
47
|
toolbarHostCountRef.current += 1;
|
|
35
48
|
return () => { toolbarHostCountRef.current -= 1; };
|
|
36
49
|
},
|
|
50
|
+
registerFiltersPanel: () => {
|
|
51
|
+
setFiltersHostCount(count => count + 1);
|
|
52
|
+
return () => setFiltersHostCount(count => count - 1);
|
|
53
|
+
},
|
|
37
54
|
}), []);
|
|
38
55
|
|
|
56
|
+
const requestFiltersPanel = useCallback(() => setToolbarFiltersRequested(true), []);
|
|
57
|
+
const closeToolbarFiltersPanel = useCallback(() => setToolbarFiltersRequested(false), []);
|
|
58
|
+
|
|
39
59
|
const openColumnsPanel = useCallback(() => {
|
|
40
60
|
if (toolbarHostCountRef.current > 0) {
|
|
41
61
|
setToolbarPanelRequested(true);
|
|
@@ -73,5 +93,8 @@ export function useGridColumnsPanel(): UseGridColumnsPanelResult {
|
|
|
73
93
|
closeStandalonePanel,
|
|
74
94
|
standalonePanelRef,
|
|
75
95
|
openColumnsPanel,
|
|
96
|
+
toolbarFiltersRequested,
|
|
97
|
+
closeToolbarFiltersPanel,
|
|
98
|
+
openFiltersPanel: filtersHostCount > 0 ? requestFiltersPanel : undefined,
|
|
76
99
|
};
|
|
77
100
|
}
|