Skip to content

Commit b87c695

Browse files
author
Asif Ansari
committed
fix: resolve duplicate DOM IDs when multiple grids render on the same page (v1.0.7)
Row checkboxes and FilterPanel inputs used data-derived id attributes (row.id, col.field) without any grid-instance prefix, causing duplicate DOM IDs when two grids shared the same row IDs or column fields. Switched to React.useId() in Row and FilterRow so each rendered element gets a stable, globally unique id.
1 parent a464828 commit b87c695

14 files changed

Lines changed: 109 additions & 35 deletions

File tree

demo/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export default function App() {
137137
<img src={`${import.meta.env.BASE_URL}logo.png`} alt="OpenGridX Logo" className="app-logo" />
138138
<h2 className="app-title">
139139
OpenGridX
140-
<span className="app-version">v1.0.6</span>
140+
<span className="app-version">v1.0.7</span>
141141
</h2>
142142
</div>
143143

demo/Home.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export default function Home(_props: HomeProps) {
6868
<div className="home-logo-hero">
6969
<img src={`${import.meta.env.BASE_URL}banner.png`} alt="OpenGridX Logo" className="home-banner-image" />
7070
</div>
71-
<span className="home-badge">OpenGridX v1.0.6</span>
71+
<span className="home-badge">OpenGridX v1.0.7</span>
7272
<p className="home-subtitle">
7373
The elite, high-performance DataGrid for modern React.
7474
Built to handle massive data with a premium developer experience.

lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,6 @@ export function ColumnVisibilityPanel<R extends GridRowModel = GridRowModel>(
109109
<div className="ogx-column-visibility-panel__search-container">
110110
<SearchIcon />
111111
<input
112-
id="ogx-column-search"
113-
name="ogx-column-search"
114112
autoComplete="off"
115113
className="ogx-column-visibility-panel__search-input"
116114
placeholder="Search"

lib/components/DataGrid/DataGrid.css

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,42 @@
177177
overflow: hidden;
178178
box-sizing: border-box;
179179
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);
180216
}
181217
.ogx__aggregation-label {
182218
font-size: 0.7em;

lib/components/DataGrid/DataGrid.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
685685
const hasRowSpanning = React.useMemo(() => effectiveColumns.some(c => !!c.rowSpan), [effectiveColumns]);
686686
const [columnsPanelOpen, setColumnsPanelOpen] = React.useState(false);
687687
const containerRef = React.useRef<HTMLDivElement>(null);
688+
const standalonePanelRef = React.useRef<HTMLDivElement>(null);
688689

689690
const toolbarProps = React.useMemo(() => {
690691
if (!slots?.toolbar) return null;
@@ -735,8 +736,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
735736
useEffect(() => {
736737
if (!columnsPanelOpen || slots?.toolbar) return;
737738
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)) {
740740
setColumnsPanelOpen(false);
741741
}
742742
}
@@ -762,6 +762,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
762762
<GridStandaloneColumnPanel<R>
763763
isOpen={columnsPanelOpen}
764764
containerRef={containerRef}
765+
panelRef={standalonePanelRef}
765766
effectiveColumns={effectiveColumns}
766767
columnVisibilityModel={columnVisibilityModel}
767768
effectiveColumnOrder={effectiveColumnOrder}
@@ -995,6 +996,7 @@ export function DataGrid<R extends GridRowModel = GridRowModel>(props: DataGridP
995996
checkboxSelection={checkboxSelection}
996997
hasDetailPanel={hasDetailPanel}
997998
rowReordering={rowReordering}
999+
pinnedColumns={pinnedColumns}
9981000
/>
9991001
)}
10001002

lib/components/DataGrid/GridAggregationFooter.tsx

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import React from 'react';
1+
import React, { useMemo } from 'react';
22
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';
45

56
interface GridAggregationFooterProps {
67
columns: GridColDef[];
@@ -11,6 +12,7 @@ interface GridAggregationFooterProps {
1112
checkboxSelection: boolean;
1213
hasDetailPanel: boolean;
1314
rowReordering: boolean;
15+
pinnedColumns?: GridColumnPinning;
1416
}
1517

1618
export function GridAggregationFooter({
@@ -22,7 +24,28 @@ export function GridAggregationFooter({
2224
checkboxSelection,
2325
hasDetailPanel,
2426
rowReordering,
27+
pinnedColumns,
2528
}: 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+
2649
return (
2750
<div
2851
className="ogx__aggregation-footer"
@@ -40,17 +63,32 @@ export function GridAggregationFooter({
4063
const rawValue = aggregationResult[col.field];
4164
const colWidth = columnWidths[col.field] ?? (typeof col.width === 'number' ? col.width : 120);
4265

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+
4386
return (
4487
<div
4588
key={col.field}
46-
className="ogx__aggregation-cell"
89+
className={className}
4790
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}
5492
>
5593
{fnName ? (
5694
<>

lib/components/DataGrid/GridStandaloneColumnPanel.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { GridColDef, GridRowModel, GridColumnOrderChangeParams } from '../.
66
export interface GridStandaloneColumnPanelProps<R extends GridRowModel> {
77
isOpen: boolean;
88
containerRef: React.RefObject<HTMLDivElement | null>;
9+
panelRef?: React.RefObject<HTMLDivElement | null>;
910
effectiveColumns: GridColDef<R>[];
1011
columnVisibilityModel: Record<string, boolean>;
1112
effectiveColumnOrder: string[];
@@ -20,6 +21,7 @@ export interface GridStandaloneColumnPanelProps<R extends GridRowModel> {
2021
export function GridStandaloneColumnPanel<R extends GridRowModel>({
2122
isOpen,
2223
containerRef,
24+
panelRef,
2325
effectiveColumns,
2426
columnVisibilityModel,
2527
effectiveColumnOrder,
@@ -64,7 +66,7 @@ export function GridStandaloneColumnPanel<R extends GridRowModel>({
6466

6567
return ReactDOM.createPortal(
6668
<div
67-
id="ogx-standalone-col-panel"
69+
ref={panelRef}
6870
style={{
6971
position: 'fixed',
7072
top: panelTop,

lib/components/FilterPanel/FilterPanel.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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';
22
import type {
33
GridFilterModel,
44
GridFilterItem,
@@ -25,6 +25,7 @@ const FilterRow: React.FC<{
2525
item: GridFilterItem | undefined;
2626
onChange: (item: GridFilterItem | null) => void;
2727
}> = ({ col, item, onChange }) => {
28+
const filterId = useId();
2829
const operators = getOperatorsForType(col.type);
2930
const isActive = !!item;
3031
const currentOperator = item?.operator ?? operators[0];
@@ -99,7 +100,7 @@ const FilterRow: React.FC<{
99100
<div className="ogx-filter__controls">
100101
{ }
101102
<select
102-
id={`ogx-filter-op-${col.field}`}
103+
id={`${filterId}-op`}
103104
name={`filter-op-${col.field}`}
104105
className={`ogx-filter__op-select${isActive ? ' ogx-filter__op-select--active' : ''}`}
105106
value={String(currentOperator)}
@@ -114,7 +115,7 @@ const FilterRow: React.FC<{
114115
{ }
115116
{showValue && isBoolean && (
116117
<select
117-
id={`ogx-filter-val-${col.field}`}
118+
id={`${filterId}-val`}
118119
name={`filter-val-${col.field}`}
119120
className={`ogx-filter__value-input ogx-filter__value-select${isActive ? ' ogx-filter__value-input--active' : ''}`}
120121
value={String(item?.value ?? 'true')}
@@ -136,7 +137,7 @@ const FilterRow: React.FC<{
136137

137138
{showValue && !isBoolean && (
138139
<input
139-
id={`ogx-filter-val-${col.field}`}
140+
id={`${filterId}-val`}
140141
name={`filter-val-${col.field}`}
141142
className={`ogx-filter__value-input${isActive ? ' ogx-filter__value-input--active' : ''}`}
142143
type="text"

lib/components/Header/Header.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useId } from 'react';
22
import { Checkbox } from '../ui/Checkbox';
33
import { ColumnResizeHandle } from '../ColumnResizeHandle/ColumnResizeHandle';
44
import type { GridColDef, GridRowModel, GridSortDirection, GridColumnPinning, GridAggregationModel, GridColumnGroup, GridColumnGroupingModel } from '../../types';
@@ -171,6 +171,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
171171
onManageColumns,
172172
} = props;
173173

174+
const selectAllId = useId();
174175
const cellRefs = React.useRef<Record<string, HTMLElement | null>>({});
175176
const [menuOpenParams, setMenuOpenParams] = React.useState<{ colDef: GridColDef<R>; anchorEl: HTMLElement } | null>(null);
176177

@@ -340,8 +341,8 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
340341
}}
341342
>
342343
<Checkbox
343-
id="ogx-select-all"
344-
name="ogx-select-all"
344+
id={selectAllId}
345+
name={selectAllId}
345346
checked={allSelected}
346347
indeterminate={someSelected}
347348
onChange={(e) => onSelectAll?.(e.target.checked)}

lib/components/Pagination/Pagination.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
import React from 'react';
2+
import React, { useId } from 'react';
33
import { Button } from '../ui/Button';
44

55
export interface PaginationProps {
@@ -14,6 +14,7 @@ export interface PaginationProps {
1414
}
1515

1616
export function Pagination(props: PaginationProps) {
17+
const pageSizeId = useId();
1718
const {
1819
page,
1920
pageSize,
@@ -61,11 +62,11 @@ export function Pagination(props: PaginationProps) {
6162
<div className="ogx-pagination" role="navigation" aria-label="Pagination">
6263
{ }
6364
<div className="ogx-pagination__page-size">
64-
<label htmlFor="page-size-select" className="ogx-pagination__label">
65+
<label htmlFor={pageSizeId} className="ogx-pagination__label">
6566
Rows per page:
6667
</label>
6768
<select
68-
id="page-size-select"
69+
id={pageSizeId}
6970
className="ogx-pagination__select"
7071
value={pageSize}
7172
onChange={handlePageSizeChange}

0 commit comments

Comments
 (0)