@opencorestack/opengridx 1.0.5 → 1.0.7

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.
@@ -164,6 +164,59 @@ The main component for displaying and interacting with data.
164
164
 
165
165
  ---
166
166
 
167
+ ## đŸ› ī¸ `GridToolbar`
168
+
169
+ The built-in toolbar component. Mount it via `slots={{ toolbar: GridToolbar }}`. All props below are passed through `slotProps.toolbar` (or directly when building a custom wrapper).
170
+
171
+ ### `GridToolbarProps`
172
+
173
+ | Prop | Type | Default | Description |
174
+ | :--- | :--- | :--- | :--- |
175
+ | `columns` | `GridColDef[]` | `[]` | Column definitions — injected automatically when used via `slots`. |
176
+ | `baseColumns` | `GridColDef[]` | — | Pre-pivot columns shown in the Pivot panel instead of synthetic pivot columns. |
177
+ | `aggregationModel` | `GridAggregationModel` | `{}` | Current aggregation configuration. |
178
+ | `onAggregationModelChange` | `(model: GridAggregationModel) => void` | — | Called when the user changes aggregation settings. |
179
+ | `pivotModel` | `GridPivotModel` | — | Current pivot configuration. Presence of this prop renders the Pivot button. |
180
+ | `onPivotModelChange` | `(model: GridPivotModel) => void` | — | Called when the user changes pivot settings. |
181
+ | `filterModel` | `GridFilterModel` | — | Current filter model. Presence of this prop renders the search bar and Filter button. |
182
+ | `onFilterModelChange` | `(model: GridFilterModel) => void` | — | Called when the user changes filters or the quick-search value. |
183
+ | `columnVisibilityModel` | `Record<string, boolean>` | `{}` | Current column visibility map. |
184
+ | `onColumnVisibilityModelChange` | `(model: Record<string, boolean>) => void` | — | Called when the user shows or hides a column. Presence renders the Columns button. |
185
+ | `onColumnReorder` | `(from: string, to: string) => void` | — | Called when the user drags a column in the Columns panel. |
186
+ | `onColumnOrderReset` | `() => void` | — | Called when the user clicks "Reset order" in the Columns panel. |
187
+ | `forceColumnsOpen` | `boolean` | — | Forces the Columns panel open (used internally by the column header context menu). |
188
+ | `onColumnsPanelClose` | `() => void` | — | Called when the Columns panel closes after a `forceColumnsOpen`. |
189
+ | `children` | `ReactNode` | — | Content rendered on the **left** side of the toolbar, before the spacer. |
190
+ | `rightContent` | `ReactNode` | — | Content rendered on the **right** side, after all built-in buttons. |
191
+ | `className` | `string` | — | Additional CSS class on the toolbar root `<div>`. Use for visual theme overrides. |
192
+ | `style` | `CSSProperties` | — | Inline styles on the toolbar root. |
193
+ | `renderColumnsButton` | `(props: ToolbarButtonRenderProps) => ReactNode` | — | Replace the built-in Columns toggle button. The Columns panel still functions normally. |
194
+ | `renderFilterButton` | `(props: ToolbarButtonRenderProps) => ReactNode` | — | Replace the built-in Filters toggle button. The Filter panel still functions normally. |
195
+ | `renderAggregationButton` | `(props: ToolbarButtonRenderProps) => ReactNode` | — | Replace the built-in Summaries toggle button. The Aggregation panel still functions normally. |
196
+ | `renderExportButton` | `() => ReactNode` | — | Inject an Export button after the Aggregation button. No built-in export button exists. |
197
+ | `renderQuickFilter` | `(props: ToolbarQuickFilterRenderProps) => ReactNode` | — | Replace the built-in `GlobalSearch` input with your own component. |
198
+
199
+ ### `ToolbarButtonRenderProps`
200
+
201
+ Passed to `renderColumnsButton`, `renderFilterButton`, and `renderAggregationButton`.
202
+
203
+ | Property | Type | Description |
204
+ | :--- | :--- | :--- |
205
+ | `onClick` | `() => void` | Toggle the associated panel open or closed. |
206
+ | `isOpen` | `boolean` | Whether the associated panel is currently open. |
207
+ | `activeCount` | `number` | Active items: hidden columns for Columns, applied filters for Filter, configured aggregations for Summaries. |
208
+
209
+ ### `ToolbarQuickFilterRenderProps`
210
+
211
+ Passed to `renderQuickFilter`.
212
+
213
+ | Property | Type | Description |
214
+ | :--- | :--- | :--- |
215
+ | `value` | `string` | Current search string. |
216
+ | `onChange` | `(value: string) => void` | Call with the updated string when the input changes. |
217
+
218
+ ---
219
+
167
220
  ## đŸ•šī¸ Imperative API (`GridApi`)
168
221
 
169
222
  Access these methods via the `apiRef` prop.
@@ -1,40 +1,221 @@
1
- # `<Toolbar />`
1
+ # `GridToolbar`
2
2
 
3
- The toolbar is an optional component rendered at the top of the grid. It typically contains global actions like exporting, filtering, and column management.
3
+ The toolbar is an optional component rendered at the top of the grid. It provides global search, column visibility management, advanced filters, aggregation configuration, and pivot mode — all in one composable component.
4
4
 
5
- ## đŸ› ī¸ Provided Component: `GridToolbar`
5
+ ## Basic usage
6
6
 
7
- OpenGridX includes a pre-built `GridToolbar` that bundles common actions.
8
-
9
- ### Usage
10
7
  ```tsx
11
8
  import { DataGrid, GridToolbar } from '@opencorestack/opengridx';
12
9
 
13
10
  <DataGrid
14
- slots={{
15
- toolbar: GridToolbar
11
+ rows={rows}
12
+ columns={columns}
13
+ filterModel={filterModel}
14
+ onFilterModelChange={setFilterModel}
15
+ columnVisibilityModel={columnVisibilityModel}
16
+ onColumnVisibilityModelChange={setColumnVisibilityModel}
17
+ aggregationModel={aggregationModel}
18
+ onAggregationModelChange={setAggregationModel}
19
+ slots={{ toolbar: GridToolbar }}
20
+ />
21
+ ```
22
+
23
+ `GridToolbar` automatically hides buttons for features whose callbacks are not wired. For example, if you don't pass `onFilterModelChange` the search bar and filter button are not rendered.
24
+
25
+ ---
26
+
27
+ ## `GridToolbarProps`
28
+
29
+ | Prop | Type | Default | Description |
30
+ |------|------|---------|-------------|
31
+ | `columns` | `GridColDef[]` | `[]` | Column definitions (injected automatically when used via `slots`). |
32
+ | `baseColumns` | `GridColDef[]` | — | Pre-pivot column definitions, used by the Pivot panel to avoid showing synthetic columns. |
33
+ | `aggregationModel` | `GridAggregationModel` | `{}` | Current aggregation configuration. |
34
+ | `onAggregationModelChange` | `(model) => void` | — | Called when the user changes aggregation settings. |
35
+ | `pivotModel` | `GridPivotModel` | — | Current pivot configuration. |
36
+ | `onPivotModelChange` | `(model) => void` | — | Called when the user changes pivot settings. Presence of this prop shows the Pivot button. |
37
+ | `filterModel` | `GridFilterModel` | — | Current filter model. Presence of this prop shows the search bar and Filter button. |
38
+ | `onFilterModelChange` | `(model) => void` | — | Called when the user changes filters or the search query. |
39
+ | `columnVisibilityModel` | `Record<string, boolean>` | `{}` | Current column visibility state. |
40
+ | `onColumnVisibilityModelChange` | `(model) => void` | — | Called when the user shows/hides columns. Presence shows the Columns button. |
41
+ | `onColumnReorder` | `(from, to) => void` | — | Called when the user drags a column in the Columns panel. |
42
+ | `onColumnOrderReset` | `() => void` | — | Called when the user clicks "Reset order" in the Columns panel. |
43
+ | `forceColumnsOpen` | `boolean` | — | When `true`, opens the Columns panel immediately (used internally by the column header menu). |
44
+ | `onColumnsPanelClose` | `() => void` | — | Called when the Columns panel closes after a `forceColumnsOpen`. |
45
+ | `children` | `ReactNode` | — | Content rendered in the **left** side of the toolbar (before the spacer). |
46
+ | `rightContent` | `ReactNode` | — | Content rendered in the **right** side of the toolbar (after all built-in buttons). |
47
+ | `className` | `string` | — | Additional CSS class applied to the toolbar root `<div>`. Use for visual overrides without replacing the component. |
48
+ | `style` | `CSSProperties` | — | Inline style applied to the toolbar root `<div>`. |
49
+ | `renderColumnsButton` | `(props: ToolbarButtonRenderProps) => ReactNode` | — | Replace the built-in Columns button. The Columns panel still opens and closes normally. |
50
+ | `renderFilterButton` | `(props: ToolbarButtonRenderProps) => ReactNode` | — | Replace the built-in Filters button. The Filter panel still opens and closes normally. |
51
+ | `renderAggregationButton` | `(props: ToolbarButtonRenderProps) => ReactNode` | — | Replace the built-in Summaries button. The Aggregation panel still opens and closes normally. |
52
+ | `renderExportButton` | `() => ReactNode` | — | Inject an Export button after the Aggregation button. No built-in export button exists — this is the slot for it. |
53
+ | `renderQuickFilter` | `(props: ToolbarQuickFilterRenderProps) => ReactNode` | — | Replace the built-in quick-filter search input with your own component. |
54
+
55
+ ---
56
+
57
+ ## Render prop types
58
+
59
+ ### `ToolbarButtonRenderProps`
60
+
61
+ Passed to `renderColumnsButton`, `renderFilterButton`, and `renderAggregationButton`.
62
+
63
+ ```ts
64
+ interface ToolbarButtonRenderProps {
65
+ onClick: () => void; // Toggle the associated panel open/closed
66
+ isOpen: boolean; // Whether the panel is currently open
67
+ activeCount: number; // Active items (hidden columns, applied filters, etc.)
68
+ }
69
+ ```
70
+
71
+ ### `ToolbarQuickFilterRenderProps`
72
+
73
+ Passed to `renderQuickFilter`.
74
+
75
+ ```ts
76
+ interface ToolbarQuickFilterRenderProps {
77
+ value: string; // Current search string
78
+ onChange: (value: string) => void; // Call with new string on input change
79
+ }
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Customization patterns
85
+
86
+ ### Add content to left or right sides
87
+
88
+ Use `children` (left) and `rightContent` (right) to inject elements without touching the built-in buttons:
89
+
90
+ ```tsx
91
+ <DataGrid
92
+ slots={{ toolbar: GridToolbar }}
93
+ slotProps={{
94
+ toolbar: {
95
+ children: <span style={{ fontWeight: 600 }}>My Grid</span>,
96
+ rightContent: (
97
+ <button onClick={handleExport}>Export CSV</button>
98
+ ),
99
+ },
100
+ }}
101
+ />
102
+ ```
103
+
104
+ ### Custom search bar
105
+
106
+ Replace only the search input while keeping the rest of the toolbar intact:
107
+
108
+ ```tsx
109
+ function MySearchBar({ value, onChange }: ToolbarQuickFilterRenderProps) {
110
+ return (
111
+ <input
112
+ className="my-search"
113
+ value={value}
114
+ onChange={e => onChange(e.target.value)}
115
+ placeholder="Searchâ€Ļ"
116
+ />
117
+ );
118
+ }
119
+
120
+ <DataGrid
121
+ slots={{ toolbar: GridToolbar }}
122
+ slotProps={{
123
+ toolbar: {
124
+ renderQuickFilter: (props) => <MySearchBar {...props} />,
125
+ },
16
126
  }}
17
127
  />
18
128
  ```
19
129
 
20
- ### Features
21
- - **Quick Search**: Search across all visible columns.
22
- - **Export Menu**: Download data as CSV, Print, or [Advanced Excel](../features/export-guide.md).
23
- - **Column Visibility**: Toggle which columns are shown via a dropdown [Visibility Panel](column-visibility.md).
24
- - **Density**: Switch between Compact, Standard, and Comfortable row heights.
130
+ ### Custom action buttons (panels still work)
25
131
 
26
- ## 🎨 Custom Toolbar
132
+ Replace individual trigger buttons while keeping their panels fully functional:
27
133
 
28
- You can provide your own component to the `toolbar` slot. It will receive props to interact with the grid state.
134
+ ```tsx
135
+ function MyFilterButton({ onClick, isOpen, activeCount }: ToolbarButtonRenderProps) {
136
+ return (
137
+ <button
138
+ className={`my-btn ${isOpen ? 'my-btn--active' : ''}`}
139
+ onClick={onClick}
140
+ >
141
+ Filters {activeCount > 0 && <span className="badge">{activeCount}</span>}
142
+ </button>
143
+ );
144
+ }
145
+
146
+ <DataGrid
147
+ slots={{ toolbar: GridToolbar }}
148
+ slotProps={{
149
+ toolbar: {
150
+ renderFilterButton: (props) => <MyFilterButton {...props} />,
151
+ renderColumnsButton: (props) => <MyColumnsButton {...props} />,
152
+ renderAggregationButton: (props) => <MyAggButton {...props} />,
153
+ },
154
+ }}
155
+ />
156
+ ```
157
+
158
+ ### Add an Export button
159
+
160
+ There is no built-in export button in the toolbar. Use `renderExportButton` to add one:
29
161
 
30
162
  ```tsx
31
- const MyToolbar = () => {
163
+ import { exportToCsv, useGridApiRef } from '@opencorestack/opengridx';
164
+
165
+ const apiRef = useGridApiRef();
166
+
167
+ <DataGrid
168
+ apiRef={apiRef}
169
+ slots={{ toolbar: GridToolbar }}
170
+ slotProps={{
171
+ toolbar: {
172
+ renderExportButton: () => (
173
+ <button onClick={() => exportToCsv(apiRef.current.getAllRows(), columns)}>
174
+ Export CSV
175
+ </button>
176
+ ),
177
+ },
178
+ }}
179
+ />
180
+ ```
181
+
182
+ ### Theme override via `className`
183
+
184
+ Style the toolbar to match your brand without replacing the component:
185
+
186
+ ```css
187
+ /* your-styles.css */
188
+ .my-toolbar {
189
+ background: linear-gradient(135deg, #1e1b4b, #312e81);
190
+ border-bottom: none;
191
+ border-radius: 8px 8px 0 0;
192
+ padding: 10px 16px;
193
+ }
194
+ ```
195
+
196
+ ```tsx
197
+ <DataGrid
198
+ slots={{ toolbar: GridToolbar }}
199
+ slotProps={{ toolbar: { className: 'my-toolbar' } }}
200
+ />
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Fully custom toolbar
206
+
207
+ If the render props don't give you enough control, replace the entire toolbar via `slots.toolbar`:
208
+
209
+ ```tsx
210
+ function MyToolbar() {
32
211
  return (
33
- <div className="custom-toolbar">
34
- <button onClick={...}>Custom Action</button>
212
+ <div className="my-toolbar-root">
213
+ <span>Custom toolbar</span>
35
214
  </div>
36
215
  );
37
- };
216
+ }
38
217
 
39
218
  <DataGrid slots={{ toolbar: MyToolbar }} />
40
219
  ```
220
+
221
+ The custom component receives all `GridToolbarProps` via `slotProps.toolbar`.
@@ -0,0 +1,205 @@
1
+ # Toolbar Customization
2
+
3
+ **Available from:** `@opencorestack/opengridx` v1.0.6
4
+
5
+ Replace individual toolbar controls — search bar, column button, filter button, aggregation button — without replacing the entire toolbar. All panels (columns, filters, aggregation) continue to work; only the trigger elements change.
6
+
7
+ ---
8
+
9
+ ## Quick start
10
+
11
+ ```tsx
12
+ import {
13
+ DataGrid,
14
+ GridToolbar,
15
+ type ToolbarButtonRenderProps,
16
+ type ToolbarQuickFilterRenderProps,
17
+ } from '@opencorestack/opengridx';
18
+
19
+ <DataGrid
20
+ rows={rows}
21
+ columns={columns}
22
+ filterModel={filterModel}
23
+ onFilterModelChange={setFilterModel}
24
+ columnVisibilityModel={columnVisibilityModel}
25
+ onColumnVisibilityModelChange={setColumnVisibilityModel}
26
+ slots={{ toolbar: GridToolbar }}
27
+ slotProps={{
28
+ toolbar: {
29
+ renderQuickFilter: ({ value, onChange }: ToolbarQuickFilterRenderProps) => (
30
+ <input
31
+ className="my-search"
32
+ value={value}
33
+ onChange={e => onChange(e.target.value)}
34
+ placeholder="Searchâ€Ļ"
35
+ />
36
+ ),
37
+ },
38
+ }}
39
+ />
40
+ ```
41
+
42
+ ---
43
+
44
+ ## New props on `GridToolbarProps`
45
+
46
+ | Prop | Type | Description |
47
+ |------|------|-------------|
48
+ | `renderColumnsButton` | `(props: ToolbarButtonRenderProps) => ReactNode` | Replace the Columns toggle button. The Columns panel still opens and closes normally. |
49
+ | `renderFilterButton` | `(props: ToolbarButtonRenderProps) => ReactNode` | Replace the Filters toggle button. The Filter panel still opens and closes normally. |
50
+ | `renderAggregationButton` | `(props: ToolbarButtonRenderProps) => ReactNode` | Replace the Summaries toggle button. The Aggregation panel still opens and closes normally. |
51
+ | `renderExportButton` | `() => ReactNode` | Insert an Export button after the Aggregation button. No built-in export button exists — this is the slot for it. |
52
+ | `renderQuickFilter` | `(props: ToolbarQuickFilterRenderProps) => ReactNode` | Replace the built-in search input with your own component. |
53
+ | `className` | `string` | Additional CSS class on the toolbar root `<div>` for theme overrides. |
54
+
55
+ ---
56
+
57
+ ## Render prop types
58
+
59
+ ### `ToolbarButtonRenderProps`
60
+
61
+ ```ts
62
+ interface ToolbarButtonRenderProps {
63
+ onClick: () => void; // toggle the panel
64
+ isOpen: boolean; // whether the panel is open
65
+ activeCount: number; // hidden columns / applied filters / active aggregations
66
+ }
67
+ ```
68
+
69
+ ### `ToolbarQuickFilterRenderProps`
70
+
71
+ ```ts
72
+ interface ToolbarQuickFilterRenderProps {
73
+ value: string;
74
+ onChange: (value: string) => void;
75
+ }
76
+ ```
77
+
78
+ Both types are exported from the package:
79
+
80
+ ```ts
81
+ import type { ToolbarButtonRenderProps, ToolbarQuickFilterRenderProps } from '@opencorestack/opengridx';
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Examples
87
+
88
+ ### Custom search bar
89
+
90
+ ```tsx
91
+ function MySearchBar({ value, onChange }: ToolbarQuickFilterRenderProps) {
92
+ return (
93
+ <div className="search-wrap">
94
+ <SearchIcon />
95
+ <input
96
+ value={value}
97
+ onChange={e => onChange(e.target.value)}
98
+ placeholder="Searchâ€Ļ"
99
+ />
100
+ {value && <button onClick={() => onChange('')}>✕</button>}
101
+ </div>
102
+ );
103
+ }
104
+
105
+ slotProps={{
106
+ toolbar: {
107
+ renderQuickFilter: (props) => <MySearchBar {...props} />,
108
+ },
109
+ }}
110
+ ```
111
+
112
+ ### Custom filter button with active-count badge
113
+
114
+ ```tsx
115
+ function MyFilterBtn({ onClick, isOpen, activeCount }: ToolbarButtonRenderProps) {
116
+ return (
117
+ <button
118
+ className={`btn ${isOpen ? 'btn--active' : ''}`}
119
+ onClick={onClick}
120
+ >
121
+ Filters
122
+ {activeCount > 0 && <span className="badge">{activeCount}</span>}
123
+ </button>
124
+ );
125
+ }
126
+
127
+ slotProps={{
128
+ toolbar: {
129
+ renderFilterButton: (props) => <MyFilterBtn {...props} />,
130
+ },
131
+ }}
132
+ ```
133
+
134
+ ### Export button wired to `apiRef`
135
+
136
+ ```tsx
137
+ import { exportToCsv, useGridApiRef } from '@opencorestack/opengridx';
138
+
139
+ const apiRef = useGridApiRef();
140
+
141
+ <DataGrid
142
+ apiRef={apiRef}
143
+ slots={{ toolbar: GridToolbar }}
144
+ slotProps={{
145
+ toolbar: {
146
+ renderExportButton: () => (
147
+ <button onClick={() => exportToCsv(apiRef.current.getAllRows(), columns, { filename: 'export' })}>
148
+ Export CSV
149
+ </button>
150
+ ),
151
+ },
152
+ }}
153
+ />
154
+ ```
155
+
156
+ ### Replacing all buttons at once
157
+
158
+ ```tsx
159
+ slotProps={{
160
+ toolbar: {
161
+ renderQuickFilter: (props) => <MySearch {...props} />,
162
+ renderColumnsButton: (props) => <MyColumnsBtn {...props} />,
163
+ renderFilterButton: (props) => <MyFilterBtn {...props} />,
164
+ renderAggregationButton: (props) => <MyAggBtn {...props} />,
165
+ renderExportButton: () => <MyExportBtn />,
166
+ },
167
+ }}
168
+ ```
169
+
170
+ ### Theme override via `className`
171
+
172
+ ```css
173
+ /* your-styles.css */
174
+ .toolbar-dark {
175
+ background: linear-gradient(135deg, #1e1b4b, #312e81);
176
+ border-bottom: none;
177
+ padding: 10px 16px;
178
+ }
179
+ ```
180
+
181
+ ```tsx
182
+ slotProps={{
183
+ toolbar: { className: 'toolbar-dark' },
184
+ }}
185
+ ```
186
+
187
+ ---
188
+
189
+ ## How panels stay functional
190
+
191
+ When you provide a custom button, the toolbar renders your element as the trigger but keeps its own open/close state. The panel (Columns, Filters, or Aggregation) is positioned relative to the wrapper `<div>` surrounding your custom button, so placement is automatic — you don't need to pass any refs.
192
+
193
+ Panel close behaviour is unchanged: Escape key closes, click-outside closes (except the Filter panel, which requires an explicit Close button to let users type without it dismissing).
194
+
195
+ ---
196
+
197
+ ## What is NOT replaced
198
+
199
+ These built-in features are unaffected regardless of which render props you provide:
200
+
201
+ - Column panel content (drag-to-reorder, show/hide toggles)
202
+ - Filter panel content (operators, values, logic operator)
203
+ - Aggregation panel content (per-column function pills)
204
+ - Pivot panel (if `onPivotModelChange` is wired)
205
+ - `children` / `rightContent` slot behaviour
@@ -109,8 +109,6 @@ export function ColumnVisibilityPanel<R extends GridRowModel = GridRowModel>(
109
109
  <div className="ogx-column-visibility-panel__search-container">
110
110
  <SearchIcon />
111
111
  <input
112
- id="ogx-column-search"
113
- name="ogx-column-search"
114
112
  autoComplete="off"
115
113
  className="ogx-column-visibility-panel__search-input"
116
114
  placeholder="Search"
@@ -177,6 +177,42 @@
177
177
  overflow: hidden;
178
178
  box-sizing: border-box;
179
179
  border-right: 1px solid var(--ogx-grid-border-color);
180
+ position: relative;
181
+ }
182
+ .ogx__aggregation-cell--pinned-left,
183
+ .ogx__aggregation-cell--pinned-right {
184
+ position: sticky;
185
+ z-index: 3;
186
+ background: var(--ogx-grid-header-background);
187
+ }
188
+ .ogx__aggregation-cell--pinned-right {
189
+ border-left: 1px solid var(--ogx-grid-border-color);
190
+ }
191
+ .ogx__aggregation-cell--pinned-left-last {
192
+ overflow: visible;
193
+ }
194
+ .ogx__aggregation-cell--pinned-left-last::after {
195
+ content: '';
196
+ position: absolute;
197
+ top: 0;
198
+ right: -10px;
199
+ bottom: 0;
200
+ width: 10px;
201
+ pointer-events: none;
202
+ box-shadow: inset 10px 0 8px -8px rgba(0, 0, 0, 0.1);
203
+ }
204
+ .ogx__aggregation-cell--pinned-right-first {
205
+ overflow: visible;
206
+ }
207
+ .ogx__aggregation-cell--pinned-right-first::before {
208
+ content: '';
209
+ position: absolute;
210
+ top: 0;
211
+ left: -10px;
212
+ bottom: 0;
213
+ width: 10px;
214
+ pointer-events: none;
215
+ box-shadow: inset -10px 0 8px -8px rgba(0, 0, 0, 0.1);
180
216
  }
181
217
  .ogx__aggregation-label {
182
218
  font-size: 0.7em;
@@ -685,6 +685,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
685
685
  const hasRowSpanning = React.useMemo(() => effectiveColumns.some(c => !!c.rowSpan), [effectiveColumns]);
686
686
  const [columnsPanelOpen, setColumnsPanelOpen] = React.useState(false);
687
687
  const containerRef = React.useRef<HTMLDivElement>(null);
688
+ const standalonePanelRef = React.useRef<HTMLDivElement>(null);
688
689
 
689
690
  const toolbarProps = React.useMemo(() => {
690
691
  if (!slots?.toolbar) return null;
@@ -735,8 +736,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
735
736
  useEffect(() => {
736
737
  if (!columnsPanelOpen || slots?.toolbar) return;
737
738
  function handleClickOutside(e: MouseEvent) {
738
- const panel = document.getElementById('ogx-standalone-col-panel');
739
- if (panel && !panel.contains(e.target as Node)) {
739
+ if (standalonePanelRef.current && !standalonePanelRef.current.contains(e.target as Node)) {
740
740
  setColumnsPanelOpen(false);
741
741
  }
742
742
  }
@@ -762,6 +762,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
762
762
  <GridStandaloneColumnPanel<R>
763
763
  isOpen={columnsPanelOpen}
764
764
  containerRef={containerRef}
765
+ panelRef={standalonePanelRef}
765
766
  effectiveColumns={effectiveColumns}
766
767
  columnVisibilityModel={columnVisibilityModel}
767
768
  effectiveColumnOrder={effectiveColumnOrder}
@@ -995,6 +996,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
995
996
  checkboxSelection={checkboxSelection}
996
997
  hasDetailPanel={hasDetailPanel}
997
998
  rowReordering={rowReordering}
999
+ pinnedColumns={pinnedColumns}
998
1000
  />
999
1001
  )}
1000
1002
 
@@ -1,6 +1,7 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import { formatAggregationValue } from '../../hooks/features/useAggregation';
3
- import type { GridColDef, GridAggregationModel, GridAggregationResult } from '../../types';
3
+ import type { GridColDef, GridAggregationModel, GridAggregationResult, GridColumnPinning } from '../../types';
4
+ import { calculatePinnedPositions, isColumnPinned } from '../../utils/pinning';
4
5
 
5
6
  interface GridAggregationFooterProps {
6
7
  columns: GridColDef[];
@@ -11,6 +12,7 @@ interface GridAggregationFooterProps {
11
12
  checkboxSelection: boolean;
12
13
  hasDetailPanel: boolean;
13
14
  rowReordering: boolean;
15
+ pinnedColumns?: GridColumnPinning;
14
16
  }
15
17
 
16
18
  export function GridAggregationFooter({
@@ -22,7 +24,28 @@ export function GridAggregationFooter({
22
24
  checkboxSelection,
23
25
  hasDetailPanel,
24
26
  rowReordering,
27
+ pinnedColumns,
25
28
  }: GridAggregationFooterProps) {
29
+ // Compute sticky left/right pixel offsets for each pinned column.
30
+ // The footer always renders checkbox/detail/reorder spacers unconditionally,
31
+ // so treat pinCheckboxColumn and pinExpandColumn as always true.
32
+ const pinnedOffsets = useMemo(
33
+ () => calculatePinnedPositions(
34
+ columns,
35
+ columnWidths,
36
+ pinnedColumns,
37
+ checkboxSelection,
38
+ true,
39
+ hasDetailPanel,
40
+ true,
41
+ rowReordering,
42
+ ),
43
+ [columns, columnWidths, pinnedColumns, checkboxSelection, hasDetailPanel, rowReordering]
44
+ );
45
+
46
+ const lastLeftField = pinnedColumns?.left?.[pinnedColumns.left.length - 1];
47
+ const firstRightField = pinnedColumns?.right?.[0];
48
+
26
49
  return (
27
50
  <div
28
51
  className="ogx__aggregation-footer"
@@ -40,17 +63,32 @@ export function GridAggregationFooter({
40
63
  const rawValue = aggregationResult[col.field];
41
64
  const colWidth = columnWidths[col.field] ?? (typeof col.width === 'number' ? col.width : 120);
42
65
 
66
+ const pinnedPosition = isColumnPinned(col.field, pinnedColumns);
67
+ const pinnedOffset = pinnedPosition ? pinnedOffsets[col.field] : undefined;
68
+
69
+ const className = [
70
+ 'ogx__aggregation-cell',
71
+ pinnedPosition === 'left' && 'ogx__aggregation-cell--pinned-left',
72
+ pinnedPosition === 'right' && 'ogx__aggregation-cell--pinned-right',
73
+ pinnedPosition === 'left' && col.field === lastLeftField && 'ogx__aggregation-cell--pinned-left-last',
74
+ pinnedPosition === 'right' && col.field === firstRightField && 'ogx__aggregation-cell--pinned-right-first',
75
+ ].filter(Boolean).join(' ');
76
+
77
+ const style: React.CSSProperties = {
78
+ width: colWidth,
79
+ minWidth: colWidth,
80
+ maxWidth: colWidth,
81
+ textAlign: (col.align as React.CSSProperties['textAlign']) || 'left',
82
+ };
83
+ if (pinnedPosition === 'left' && pinnedOffset !== undefined) style.left = pinnedOffset;
84
+ if (pinnedPosition === 'right' && pinnedOffset !== undefined) style.right = pinnedOffset;
85
+
43
86
  return (
44
87
  <div
45
88
  key={col.field}
46
- className="ogx__aggregation-cell"
89
+ className={className}
47
90
  role="gridcell"
48
- style={{
49
- width: colWidth,
50
- minWidth: colWidth,
51
- maxWidth: colWidth,
52
- textAlign: (col.align as React.CSSProperties['textAlign']) || 'left',
53
- }}
91
+ style={style}
54
92
  >
55
93
  {fnName ? (
56
94
  <>