@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.
@@ -6,6 +6,7 @@ import type { GridColDef, GridRowModel, GridColumnOrderChangeParams } from '../.
6
6
  export interface GridStandaloneColumnPanelProps<R extends GridRowModel> {
7
7
  isOpen: boolean;
8
8
  containerRef: React.RefObject<HTMLDivElement | null>;
9
+ panelRef?: React.RefObject<HTMLDivElement | null>;
9
10
  effectiveColumns: GridColDef<R>[];
10
11
  columnVisibilityModel: Record<string, boolean>;
11
12
  effectiveColumnOrder: string[];
@@ -20,6 +21,7 @@ export interface GridStandaloneColumnPanelProps<R extends GridRowModel> {
20
21
  export function GridStandaloneColumnPanel<R extends GridRowModel>({
21
22
  isOpen,
22
23
  containerRef,
24
+ panelRef,
23
25
  effectiveColumns,
24
26
  columnVisibilityModel,
25
27
  effectiveColumnOrder,
@@ -64,7 +66,7 @@ export function GridStandaloneColumnPanel<R extends GridRowModel>({
64
66
 
65
67
  return ReactDOM.createPortal(
66
68
  <div
67
- id="ogx-standalone-col-panel"
69
+ ref={panelRef}
68
70
  style={{
69
71
  position: 'fixed',
70
72
  top: panelTop,
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
1
+ import React, { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';
2
2
  import type {
3
3
  GridFilterModel,
4
4
  GridFilterItem,
@@ -25,6 +25,7 @@ const FilterRow: React.FC<{
25
25
  item: GridFilterItem | undefined;
26
26
  onChange: (item: GridFilterItem | null) => void;
27
27
  }> = ({ col, item, onChange }) => {
28
+ const filterId = useId();
28
29
  const operators = getOperatorsForType(col.type);
29
30
  const isActive = !!item;
30
31
  const currentOperator = item?.operator ?? operators[0];
@@ -99,7 +100,7 @@ const FilterRow: React.FC<{
99
100
  <div className="ogx-filter__controls">
100
101
  { }
101
102
  <select
102
- id={`ogx-filter-op-${col.field}`}
103
+ id={`${filterId}-op`}
103
104
  name={`filter-op-${col.field}`}
104
105
  className={`ogx-filter__op-select${isActive ? ' ogx-filter__op-select--active' : ''}`}
105
106
  value={String(currentOperator)}
@@ -114,7 +115,7 @@ const FilterRow: React.FC<{
114
115
  { }
115
116
  {showValue && isBoolean && (
116
117
  <select
117
- id={`ogx-filter-val-${col.field}`}
118
+ id={`${filterId}-val`}
118
119
  name={`filter-val-${col.field}`}
119
120
  className={`ogx-filter__value-input ogx-filter__value-select${isActive ? ' ogx-filter__value-input--active' : ''}`}
120
121
  value={String(item?.value ?? 'true')}
@@ -136,7 +137,7 @@ const FilterRow: React.FC<{
136
137
 
137
138
  {showValue && !isBoolean && (
138
139
  <input
139
- id={`ogx-filter-val-${col.field}`}
140
+ id={`${filterId}-val`}
140
141
  name={`filter-val-${col.field}`}
141
142
  className={`ogx-filter__value-input${isActive ? ' ogx-filter__value-input--active' : ''}`}
142
143
  type="text"
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useId } from 'react';
2
2
  import { Checkbox } from '../ui/Checkbox';
3
3
  import { ColumnResizeHandle } from '../ColumnResizeHandle/ColumnResizeHandle';
4
4
  import type { GridColDef, GridRowModel, GridSortDirection, GridColumnPinning, GridAggregationModel, GridColumnGroup, GridColumnGroupingModel } from '../../types';
@@ -171,6 +171,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
171
171
  onManageColumns,
172
172
  } = props;
173
173
 
174
+ const selectAllId = useId();
174
175
  const cellRefs = React.useRef<Record<string, HTMLElement | null>>({});
175
176
  const [menuOpenParams, setMenuOpenParams] = React.useState<{ colDef: GridColDef<R>; anchorEl: HTMLElement } | null>(null);
176
177
 
@@ -340,8 +341,8 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
340
341
  }}
341
342
  >
342
343
  <Checkbox
343
- id="ogx-select-all"
344
- name="ogx-select-all"
344
+ id={selectAllId}
345
+ name={selectAllId}
345
346
  checked={allSelected}
346
347
  indeterminate={someSelected}
347
348
  onChange={(e) => onSelectAll?.(e.target.checked)}
@@ -1,5 +1,5 @@
1
1
 
2
- import React from 'react';
2
+ import React, { useId } from 'react';
3
3
  import { Button } from '../ui/Button';
4
4
 
5
5
  export interface PaginationProps {
@@ -14,6 +14,7 @@ export interface PaginationProps {
14
14
  }
15
15
 
16
16
  export function Pagination(props: PaginationProps) {
17
+ const pageSizeId = useId();
17
18
  const {
18
19
  page,
19
20
  pageSize,
@@ -61,11 +62,11 @@ export function Pagination(props: PaginationProps) {
61
62
  <div className="ogx-pagination" role="navigation" aria-label="Pagination">
62
63
  { }
63
64
  <div className="ogx-pagination__page-size">
64
- <label htmlFor="page-size-select" className="ogx-pagination__label">
65
+ <label htmlFor={pageSizeId} className="ogx-pagination__label">
65
66
  Rows per page:
66
67
  </label>
67
68
  <select
68
- id="page-size-select"
69
+ id={pageSizeId}
69
70
  className="ogx-pagination__select"
70
71
  value={pageSize}
71
72
  onChange={handlePageSizeChange}
@@ -1,5 +1,5 @@
1
1
 
2
- import React from 'react';
2
+ import React, { useId } from 'react';
3
3
  import { Cell } from '../Cell/Cell';
4
4
  import { Checkbox } from '../ui/Checkbox';
5
5
  import { ExpandIcon } from '../ui/ExpandIcon';
@@ -94,6 +94,7 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
94
94
  rowHeight = 52
95
95
  } = props;
96
96
 
97
+ const checkboxId = useId();
97
98
  const expandCellRef = React.useRef<HTMLDivElement>(null);
98
99
  const checkboxCellRef = React.useRef<HTMLDivElement>(null);
99
100
 
@@ -291,7 +292,7 @@ export function Row<R extends GridRowModel = GridRowModel>(props: RowProps<R>) {
291
292
  }}
292
293
  >
293
294
  <Checkbox
294
- id={`ogx-select-row-${row.id}`}
295
+ id={checkboxId}
295
296
  name={`ogx-select-row-${row.id}`}
296
297
  checked={isSelected}
297
298
  onChange={handleCheckboxChange}
@@ -146,8 +146,6 @@ export function GlobalSearch({ value = '', onChange, placeholder = 'Search...',
146
146
  </div>
147
147
  <input
148
148
  ref={inputRef}
149
- id="ogx-global-search-input"
150
- name="ogx-global-search"
151
149
  className="ogx-global-search__input"
152
150
  value={localValue}
153
151
  onChange={(e) => setLocalValue(e.target.value)}
@@ -7,6 +7,24 @@ import { ColumnVisibilityPanel } from '../ColumnVisibilityPanel/ColumnVisibility
7
7
  import { FilterPanel } from '../FilterPanel/FilterPanel';
8
8
  import { GridTooltip } from '../Tooltip/Tooltip';
9
9
 
10
+ /** Props passed to a custom toolbar button renderer. */
11
+ export interface ToolbarButtonRenderProps {
12
+ /** Call this to toggle the associated panel open/closed. */
13
+ onClick: () => void;
14
+ /** Whether the associated panel is currently open. */
15
+ isOpen: boolean;
16
+ /** Number of active items (filters applied, aggregations configured, etc.). */
17
+ activeCount: number;
18
+ }
19
+
20
+ /** Props passed to a custom quick-filter renderer. */
21
+ export interface ToolbarQuickFilterRenderProps {
22
+ /** Current search string. */
23
+ value: string;
24
+ /** Call with the new search string when the input changes. */
25
+ onChange: (value: string) => void;
26
+ }
27
+
10
28
  export interface GridToolbarProps {
11
29
  columns?: GridColDef[];
12
30
  /** Original user-defined columns (before pivot transformation).
@@ -29,6 +47,33 @@ export interface GridToolbarProps {
29
47
  children?: React.ReactNode;
30
48
  rightContent?: React.ReactNode;
31
49
  style?: React.CSSProperties;
50
+ /**
51
+ * Replace the built-in Columns button. The columns panel still opens and
52
+ * closes normally — only the trigger element is replaced.
53
+ */
54
+ renderColumnsButton?: (props: ToolbarButtonRenderProps) => React.ReactNode;
55
+ /**
56
+ * Replace the built-in Filters button. The filter panel still opens and
57
+ * closes normally — only the trigger element is replaced.
58
+ */
59
+ renderFilterButton?: (props: ToolbarButtonRenderProps) => React.ReactNode;
60
+ /**
61
+ * Replace the built-in Aggregation/Summaries button. The aggregation panel
62
+ * still opens and closes normally — only the trigger element is replaced.
63
+ */
64
+ renderAggregationButton?: (props: ToolbarButtonRenderProps) => React.ReactNode;
65
+ /**
66
+ * Render an Export button. There is no built-in export button — this slot
67
+ * inserts your element after the Aggregation button in the toolbar action row.
68
+ */
69
+ renderExportButton?: () => React.ReactNode;
70
+ /**
71
+ * Replace the built-in quick-filter search bar. Receives the current search
72
+ * value and an onChange handler; connect them to your own input element.
73
+ */
74
+ renderQuickFilter?: (props: ToolbarQuickFilterRenderProps) => React.ReactNode;
75
+ /** Additional CSS class applied to the toolbar root element. */
76
+ className?: string;
32
77
  }
33
78
 
34
79
  const AGGREGATION_FUNCTIONS = ['none', 'sum', 'avg', 'count', 'min', 'max'] as const;
@@ -78,7 +123,7 @@ function AggregationPanel({
78
123
  onClose,
79
124
  activeCount,
80
125
  }: {
81
- anchorRef: React.RefObject<HTMLButtonElement | null>;
126
+ anchorRef: React.RefObject<HTMLElement | null>;
82
127
  aggregationModel: GridAggregationModel;
83
128
  aggregableColumns: GridColDef[];
84
129
  onFunctionChange: (field: string, fn: string) => void;
@@ -205,7 +250,7 @@ function ColumnsPanelWrapper({
205
250
  onColumnOrderReset,
206
251
  onClose
207
252
  }: {
208
- anchorRef: React.RefObject<HTMLButtonElement | null>;
253
+ anchorRef: React.RefObject<HTMLElement | null>;
209
254
  columns: GridColDef[];
210
255
  visibleColumns: Set<string>;
211
256
  onVisibilityChange: (field: string, isVisible: boolean) => void;
@@ -286,7 +331,7 @@ function FilterPanelWrapper({
286
331
  onFilterModelChange,
287
332
  onClose
288
333
  }: {
289
- anchorRef: React.RefObject<HTMLButtonElement | null>;
334
+ anchorRef: React.RefObject<HTMLElement | null>;
290
335
  columns: GridColDef[];
291
336
  filterModel: GridFilterModel;
292
337
  onFilterModelChange: (model: GridFilterModel) => void;
@@ -413,6 +458,12 @@ export function GridToolbar({
413
458
  children,
414
459
  rightContent,
415
460
  style,
461
+ renderColumnsButton,
462
+ renderFilterButton,
463
+ renderAggregationButton,
464
+ renderExportButton,
465
+ renderQuickFilter,
466
+ className,
416
467
  }: GridToolbarProps) {
417
468
  const [aggOpen, setAggOpen] = useState(false);
418
469
  const [pivotOpen, setPivotOpen] = useState(false);
@@ -433,6 +484,12 @@ export function GridToolbar({
433
484
  const colsButtonRef = useRef<HTMLButtonElement>(null);
434
485
  const filterButtonRef = useRef<HTMLButtonElement>(null);
435
486
 
487
+ // Wrapper div refs — used as panel anchors when a custom button renderer
488
+ // is provided (the custom element doesn't expose a ref to us).
489
+ const colsWrapperRef = useRef<HTMLDivElement>(null);
490
+ const filterWrapperRef = useRef<HTMLDivElement>(null);
491
+ const aggWrapperRef = useRef<HTMLDivElement>(null);
492
+
436
493
  const currentPivotModel = pivotModel ?? EMPTY_PIVOT;
437
494
  const pivotActive = currentPivotModel.rowFields.length > 0
438
495
  || currentPivotModel.columnFields.length > 0
@@ -512,7 +569,7 @@ export function GridToolbar({
512
569
  }, [columns, columnVisibilityModel, onColumnVisibilityModelChange]);
513
570
 
514
571
  return (
515
- <div className="ogx-toolbar" role="toolbar" aria-label="Grid toolbar" style={style}>
572
+ <div className={['ogx-toolbar', className].filter(Boolean).join(' ')} role="toolbar" aria-label="Grid toolbar" style={style}>
516
573
  {/* Left slot — custom content */}
517
574
  {children && <div className="ogx-toolbar__left">{children}</div>}
518
575
 
@@ -523,35 +580,49 @@ export function GridToolbar({
523
580
  <div className="ogx-toolbar__actions">
524
581
  {onFilterModelChange && (
525
582
  <div className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
526
- <GlobalSearch
527
- value={searchValue}
528
- onChange={handleSearchChange}
529
- placeholder="Search..."
530
- />
583
+ {renderQuickFilter
584
+ ? renderQuickFilter({ value: searchValue, onChange: handleSearchChange })
585
+ : (
586
+ <GlobalSearch
587
+ value={searchValue}
588
+ onChange={handleSearchChange}
589
+ placeholder="Search..."
590
+ />
591
+ )
592
+ }
531
593
  </div>
532
594
  )}
533
595
 
534
596
  {/* Columns Button */}
535
597
  {onColumnVisibilityModelChange && (
536
- <div className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
537
- <GridTooltip title="Columns">
538
- <button
539
- ref={colsButtonRef}
540
- className={`ogx-toolbar__icon-btn${colsOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
541
- aria-label="Manage columns"
542
- onClick={() => {
543
- setColsOpen(!colsOpen);
544
- setAggOpen(false);
545
- setPivotOpen(false);
546
- setFilterOpen(false);
547
- }}
548
- >
549
- <ViewColumnIcon />
550
- </button>
551
- </GridTooltip>
598
+ <div ref={colsWrapperRef} className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
599
+ {renderColumnsButton
600
+ ? renderColumnsButton({
601
+ onClick: () => { setColsOpen(o => !o); setAggOpen(false); setPivotOpen(false); setFilterOpen(false); },
602
+ isOpen: colsOpen,
603
+ activeCount: columns.length - visibleColumns.size,
604
+ })
605
+ : (
606
+ <GridTooltip title="Columns">
607
+ <button
608
+ ref={colsButtonRef}
609
+ className={`ogx-toolbar__icon-btn${colsOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
610
+ aria-label="Manage columns"
611
+ onClick={() => {
612
+ setColsOpen(!colsOpen);
613
+ setAggOpen(false);
614
+ setPivotOpen(false);
615
+ setFilterOpen(false);
616
+ }}
617
+ >
618
+ <ViewColumnIcon />
619
+ </button>
620
+ </GridTooltip>
621
+ )
622
+ }
552
623
  {colsOpen && (
553
624
  <ColumnsPanelWrapper
554
- anchorRef={colsButtonRef}
625
+ anchorRef={renderColumnsButton ? colsWrapperRef : colsButtonRef}
555
626
  columns={columns}
556
627
  visibleColumns={visibleColumns}
557
628
  onVisibilityChange={handleVisibilityChange}
@@ -567,28 +638,37 @@ export function GridToolbar({
567
638
 
568
639
  {/* Filters Button */}
569
640
  {onFilterModelChange && (
570
- <div className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
571
- <GridTooltip title="Filters">
572
- <button
573
- ref={filterButtonRef}
574
- className={`ogx-toolbar__icon-btn${filterOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
575
- aria-label="Advanced filters"
576
- onClick={() => {
577
- setFilterOpen(!filterOpen);
578
- setAggOpen(false);
579
- setPivotOpen(false);
580
- setColsOpen(false);
581
- }}
582
- >
583
- <FilterIcon />
584
- {activeFilterCount > 0 && (
585
- <span className="ogx-toolbar__dot" aria-label="Filters active" />
586
- )}
587
- </button>
588
- </GridTooltip>
641
+ <div ref={filterWrapperRef} className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
642
+ {renderFilterButton
643
+ ? renderFilterButton({
644
+ onClick: () => { setFilterOpen(o => !o); setAggOpen(false); setPivotOpen(false); setColsOpen(false); },
645
+ isOpen: filterOpen,
646
+ activeCount: activeFilterCount,
647
+ })
648
+ : (
649
+ <GridTooltip title="Filters">
650
+ <button
651
+ ref={filterButtonRef}
652
+ className={`ogx-toolbar__icon-btn${filterOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
653
+ aria-label="Advanced filters"
654
+ onClick={() => {
655
+ setFilterOpen(!filterOpen);
656
+ setAggOpen(false);
657
+ setPivotOpen(false);
658
+ setColsOpen(false);
659
+ }}
660
+ >
661
+ <FilterIcon />
662
+ {activeFilterCount > 0 && (
663
+ <span className="ogx-toolbar__dot" aria-label="Filters active" />
664
+ )}
665
+ </button>
666
+ </GridTooltip>
667
+ )
668
+ }
589
669
  {filterOpen && (
590
670
  <FilterPanelWrapper
591
- anchorRef={filterButtonRef}
671
+ anchorRef={renderFilterButton ? filterWrapperRef : filterButtonRef}
592
672
  columns={columns}
593
673
  filterModel={filterModel || { items: [] }}
594
674
  onFilterModelChange={onFilterModelChange}
@@ -604,11 +684,9 @@ export function GridToolbar({
604
684
  <GridTooltip title="Pivot">
605
685
  <button
606
686
  ref={pivotButtonRef}
607
- id="ogx-pivot-btn"
608
687
  className={`ogx-toolbar__icon-btn${pivotOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
609
688
  aria-label="Configure pivot"
610
689
  aria-expanded={pivotOpen}
611
- aria-controls="ogx-pivot-panel"
612
690
  onClick={() => {
613
691
  setPivotOpen(!pivotOpen);
614
692
  setAggOpen(false);
@@ -636,32 +714,39 @@ export function GridToolbar({
636
714
  )}
637
715
 
638
716
  {/* Summaries button */}
639
- <div className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
640
- <GridTooltip title="Summaries">
641
- <button
642
- ref={aggButtonRef}
643
- id="ogx-aggregation-btn"
644
- className={`ogx-toolbar__icon-btn${aggOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
645
- aria-label="Configure summaries"
646
- aria-expanded={aggOpen}
647
- aria-controls="ogx-aggregation-panel"
648
- onClick={() => {
649
- setAggOpen(!aggOpen);
650
- setPivotOpen(false);
651
- setColsOpen(false);
652
- setFilterOpen(false);
653
- }}
654
- >
655
- <SigmaIcon />
656
- {activeCount > 0 && (
657
- <span className="ogx-toolbar__dot" aria-label={`${activeCount} active summaries`} />
658
- )}
659
- </button>
660
- </GridTooltip>
717
+ <div ref={aggWrapperRef} className="ogx-toolbar__dropdown-wrapper" style={{ marginRight: 4 }}>
718
+ {renderAggregationButton
719
+ ? renderAggregationButton({
720
+ onClick: () => { setAggOpen(o => !o); setPivotOpen(false); setColsOpen(false); setFilterOpen(false); },
721
+ isOpen: aggOpen,
722
+ activeCount,
723
+ })
724
+ : (
725
+ <GridTooltip title="Summaries">
726
+ <button
727
+ ref={aggButtonRef}
728
+ className={`ogx-toolbar__icon-btn${aggOpen ? ' ogx-toolbar__icon-btn--active' : ''}`}
729
+ aria-label="Configure summaries"
730
+ aria-expanded={aggOpen}
731
+ onClick={() => {
732
+ setAggOpen(!aggOpen);
733
+ setPivotOpen(false);
734
+ setColsOpen(false);
735
+ setFilterOpen(false);
736
+ }}
737
+ >
738
+ <SigmaIcon />
739
+ {activeCount > 0 && (
740
+ <span className="ogx-toolbar__dot" aria-label={`${activeCount} active summaries`} />
741
+ )}
742
+ </button>
743
+ </GridTooltip>
744
+ )
745
+ }
661
746
 
662
747
  {aggOpen && (
663
748
  <AggregationPanel
664
- anchorRef={aggButtonRef}
749
+ anchorRef={renderAggregationButton ? aggWrapperRef : aggButtonRef}
665
750
  aggregationModel={aggregationModel}
666
751
  aggregableColumns={aggregableColumns}
667
752
  onFunctionChange={handleFunctionChange}
@@ -672,6 +757,9 @@ export function GridToolbar({
672
757
  )}
673
758
  </div>
674
759
 
760
+ {/* Export button slot */}
761
+ {renderExportButton && renderExportButton()}
762
+
675
763
  {rightContent}
676
764
  </div>
677
765
  </div>
package/lib/index.ts CHANGED
@@ -9,7 +9,7 @@ export { Header } from './components/Header/Header';
9
9
  export { Skeleton } from './components/Skeleton/Skeleton';
10
10
  export { FilterPanel } from './components/FilterPanel';
11
11
  export { GridToolbar } from './components/Toolbar/GridToolbar';
12
- export type { GridToolbarProps } from './components/Toolbar/GridToolbar';
12
+ export type { GridToolbarProps, ToolbarButtonRenderProps, ToolbarQuickFilterRenderProps } from './components/Toolbar/GridToolbar';
13
13
  export { GridTooltip } from './components/Tooltip/Tooltip';
14
14
 
15
15
  export { useGridApiRef } from './hooks/core/useGridApiRef';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencorestack/opengridx",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "OpenGridX: High-performance React data infrastructure. Unlock advanced Row Grouping, Excel Export, and Column Pinning without the usual \"Pro\" gatekeeping. Built for speed, scale, and complete architectural freedom. Fully open, virtualization-ready, and feature-complete.",
5
5
  "type": "module",
6
6
  "main": "./dist/opengridx.umd.js",