Compare commits

...
Author SHA1 Message Date
Ken 2bbf17e8b0 perf(DataGrid.Table): Optimize table rebuild 2026-11-02 16:23:44 +01:00
2 changed files with 43 additions and 1 deletions

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@navikt/ds-react": patch
---
DataGrid: Keyboard navigation no longer rebuilds the internal grid map on every arrow key press, improving responsiveness in large tables.
@@ -6,6 +6,7 @@ import { type GridCache, buildTableGridMap } from "../helpers/table-grid-nav";
* Manages the table grid cache and observes DOM changes that require grid rebuilding.
* Watches for structural changes (rows/cells added/removed) and attribute changes
* (colspan, rowspan, hidden, style) that affect grid layout and focusability.
* Style mutations that only touch scroll-margin (written during focus navigation) are ignored.
*/
function useGridCache(tableRef: HTMLTableElement | null, enabled: boolean) {
const gridCacheRef = useRef<GridCache>({
@@ -22,7 +23,11 @@ function useGridCache(tableRef: HTMLTableElement | null, enabled: boolean) {
return;
}
observerRef.current = new MutationObserver(() => {
observerRef.current = new MutationObserver((records) => {
if (records.every(isScrollMarginOnlyMutation)) {
return;
}
gridCacheRef.current.dirty = true;
if (activeCellRef.current && !activeCellRef.current.isConnected) {
@@ -35,6 +40,7 @@ function useGridCache(tableRef: HTMLTableElement | null, enabled: boolean) {
childList: true,
attributes: true,
attributeFilter: ["colspan", "rowspan", "hidden", "style"],
attributeOldValue: true,
});
return () => {
@@ -71,4 +77,35 @@ function useGridCache(tableRef: HTMLTableElement | null, enabled: boolean) {
};
}
function stripScrollMargin(style: string | null): string {
if (!style) {
return "";
}
return style
.split(";")
.map((declaration) => declaration.trim())
.filter(
(declaration) => declaration && !declaration.startsWith("scroll-margin"),
)
.join(";");
}
/**
* Focus navigation temporarily writes inline scroll-margin (see `applyFocusAndScroll`).
* Those writes don't affect grid topology, so they should not invalidate the cache.
* Compares the value before the first mutation with the current value, so any
* net change to other inline styles (e.g. `display: none`) still invalidates.
*/
function isScrollMarginOnlyMutation(record: MutationRecord): boolean {
if (record.type !== "attributes" || record.attributeName !== "style") {
return false;
}
return (
stripScrollMargin(record.oldValue) ===
stripScrollMargin((record.target as Element).getAttribute("style"))
);
}
export { useGridCache };