diff --git a/.env b/.env
index 6330f63e9ba..85421b83698 100644
--- a/.env
+++ b/.env
@@ -1,6 +1,6 @@
# Production Build
-BUILD_GRID_VERSION=35.1.0-beta.20260316.108
-BUILD_CHARTS_VERSION=13.1.0-beta.20260316
+BUILD_GRID_VERSION=35.1.0-beta.20260317.135
+BUILD_CHARTS_VERSION=13.1.0-beta.20260317
ENV=local
NX_BATCH_MODE=true
NX_ADD_PLUGINS=false
diff --git a/community-modules/locale/package.json b/community-modules/locale/package.json
index 10c23da8bbd..46e8b6b86a7 100644
--- a/community-modules/locale/package.json
+++ b/community-modules/locale/package.json
@@ -1,6 +1,6 @@
{
"name": "@ag-grid-community/locale",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"description": "Localisation Module for AG Grid, providing translations in 31 languages.",
"main": "./dist/package/main.cjs.js",
"types": "./dist/types/src/main.d.ts",
diff --git a/community-modules/styles/package.json b/community-modules/styles/package.json
index 887d6b5b8cf..ae6060f5592 100644
--- a/community-modules/styles/package.json
+++ b/community-modules/styles/package.json
@@ -1,6 +1,6 @@
{
"name": "@ag-grid-community/styles",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"description": "AG Grid Styles and Themes",
"main": "_index.scss",
"files": [
diff --git a/documentation/ag-grid-docs/package.json b/documentation/ag-grid-docs/package.json
index 1f3ecdf6a23..7c7fc94dbe3 100644
--- a/documentation/ag-grid-docs/package.json
+++ b/documentation/ag-grid-docs/package.json
@@ -2,7 +2,7 @@
"name": "ag-grid-docs",
"description": "Documentation for AG Grid",
"type": "module",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"repository": {
"type": "git",
"url": "https://github.com/ag-grid/ag-grid.git"
@@ -53,17 +53,17 @@
"@types/react": "^18.2.47",
"@types/react-dom": "^18.2.18",
"@pqina/flip": "^1.8.4",
- "ag-charts-angular": "13.1.0-beta.20260316",
- "ag-charts-community": "13.1.0-beta.20260316",
- "ag-charts-enterprise": "13.1.0-beta.20260316",
- "ag-charts-types": "13.1.0-beta.20260316",
- "ag-charts-react": "13.1.0-beta.20260316",
- "ag-charts-vue3": "13.1.0-beta.20260316",
- "ag-grid-angular": "35.1.0-beta.20260316.108",
- "ag-grid-community": "35.1.0-beta.20260316.108",
- "ag-grid-enterprise": "35.1.0-beta.20260316.108",
- "ag-grid-react": "35.1.0-beta.20260316.108",
- "ag-grid-vue3": "35.1.0-beta.20260316.108",
+ "ag-charts-angular": "13.1.0-beta.20260317",
+ "ag-charts-community": "13.1.0-beta.20260317",
+ "ag-charts-enterprise": "13.1.0-beta.20260317",
+ "ag-charts-types": "13.1.0-beta.20260317",
+ "ag-charts-react": "13.1.0-beta.20260317",
+ "ag-charts-vue3": "13.1.0-beta.20260317",
+ "ag-grid-angular": "35.1.0-beta.20260317.135",
+ "ag-grid-community": "35.1.0-beta.20260317.135",
+ "ag-grid-enterprise": "35.1.0-beta.20260317.135",
+ "ag-grid-react": "35.1.0-beta.20260317.135",
+ "ag-grid-vue3": "35.1.0-beta.20260317.135",
"algoliasearch": "^4.18.0",
"astro": "5.16.6",
"cheerio": "^1.0.0",
diff --git a/documentation/ag-grid-docs/src/content/api-documentation/grid-api/api.json b/documentation/ag-grid-docs/src/content/api-documentation/grid-api/api.json
index 914a3bd29af..cc7fcc31d45 100644
--- a/documentation/ag-grid-docs/src/content/api-documentation/grid-api/api.json
+++ b/documentation/ag-grid-docs/src/content/api-documentation/grid-api/api.json
@@ -662,8 +662,24 @@
"displayName": "Row Expand and Collapse",
"description": "See [Row Grouping](./grouping-opening-groups/), [Master Detail](./master-detail/) or [Tree Data](./tree-data-opening-groups/) for more information"
},
- "expandAll": {},
- "collapseAll": {},
+ "expandAll": {
+ "more": {
+ "name": "Expanding Groups",
+ "url": "./grouping-opening-groups/#api"
+ }
+ },
+ "collapseAll": {
+ "more": {
+ "name": "Expanding Groups",
+ "url": "./grouping-opening-groups/#api"
+ }
+ },
+ "resetRowGroupExpansion": {
+ "more": {
+ "name": "Reset Group Expansion",
+ "url": "./grouping-opening-groups/#reset-group-expansion"
+ }
+ },
"setRowNodeExpanded": {
"more": {
"name": "Expand to a Row",
diff --git a/documentation/ag-grid-docs/src/content/api-documentation/grid-options/properties.json b/documentation/ag-grid-docs/src/content/api-documentation/grid-options/properties.json
index d78e3f4bd04..57a60cd22d9 100644
--- a/documentation/ag-grid-docs/src/content/api-documentation/grid-options/properties.json
+++ b/documentation/ag-grid-docs/src/content/api-documentation/grid-options/properties.json
@@ -1731,8 +1731,18 @@
"url": "./server-side-model-pivoting/#supplying-pivot-result-fields-simple"
}
},
- "serverSideSortAllLevels": {},
- "serverSideEnableClientSideSort": {},
+ "serverSideSortAllLevels": {
+ "more": {
+ "name": "Sorting All Levels",
+ "url": "./server-side-model-sorting/#sorting-all-levels"
+ }
+ },
+ "serverSideEnableClientSideSort": {
+ "more": {
+ "name": "Client-side Sorting",
+ "url": "./server-side-model-sorting/#client-side-sorting"
+ }
+ },
"serverSideOnlyRefreshFilteredGroups": {
"more": {
"name": "Filters",
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/_examples/reset-group-expansion/example.spec.ts b/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/_examples/reset-group-expansion/example.spec.ts
new file mode 100644
index 00000000000..20e1e009d24
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/_examples/reset-group-expansion/example.spec.ts
@@ -0,0 +1,34 @@
+import { expect, test } from '@utils/grid/test-utils';
+
+test.agExample(import.meta, () => {
+ test.eachFramework('Example', async ({ page, agIdFor }) => {
+ const australiaId = 'row-group-country-Australia';
+ const australia2004Id = 'row-group-country-Australia-year-2004';
+ const unitedStatesId = 'row-group-country-United States';
+
+ // Initial state: isGroupOpenByDefault expands Australia > 2004
+ await expect(agIdFor.autoGroupExpanded(australiaId)).toBeVisible();
+ await expect(agIdFor.autoGroupExpanded(australia2004Id)).toBeVisible();
+ await expect(agIdFor.autoGroupContracted(unitedStatesId)).toBeVisible();
+
+ // Expand All — United States should now be expanded
+ await page.getByRole('button', { name: 'Expand All' }).click();
+ await expect(agIdFor.autoGroupExpanded(unitedStatesId)).toBeVisible();
+
+ // Reset to Defaults — back to isGroupOpenByDefault state
+ await page.getByRole('button', { name: 'Reset to Defaults' }).click();
+ await expect(agIdFor.autoGroupExpanded(australiaId)).toBeVisible();
+ await expect(agIdFor.autoGroupExpanded(australia2004Id)).toBeVisible();
+ await expect(agIdFor.autoGroupContracted(unitedStatesId)).toBeVisible();
+
+ // Collapse All — everything collapsed
+ await page.getByRole('button', { name: 'Collapse All' }).click();
+ await expect(agIdFor.autoGroupContracted(australiaId)).toBeVisible();
+
+ // Reset to Defaults — Australia > 2004 expanded again
+ await page.getByRole('button', { name: 'Reset to Defaults' }).click();
+ await expect(agIdFor.autoGroupExpanded(australiaId)).toBeVisible();
+ await expect(agIdFor.autoGroupExpanded(australia2004Id)).toBeVisible();
+ await expect(agIdFor.autoGroupContracted(unitedStatesId)).toBeVisible();
+ });
+});
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/_examples/reset-group-expansion/index.html b/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/_examples/reset-group-expansion/index.html
new file mode 100644
index 00000000000..1bd6b185e1f
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/_examples/reset-group-expansion/index.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/_examples/reset-group-expansion/main.ts b/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/_examples/reset-group-expansion/main.ts
new file mode 100644
index 00000000000..22017c70ac5
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/_examples/reset-group-expansion/main.ts
@@ -0,0 +1,64 @@
+import type { GridApi, GridOptions, IsGroupOpenByDefaultParams } from 'ag-grid-community';
+import {
+ ClientSideRowModelApiModule,
+ ClientSideRowModelModule,
+ ModuleRegistry,
+ RowApiModule,
+ ValidationModule,
+ createGrid,
+} from 'ag-grid-community';
+import { RowGroupingModule } from 'ag-grid-enterprise';
+
+ModuleRegistry.registerModules([
+ RowApiModule,
+ ClientSideRowModelModule,
+ ClientSideRowModelApiModule,
+ RowGroupingModule,
+ ...(process.env.NODE_ENV !== 'production' ? [ValidationModule] : []),
+]);
+
+let gridApi: GridApi;
+
+const gridOptions: GridOptions = {
+ columnDefs: [
+ { field: 'country', rowGroup: true },
+ { field: 'year', rowGroup: true },
+ { field: 'sport' },
+ { field: 'athlete' },
+ { field: 'total' },
+ ],
+ defaultColDef: {
+ flex: 1,
+ minWidth: 100,
+ },
+ autoGroupColumnDef: {
+ minWidth: 200,
+ },
+ isGroupOpenByDefault: (params: IsGroupOpenByDefaultParams) => {
+ const route = params.rowNode.getRoute();
+ const destPath = ['Australia', '2004'];
+ return !!route?.every((item, idx) => destPath[idx] === item);
+ },
+};
+
+function onBtExpandAll() {
+ gridApi!.expandAll();
+}
+
+function onBtCollapseAll() {
+ gridApi!.collapseAll();
+}
+
+function onBtResetExpansion() {
+ gridApi!.resetRowGroupExpansion();
+}
+
+// setup the grid after the page has finished loading
+document.addEventListener('DOMContentLoaded', function () {
+ const gridDiv = document.querySelector('#myGrid')!;
+ gridApi = createGrid(gridDiv, gridOptions);
+
+ fetch('https://www.ag-grid.com/example-assets/olympic-winners.json')
+ .then((response) => response.json())
+ .then((data: IOlympicData[]) => gridApi!.setGridOption('rowData', data));
+});
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/_examples/reset-group-expansion/styles.css b/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/_examples/reset-group-expansion/styles.css
new file mode 100644
index 00000000000..072f3617e55
--- /dev/null
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/_examples/reset-group-expansion/styles.css
@@ -0,0 +1,10 @@
+.example-wrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+#myGrid {
+ flex: 1 1 0px;
+ width: 100%;
+}
diff --git a/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/index.mdoc b/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/index.mdoc
index 3ce3a88ba68..947bf5b7d55 100644
--- a/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/grouping-opening-groups/index.mdoc
@@ -75,7 +75,7 @@ The row group expansion state can be saved and restored as part of [Grid State](
{% /note %}
The grid exposes API methods to expand or collapse groups programmatically.
-{% apiDocumentation source="grid-api/api.json" section="rowExpansion" names=["setRowNodeExpanded", "expandAll", "collapseAll"] /%}
+{% apiDocumentation source="grid-api/api.json" section="rowExpansion" names=["setRowNodeExpanded", "expandAll", "collapseAll", "resetRowGroupExpansion"] /%}
### Expand Row Ancestors
@@ -93,3 +93,11 @@ const expandToRow = () => {
}
}
```
+
+### Reset Group Expansion
+
+After users have expanded or collapsed groups, `resetRowGroupExpansion()` discards all overrides and re-evaluates each group against the configured defaults (`groupDefaultExpanded` or `isGroupOpenByDefault`).
+
+In the example below, the `isGroupOpenByDefault` callback expands the `Australia > 2004` path by default. Try expanding or collapsing groups, then click **Reset to Defaults** to restore the original expansion state.
+
+{% gridExampleRunner title="Reset Group Expansion" name="reset-group-expansion" /%}
diff --git a/documentation/update-algolia-indices/package.json b/documentation/update-algolia-indices/package.json
index 16a665039e5..d6896934039 100644
--- a/documentation/update-algolia-indices/package.json
+++ b/documentation/update-algolia-indices/package.json
@@ -1,6 +1,6 @@
{
"name": "update-algolia-indices",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"description": "Update algolia indices",
"main": "src/index.ts",
"type": "module",
diff --git a/package.json b/package.json
index ceab2c959a3..893196768a6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ag-grid",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"license": "MIT",
"scripts": {
"compressVideo": "tsx external/ag-website-shared/scripts/compress-video",
diff --git a/packages/ag-grid-angular/package.json b/packages/ag-grid-angular/package.json
index 9308fb4c7c8..4cdf1133ee0 100644
--- a/packages/ag-grid-angular/package.json
+++ b/packages/ag-grid-angular/package.json
@@ -1,6 +1,6 @@
{
"name": "ag-grid-angular",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"description": "AG Grid Angular Component",
"scripts": {
"clean": "rimraf dist",
@@ -15,7 +15,7 @@
"module": "./dist/ag-grid-angular/fesm2022/ag-grid-angular.mjs",
"typings": "./dist/ag-grid-angular/index.d.ts",
"dependencies": {
- "ag-grid-community": "35.1.0-beta.20260316.108",
+ "ag-grid-community": "35.1.0-beta.20260317.135",
"@angular/animations": "^18.0.7",
"@angular/common": "^18.0.7",
"@angular/compiler": "^18.0.7",
@@ -27,7 +27,7 @@
"zone.js": "~0.15.1"
},
"devDependencies": {
- "ag-grid-community": "35.1.0-beta.20260316.108",
+ "ag-grid-community": "35.1.0-beta.20260317.135",
"@angular-devkit/build-angular": "^18.0.7",
"@angular/cli": "^18.0.7",
"@angular/forms": "^18.0.7",
diff --git a/packages/ag-grid-angular/projects/ag-grid-angular/package.json b/packages/ag-grid-angular/projects/ag-grid-angular/package.json
index 221f2b96f12..17271e576c8 100644
--- a/packages/ag-grid-angular/projects/ag-grid-angular/package.json
+++ b/packages/ag-grid-angular/projects/ag-grid-angular/package.json
@@ -1,6 +1,6 @@
{
"name": "ag-grid-angular",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"description": "AG Grid Angular Component",
"license": "MIT",
"peerDependencies": {
@@ -8,7 +8,7 @@
"@angular/core": ">= 18.0.0"
},
"dependencies": {
- "ag-grid-community": "35.1.0-beta.20260316.108",
+ "ag-grid-community": "35.1.0-beta.20260317.135",
"tslib": "^2.3.0"
},
"repository": {
diff --git a/packages/ag-grid-community/package.json b/packages/ag-grid-community/package.json
index 35c21f8d2e9..a5d40612bb5 100644
--- a/packages/ag-grid-community/package.json
+++ b/packages/ag-grid-community/package.json
@@ -1,6 +1,6 @@
{
"name": "ag-grid-community",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"description": "Advanced Data Grid / Data Table supporting Javascript / Typescript / React / Angular / Vue",
"main": "./dist/package/main.cjs.js",
"types": "./dist/types/src/main.d.ts",
@@ -119,7 +119,7 @@
],
"homepage": "https://www.ag-grid.com/",
"dependencies": {
- "ag-charts-types": "13.1.0-beta.20260316"
+ "ag-charts-types": "13.1.0-beta.20260317"
},
"devDependencies": {
"web-streams-polyfill": "^3.3.2",
diff --git a/packages/ag-grid-community/src/api/gridApi.ts b/packages/ag-grid-community/src/api/gridApi.ts
index f5b6d21ecde..90ad0dbddb3 100644
--- a/packages/ag-grid-community/src/api/gridApi.ts
+++ b/packages/ag-grid-community/src/api/gridApi.ts
@@ -529,6 +529,15 @@ export interface _CsrmSsrmSharedGridApi {
* @agModule `ClientSideRowModelApiModule / ServerSideRowModelApiModule`
*/
collapseAll(): void;
+
+ /**
+ * Reset all group expansion to defaults, as determined by `groupDefaultExpanded`,
+ * `isGroupOpenByDefault`, or `isServerSideGroupOpenByDefault`.
+ * Any user-initiated expand/collapse overrides are discarded.
+ *
+ * @agModule `ClientSideRowModelApiModule / ServerSideRowModelApiModule`
+ */
+ resetRowGroupExpansion(): void;
}
export interface _RowModelSharedApi {
@@ -631,7 +640,7 @@ export interface _ColumnResizeApi {
}
export interface _ColumnMoveApi {
- /** Moves the column at `fromIdex` to `toIndex`. The column is first removed, then added at the `toIndex` location, thus index locations will change to the right of the column after the removal. */
+ /** Moves the column at `fromIndex` to `toIndex`. The column is first removed, then added at the `toIndex` location, thus index locations will change to the right of the column after the removal. */
moveColumnByIndex(fromIndex: number, toIndex: number): void;
/** Moves columns to `toIndex`. The columns are first removed, then added at the `toIndex` location, thus index locations will change to the right of the column after the removal. */
diff --git a/packages/ag-grid-community/src/api/gridApiFunctions.ts b/packages/ag-grid-community/src/api/gridApiFunctions.ts
index 49f56ac938b..afd445844a6 100644
--- a/packages/ag-grid-community/src/api/gridApiFunctions.ts
+++ b/packages/ag-grid-community/src/api/gridApiFunctions.ts
@@ -287,6 +287,7 @@ export const gridApiFunctionsMap: Record =
...mod<_CsrmSsrmSharedGridApi>('CsrmSsrmSharedApi', {
expandAll: 0,
collapseAll: 0,
+ resetRowGroupExpansion: 0,
}),
...mod<_SsrmInfiniteSharedGridApi>('SsrmInfiniteSharedApi', {
setRowCount: 0,
diff --git a/packages/ag-grid-community/src/api/rowModelSharedApi.ts b/packages/ag-grid-community/src/api/rowModelSharedApi.ts
index 87950571ed2..c012dbf703d 100644
--- a/packages/ag-grid-community/src/api/rowModelSharedApi.ts
+++ b/packages/ag-grid-community/src/api/rowModelSharedApi.ts
@@ -22,3 +22,7 @@ export function resetRowHeights(beans: BeanCollection) {
}
beans.rowModel?.resetRowHeights();
}
+
+export function resetRowGroupExpansion(beans: BeanCollection) {
+ beans.expansionSvc?.resetExpansion();
+}
diff --git a/packages/ag-grid-community/src/api/sharedApiModule.ts b/packages/ag-grid-community/src/api/sharedApiModule.ts
index aa067bd639a..25239071127 100644
--- a/packages/ag-grid-community/src/api/sharedApiModule.ts
+++ b/packages/ag-grid-community/src/api/sharedApiModule.ts
@@ -1,7 +1,13 @@
import type { _ModuleWithApi } from '../interfaces/iModule';
import { VERSION } from '../version';
import type { _CsrmSsrmSharedGridApi, _RowModelSharedApi, _SsrmInfiniteSharedGridApi } from './gridApi';
-import { collapseAll, expandAll, onRowHeightChanged, resetRowHeights } from './rowModelSharedApi';
+import {
+ collapseAll,
+ expandAll,
+ onRowHeightChanged,
+ resetRowGroupExpansion,
+ resetRowHeights,
+} from './rowModelSharedApi';
import { getCacheBlockState, isLastRowIndexKnown, setRowCount } from './ssrmInfiniteSharedApi';
// these modules are not used in core, but are shared between multiple other modules
@@ -12,7 +18,7 @@ import { getCacheBlockState, isLastRowIndexKnown, setRowCount } from './ssrmInfi
export const CsrmSsrmSharedApiModule: _ModuleWithApi<_CsrmSsrmSharedGridApi> = {
moduleName: 'CsrmSsrmSharedApi',
version: VERSION,
- apiFunctions: { expandAll, collapseAll },
+ apiFunctions: { expandAll, collapseAll, resetRowGroupExpansion },
};
/**
diff --git a/packages/ag-grid-community/src/clientSideRowModel/clientSideNodeManager.ts b/packages/ag-grid-community/src/clientSideRowModel/clientSideNodeManager.ts
index adc4c0ca83f..5fbfc35ec1a 100644
--- a/packages/ag-grid-community/src/clientSideRowModel/clientSideNodeManager.ts
+++ b/packages/ag-grid-community/src/clientSideRowModel/clientSideNodeManager.ts
@@ -338,7 +338,6 @@ export class ClientSideNodeManager extends BeanStub {
node.parent = this.rootNode;
node.level = level;
node.group = false;
- node.expanded = false;
if (sourceRowIndex != null) {
node.sourceRowIndex = sourceRowIndex;
}
@@ -413,6 +412,7 @@ const adjustAddIndexForDataPath = (allLeafs: RowNode[], addIndex:
const initRootNode = (rootNode: RowNode): RowNode => {
rootNode.group = true;
rootNode.level = -1;
+ rootNode._expanded = true;
rootNode.id = 'ROOT_NODE_ID';
if (rootNode._leafs?.length !== 0) {
rootNode._leafs = [];
diff --git a/packages/ag-grid-community/src/context/context.ts b/packages/ag-grid-community/src/context/context.ts
index 3764ad5ca7b..b6e754ebb6b 100644
--- a/packages/ag-grid-community/src/context/context.ts
+++ b/packages/ag-grid-community/src/context/context.ts
@@ -60,7 +60,11 @@ import type { AgGridCommon } from '../interfaces/iCommon';
import type { IContextMenuService } from '../interfaces/iContextMenu';
import type { ICsvCreator } from '../interfaces/iCsvCreator';
import type { IExcelCreator } from '../interfaces/iExcelCreator';
-import type { IExpansionService } from '../interfaces/iExpansionService';
+import type {
+ IExpansionService,
+ RowGroupBulkExpansionState,
+ RowGroupExpansionState,
+} from '../interfaces/iExpansionService';
import type { IFindService } from '../interfaces/iFind';
import type { IFooterService } from '../interfaces/iFooterService';
import type { IFrameworkOverrides } from '../interfaces/iFrameworkOverrides';
@@ -334,7 +338,7 @@ interface CoreBeanCollection
pageBounds: PageBoundsService;
apiFunctionSvc: ApiFunctionService;
gridDestroySvc: GridDestroyService;
- expansionSvc?: IExpansionService;
+ expansionSvc?: IExpansionService;
sideBar?: ISideBarService;
ssrmTxnManager?: IServerSideTransactionManager;
aggFuncSvc?: IAggFuncService;
diff --git a/packages/ag-grid-community/src/entities/rowNode.ts b/packages/ag-grid-community/src/entities/rowNode.ts
index f4b8848209a..64c34a75ec4 100644
--- a/packages/ag-grid-community/src/entities/rowNode.ts
+++ b/packages/ag-grid-community/src/entities/rowNode.ts
@@ -244,8 +244,23 @@ export class RowNode
/** Server Side Row Model Only - the children are in an infinite cache. */
public childStore: IServerSideStore | null;
- /** `true` if group is expanded, otherwise `false`. */
- public expanded: boolean;
+ /**
+ * Backing field for `expanded` property.
+ * - `true`/`false`: explicit expansion state.
+ * - `null`: triggers lazy evaluation — in CSRM, SSRM, getter resolves the default on first access and caches it.
+ * - `undefined`: uninitialized, means false.
+ */
+ public _expanded: boolean | null | undefined = undefined;
+
+ /** `true` if group or master row is expanded. */
+ public get expanded(): boolean {
+ const expansionSvc = this.beans.expansionSvc;
+ return expansionSvc ? expansionSvc.isExpanded(this) : this.level === -1 ? true : !!this._expanded;
+ }
+
+ public set expanded(value: boolean) {
+ this._expanded = value;
+ }
/** If using footers, reference to the footer node for this group. */
public sibling: RowNode;
diff --git a/packages/ag-grid-community/src/interfaces/iExpansionService.ts b/packages/ag-grid-community/src/interfaces/iExpansionService.ts
index 623f3e9c3ce..e5f341355d2 100644
--- a/packages/ag-grid-community/src/interfaces/iExpansionService.ts
+++ b/packages/ag-grid-community/src/interfaces/iExpansionService.ts
@@ -26,9 +26,7 @@ export interface RowGroupBulkExpansionState {
}
/** @internal AG_GRID_INTERNAL - Not for public use. Can change / be removed at any time. */
-export interface IExpansionService<
- T extends RowGroupExpansionState | RowGroupBulkExpansionState = RowGroupExpansionState,
-> {
+export interface IExpansionService {
addExpandedCss(classes: string[], rowNode: RowNode): void;
getRowExpandedListeners(rowCtrl: RowCtrl): {
@@ -40,6 +38,7 @@ export interface IExpansionService<
getExpansionState(): T;
expandAll(value: boolean): void;
+ resetExpansion(): void;
onGroupExpandedOrCollapsed(): void;
@@ -47,5 +46,7 @@ export interface IExpansionService<
isExpandable(rowNode: RowNode): boolean;
+ isExpanded(rowNode: RowNode): boolean;
+
setDetailsExpansionState(detailGridApi: GridApi): void;
}
diff --git a/packages/ag-grid-community/src/misc/state/stateService.ts b/packages/ag-grid-community/src/misc/state/stateService.ts
index 9230e0413d7..cdd850ff602 100644
--- a/packages/ag-grid-community/src/misc/state/stateService.ts
+++ b/packages/ag-grid-community/src/misc/state/stateService.ts
@@ -49,19 +49,7 @@ export class StateService extends BeanStub implements NamedBean {
);
// If user is doing a manual expand all node by node, we don't want to process one at a time.
// EVENT_ROW_GROUP_OPENED is already async, so no impact of making the state async here.
- private readonly onRowGroupOpenedDebounced = _debounce(
- this,
- () => {
- if (this.beans.gos.get('ssrmExpandAllAffectsAllRows')) {
- this.updateCachedState('ssrmRowGroupExpansion', this.getRowGroupExpansionState());
- this.updateCachedState('rowGroupExpansion', undefined);
- } else {
- this.updateCachedState('rowGroupExpansion', this.getRowGroupExpansionState() as RowGroupExpansionState);
- this.updateCachedState('ssrmRowGroupExpansion', undefined);
- }
- },
- 0
- );
+ private readonly onRowGroupOpenedDebounced = _debounce(this, () => this.updateGroupExpansionState(), 0);
// similar to row expansion, want to debounce. However, selection is synchronous, so need to mark as stale in case `getState` is called.
private readonly onRowSelectedDebounced = _debounce(
this,
@@ -225,7 +213,15 @@ export class StateService extends BeanStub implements NamedBean {
columnPivotModeChanged: onUpdate('pivot'),
columnRowGroupChanged: onUpdate('rowGroup'),
sortChanged: onUpdate('sort'),
- newColumnsLoaded: this.updateColumnAndGroupState.bind(this),
+ newColumnsLoaded: ({ source }) => {
+ this.updateColumnAndGroupState();
+ // When setGridOption('columnDefs') changes row groups, columnRowGroupChanged fires
+ // while changeEventsDispatching=true, so CSRM defers its re-group to newColumnsLoaded.
+ // Re-read expansion state here so the cache reflects the post-re-group node IDs.
+ if (source !== 'gridInitializing' && this.isClientSideRowModel) {
+ this.onRowGroupOpenedDebounced();
+ }
+ },
columnGroupOpened: () => this.updateCachedState('columnGroup', this.getColumnGroupState()),
});
}
@@ -249,7 +245,10 @@ export class StateService extends BeanStub implements NamedBean {
if (shouldSetState('filter', filterState)) {
this.setFilterState(filterState);
}
- if (shouldSetState('rowGroupExpansion', rowGroupExpansionState)) {
+ if (
+ shouldSetState('rowGroupExpansion', rowGroupExpansionState) ||
+ shouldSetState('ssrmRowGroupExpansion', ssrmRowGroupExpansion)
+ ) {
this.setRowGroupExpansionState(ssrmRowGroupExpansion, rowGroupExpansionState, source);
}
if (shouldSetState('rowSelection', rowSelectionState)) {
@@ -264,13 +263,7 @@ export class StateService extends BeanStub implements NamedBean {
const updateCachedState = this.updateCachedState.bind(this);
updateCachedState('filter', this.getFilterState());
- if (this.beans.gos.get('ssrmExpandAllAffectsAllRows')) {
- updateCachedState('ssrmRowGroupExpansion', this.getRowGroupExpansionState());
- updateCachedState('rowGroupExpansion', undefined);
- } else {
- updateCachedState('rowGroupExpansion', this.getRowGroupExpansionState() as RowGroupExpansionState);
- updateCachedState('ssrmRowGroupExpansion', undefined);
- }
+ this.updateGroupExpansionState();
updateCachedState('rowSelection', this.getRowSelectionState());
updateCachedState('pagination', this.getPaginationState());
@@ -282,13 +275,7 @@ export class StateService extends BeanStub implements NamedBean {
const updateCachedState = this.updateCachedState.bind(this);
const updateRowGroupExpansionState = () => {
this.updateRowGroupExpansionStateTimer = 0;
- if (this.beans.gos.get('ssrmExpandAllAffectsAllRows')) {
- updateCachedState('ssrmRowGroupExpansion', this.getRowGroupExpansionState());
- updateCachedState('rowGroupExpansion', undefined);
- } else {
- updateCachedState('rowGroupExpansion', this.getRowGroupExpansionState() as RowGroupExpansionState);
- updateCachedState('ssrmRowGroupExpansion', undefined);
- }
+ this.updateGroupExpansionState();
};
const updateFilterState = () => updateCachedState('filter', this.getFilterState());
@@ -835,13 +822,16 @@ export class StateService extends BeanStub implements NamedBean {
this.beans.selectionSvc?.setSelectionState(rowSelectionState, source, source === 'api');
}
- private getRowGroupExpansionState(): RowGroupExpansionState | RowGroupBulkExpansionState | undefined {
- const { expansionSvc } = this.beans;
- if (!expansionSvc) {
- return undefined;
- }
+ private updateGroupExpansionState(): void {
+ const { expansionSvc, gos } = this.beans;
+ const state = expansionSvc?.getExpansionState();
+ const ssrmExpandAllAffectsAllRows = gos.get('ssrmExpandAllAffectsAllRows');
- return expansionSvc.getExpansionState();
+ this.updateCachedState('ssrmRowGroupExpansion', ssrmExpandAllAffectsAllRows ? state : undefined);
+ this.updateCachedState(
+ 'rowGroupExpansion',
+ ssrmExpandAllAffectsAllRows ? undefined : (state as RowGroupExpansionState)
+ );
}
private getRowPinningState(): RowPinningState | undefined {
@@ -862,13 +852,9 @@ export class StateService extends BeanStub implements NamedBean {
rowGroupExpansionState: RowGroupExpansionState | undefined,
source: 'gridInitializing' | 'api'
): void {
- const expansionSvc = this.beans.expansionSvc;
- if (!expansionSvc) {
- return;
- }
-
- const newExpansionState = rowGroupExpansionState ?? { expandedRowGroupIds: [], collapsedRowGroupIds: [] };
- expansionSvc.setExpansionState(newExpansionState, source);
+ const state = ssrmRowGroupExpansionState ??
+ rowGroupExpansionState ?? { expandedRowGroupIds: [], collapsedRowGroupIds: [] };
+ this.beans.expansionSvc?.setExpansionState(state, source);
}
private updateColumnState(features: (keyof GridState)[]): void {
diff --git a/packages/ag-grid-community/src/rendering/row/rowCtrl.ts b/packages/ag-grid-community/src/rendering/row/rowCtrl.ts
index 2eb30ebf778..3d2515d6937 100644
--- a/packages/ag-grid-community/src/rendering/row/rowCtrl.ts
+++ b/packages/ag-grid-community/src/rendering/row/rowCtrl.ts
@@ -287,7 +287,7 @@ export class RowCtrl extends BeanStub {
this.executeSlideAndFadeAnimations(gui);
if (this.rowNode.group) {
- _setAriaExpanded(gui.element, this.rowNode.expanded == true);
+ _setAriaExpanded(gui.element, !!this.rowNode.expanded);
}
this.setRowCompRowId(comp);
diff --git a/packages/ag-grid-community/src/selection/selectionService.ts b/packages/ag-grid-community/src/selection/selectionService.ts
index ffa82b11849..4b565c4035f 100644
--- a/packages/ag-grid-community/src/selection/selectionService.ts
+++ b/packages/ag-grid-community/src/selection/selectionService.ts
@@ -543,7 +543,7 @@ export class SelectionService extends BaseSelectionService implements NamedBean,
return;
}
- if (!node.expanded && !node.footer) {
+ if (!node.footer && !node.expanded) {
// even with groupSelectsChildren, do this recursively as only the filtered children
// are considered as the current page
const recursivelyAddChildren = (child: RowNode) => {
diff --git a/packages/ag-grid-community/src/valueService/valueService.ts b/packages/ag-grid-community/src/valueService/valueService.ts
index fa334c4c450..bbb257c8a5a 100644
--- a/packages/ag-grid-community/src/valueService/valueService.ts
+++ b/packages/ag-grid-community/src/valueService/valueService.ts
@@ -235,19 +235,22 @@ export class ValueService extends BeanStub implements NamedBean {
// in the header when the group is open.
// Result is: isOpenedGroup && !groupShowsAggData
- // Check isOpenedGroup conditions: node.group && node.expanded && !node.footer && !isPivotLeaf
- if (!node.group || !node.expanded || node.footer) {
+ // Check isOpenedGroup conditions: node.group && !node.footer && !isPivotLeaf && node.expanded
+ // The root node (level -1) is always expanded but should not suppress its agg data display.
+ if (!node.group || node.footer || node.level === -1) {
+ return false;
+ }
+ // groupShowsAggData = this.gos.get('groupSuppressBlankHeader') || !node.sibling
+ // We return true only if !groupShowsAggData, i.e., !groupSuppressBlankHeader && node.sibling
+ if (!node.sibling || this.gos.get('groupSuppressBlankHeader')) {
return false;
}
// When in pivot mode, leafGroups cannot be expanded
if (node.leafGroup && this.colModel.isPivotMode()) {
- return false; // isPivotLeaf - not an opened group
+ return false;
}
-
- // isOpenedGroup is true. Now check if groupShowsAggData.
- // groupShowsAggData = this.gos.get('groupSuppressBlankHeader') || !node.sibling
- // We return true only if !groupShowsAggData, i.e., !groupSuppressBlankHeader && node.sibling
- return !!node.sibling && !this.gos.get('groupSuppressBlankHeader');
+ // node.expanded (getter with side effects) evaluated last
+ return !!node.expanded;
}
private resolveValue(
diff --git a/packages/ag-grid-community/src/version.ts b/packages/ag-grid-community/src/version.ts
index 353202c0311..63095b37ec2 100644
--- a/packages/ag-grid-community/src/version.ts
+++ b/packages/ag-grid-community/src/version.ts
@@ -1,2 +1,2 @@
// DO NOT UPDATE MANUALLY: Generated from script during build time
-export const VERSION = '35.1.0-beta.20260316.108';
+export const VERSION = '35.1.0-beta.20260317.135';
diff --git a/packages/ag-grid-enterprise/package.json b/packages/ag-grid-enterprise/package.json
index 40265090674..b739aadeff5 100644
--- a/packages/ag-grid-enterprise/package.json
+++ b/packages/ag-grid-enterprise/package.json
@@ -1,6 +1,6 @@
{
"name": "ag-grid-enterprise",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"description": "Advanced Data Grid / Data Table supporting Javascript / Typescript / React / Angular / Vue",
"main": "./dist/package/main.cjs.js",
"types": "./dist/types/src/main.d.ts",
@@ -113,15 +113,15 @@
],
"homepage": "https://www.ag-grid.com/",
"dependencies": {
- "ag-grid-community": "35.1.0-beta.20260316.108"
+ "ag-grid-community": "35.1.0-beta.20260317.135"
},
"optionalDependencies": {
- "ag-charts-community": "13.1.0-beta.20260316",
- "ag-charts-enterprise": "13.1.0-beta.20260316"
+ "ag-charts-community": "13.1.0-beta.20260317",
+ "ag-charts-enterprise": "13.1.0-beta.20260317"
},
"devDependencies": {
- "ag-charts-community": "13.1.0-beta.20260316",
- "ag-charts-enterprise": "13.1.0-beta.20260316",
+ "ag-charts-community": "13.1.0-beta.20260317",
+ "ag-charts-enterprise": "13.1.0-beta.20260317",
"@types/jest": "^29.5.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.7.0",
diff --git a/packages/ag-grid-enterprise/src/masterDetail/masterDetailService.ts b/packages/ag-grid-enterprise/src/masterDetail/masterDetailService.ts
index b3b79bf3127..888963f3784 100644
--- a/packages/ag-grid-enterprise/src/masterDetail/masterDetailService.ts
+++ b/packages/ag-grid-enterprise/src/masterDetail/masterDetailService.ts
@@ -18,8 +18,6 @@ import {
_observeResize,
} from 'ag-grid-community';
-import { _getRowDefaultExpanded } from '../rowHierarchy/rowHierarchyUtils';
-
export class MasterDetailService extends BeanStub implements NamedBean, IMasterDetailService {
beanName: BeanName = 'masterDetailSvc' as const;
@@ -96,15 +94,14 @@ export class MasterDetailService extends BeanStub implements NamedBean, IMasterD
}
}
- const beans = this.beans;
if (!treeData) {
// Note that with treeData the initialization of the expansed state is delegated to treeGroupStrategy
- if (newMaster && created) {
- const level = beans.rowGroupColsSvc?.columns.length ?? 0;
- row.expanded = _getRowDefaultExpanded(beans, row, level, false);
- } else if (!newMaster && oldMaster) {
- // if changing AWAY from master, then un-expand, otherwise next time it's shown it is expanded again
- row.expanded = false;
+ if (
+ (newMaster && created) ||
+ // if changing AWAY from master, forget current state
+ (!newMaster && oldMaster)
+ ) {
+ row._expanded ??= null;
}
}
diff --git a/packages/ag-grid-enterprise/src/rowGrouping/groupStrategy/groupStrategy.ts b/packages/ag-grid-enterprise/src/rowGrouping/groupStrategy/groupStrategy.ts
index 2e9ff4eb2ef..c5437ce9213 100644
--- a/packages/ag-grid-enterprise/src/rowGrouping/groupStrategy/groupStrategy.ts
+++ b/packages/ag-grid-enterprise/src/rowGrouping/groupStrategy/groupStrategy.ts
@@ -1,15 +1,14 @@
import type {
AgColumn,
ChangedPath,
- GridOptions,
IRowNode,
+ InitialGroupOrderComparator,
RefreshModelParams,
_ChangedRowNodes,
} from 'ag-grid-community';
import { BeanStub, RowNode, _csrmFirstLeaf, _forEachChangedGroupDepthFirst, _warn } from 'ag-grid-community';
import type { IRowGroupingStrategy } from '../../rowHierarchy/rowHierarchyUtils';
-import { _getRowDefaultExpanded } from '../../rowHierarchy/rowHierarchyUtils';
import { setRowNodeGroup } from '../rowGroupingUtils';
import type { GroupColumn } from './groupColumns';
import { groupColumnsChanged, makeGroupColumns } from './groupColumns';
@@ -82,8 +81,9 @@ export class GroupStrategy extends BeanStub implements IRowGroupingStrategy {
}
public execute(rootNode: RowNode, params: RefreshModelParams): void {
- const changedPath = params.changedPath;
- if (this.initRefresh(params)) {
+ const changedPath = params.changedPath!;
+ const refreshResult = this.initRefresh(params);
+ if (refreshResult !== 'skip') {
const changedRowNodes = params.changedRowNodes;
if (changedRowNodes) {
this.handleDeltaUpdate(rootNode, changedPath, changedRowNodes, !!params.animate);
@@ -137,11 +137,11 @@ export class GroupStrategy extends BeanStub implements IRowGroupingStrategy {
});
}
- private initRefresh(params: RefreshModelParams): boolean {
+ private initRefresh(params: RefreshModelParams): 'skip' | 'refresh' | 'groupColsChanged' {
const { rowGroupColsSvc, colModel, gos } = this.beans;
- const pivotMode = colModel.isPivotMode();
- this.pivotMode = pivotMode;
- this.groupEmpty = pivotMode || !gos.get('groupAllowUnbalanced');
+
+ this.pivotMode = colModel.isPivotMode();
+ this.groupEmpty = this.pivotMode || !gos.get('groupAllowUnbalanced');
const cols = rowGroupColsSvc?.columns;
const groupCols = this.groupCols;
const afterColumnsChanged = params.afterColumnsChanged;
@@ -149,16 +149,19 @@ export class GroupStrategy extends BeanStub implements IRowGroupingStrategy {
this.checkGroupCols = false;
if (groupCols && !groupColumnsChanged(groupCols, cols)) {
if (afterColumnsChanged) {
- return false; // no change to grouping
+ return 'skip'; // no change to grouping
}
} else {
- // Group columns changed.
- params.animate = false; // if grouping columns change, we don't animate the regrouping
- makeGroupColumns(cols, this.groupCols);
+ params.animate = false;
+ // If the top-level group column changed, every existing node ID will differ after
+ // rebuild, so no group nodes can be reused. Check before makeGroupColumns overwrites groupCols.
+ const topLevelChanged = groupCols[0]?.col.getId() !== cols?.[0]?.getId();
+ makeGroupColumns(cols, groupCols);
+ return topLevelChanged ? 'refresh' : 'groupColsChanged';
}
}
- return true;
+ return 'refresh';
}
private handleDeltaUpdate(
@@ -223,7 +226,7 @@ export class GroupStrategy extends BeanStub implements IRowGroupingStrategy {
}
private orderGroups(rootNode: RowNode): void {
- const initialGroupOrderComparator: GridOptions['initialGroupOrderComparator'] =
+ const initialGroupOrderComparator: InitialGroupOrderComparator | undefined =
this.gos.getCallback('initialGroupOrderComparator');
if (!initialGroupOrderComparator) {
return;
@@ -408,10 +411,16 @@ export class GroupStrategy extends BeanStub implements IRowGroupingStrategy {
}
private shotgunResetEverything(rootNode: RowNode): void {
- // groups are about to get disposed, so need to deselect any that are selected
- this.beans.selectionSvc?.filterFromSelection?.((node) => !node.group);
+ const groupsById = this.nonLeafsById;
+
+ // Mark all existing group nodes as stale by nulling childrenAfterGroup.
+ // Nodes that are reused during insertion will have childrenAfterGroup reset to [].
+ // Nodes still null after insertion are stale and will be destroyed.
+ for (const node of groupsById.values()) {
+ node.childrenAfterGroup = null;
+ node.childrenMapped = null;
+ }
- this.nonLeafsById.clear();
// because we are not creating the root node each time, we have the logic
// here to change leafGroup once.
rootNode.leafGroup = !this.groupCols?.length;
@@ -431,11 +440,37 @@ export class GroupStrategy extends BeanStub implements IRowGroupingStrategy {
for (let i = 0, len = allLeafs.length; i < len; ++i) {
this.insertOneNode(rootNode, allLeafs[i]);
}
+
+ // Destroy stale group nodes that were not reused during insertion
+ this.destroyStaleGroups(groupsById);
+ }
+
+ /** Remove and destroy group nodes that were not reused (still have childrenAfterGroup === null) */
+ private destroyStaleGroups(groupsById: Map): void {
+ const selectionSvc = this.beans.selectionSvc;
+ let nodesToDeselect: RowNode[] | undefined;
+ for (const [id, node] of groupsById) {
+ if (node.childrenAfterGroup !== null) {
+ continue;
+ }
+ if (selectionSvc && node.isSelected()) {
+ (nodesToDeselect ??= []).push(node);
+ }
+ groupsById.delete(id);
+ node._destroy(false);
+ }
+ if (nodesToDeselect) {
+ selectionSvc!.setNodesSelected({
+ nodes: nodesToDeselect,
+ newValue: false,
+ source: 'rowGroupChanged',
+ });
+ }
}
private insertOneNode(rootNode: RowNode, childNode: RowNode): void {
let parentGroup = rootNode;
- const { beans, pivotMode, groupCols, groupEmpty } = this;
+ const { beans, groupCols, groupEmpty } = this;
const valueSvc = beans.valueSvc;
if (!groupCols) {
return;
@@ -460,7 +495,7 @@ export class GroupStrategy extends BeanStub implements IRowGroupingStrategy {
const isLeafLevel = nextLevel >= len - 1;
const newGroup = this.createGroup(parentGroup, groupCol, key, nextLevel, isLeafLevel, childNode);
this.addToParent(newGroup, parentGroup);
- this.setExpandedInitialValue(pivotMode, newGroup);
+
parentGroup = newGroup;
}
if (!parentGroup.group) {
@@ -485,37 +520,62 @@ export class GroupStrategy extends BeanStub implements IRowGroupingStrategy {
const id = (parent.level >= 0 ? parent.id! + '-' : 'row-group-') + (col.getColId() + '-' + key);
const groupsById = this.nonLeafsById;
- let groupNode = groupsById.get(id);
- if (groupNode !== undefined) {
- return groupNode; // already exists
- }
-
- groupNode = new RowNode(this.beans);
- groupNode.group = true;
- groupNode.parent = parent;
- groupNode.field = groupCol.field ?? null;
- groupNode.rowGroupColumn = col;
-
- groupNode.key = key;
- groupNode.id = id;
-
- groupNode.level = level;
- groupNode.leafGroup = isLeafLevel;
+ let node = groupsById.get(id);
+ let singleUse = true;
+
+ if (node) {
+ if (node.childrenAfterGroup !== null) {
+ // Already active — just ensure lazy expansion state.
+ node._expanded ??= null;
+ return node;
+ }
+ singleUse = false;
+ // Reused existing group node from a shotgun reset.
+ // Reset children but preserve selection and expansion state.
+ invalidateAllLeafChildren(node);
+ } else {
+ // Brand new group node.
+ node = new RowNode(this.beans);
+ node.group = true;
+ node.key = key;
+ node.id = id;
+ node.aggData = null;
+ groupsById.set(id, node);
+ }
+
+ const applyValuesToNode = (n: RowNode) => {
+ n.childrenAfterGroup = children;
+ n.childrenMapped = mapped;
+ n.parent = parent;
+ n.level = level;
+ n.rowGroupIndex = level;
+ n.leafGroup = isLeafLevel;
+ };
- groupNode.rowGroupIndex = level;
- groupNode.childrenAfterGroup = [];
- groupNode.childrenMapped = {};
+ // Shared setup for both reused and new nodes.
+ const children: RowNode[] = [];
+ const mapped = {};
+ applyValuesToNode(node);
+ node.field = groupCol.field ?? null;
+ node.rowGroupColumn = col;
+ node.groupValue = this.beans.valueSvc.getValue(col, leafNode, 'data');
+ // null triggers lazy default resolution in the expanded getter.
+ node._expanded ??= null;
- groupsById.set(id, groupNode);
+ if (singleUse) {
+ node.setAllChildrenCount(0);
+ node.updateHasChildren();
+ return node;
+ }
- groupNode.groupValue = leafNode && this.beans.valueSvc.getValue(col, leafNode, 'data');
+ const sibling = node.sibling;
+ if (sibling) {
+ applyValuesToNode(sibling);
+ }
- // why is this done here? we are not updating the children count as we go,
- // i suspect this is updated in the filter stage
- groupNode.setAllChildrenCount(0);
- groupNode.updateHasChildren();
+ node.dispatchRowEvent('hasChildrenChanged');
- return groupNode;
+ return node;
}
private getChildrenMappedKey(key: string, rowGroupColumn: AgColumn | null): string {
@@ -523,16 +583,6 @@ export class GroupStrategy extends BeanStub implements IRowGroupingStrategy {
return rowGroupColumn ? rowGroupColumn.getId() + '-' + key : key;
}
- private setExpandedInitialValue(pivotMode: boolean, groupNode: RowNode): void {
- // if pivoting the leaf group is never expanded as we do not show leaf rows
- if (pivotMode && groupNode.leafGroup) {
- groupNode.expanded = false;
- return;
- }
-
- groupNode.expanded = _getRowDefaultExpanded(this.beans, groupNode, groupNode.level);
- }
-
public onShowRowGroupColsSetChanged(): void {
const { rowModel, valueSvc } = this.beans;
for (const groupNode of this.nonLeafsById.values()) {
diff --git a/packages/ag-grid-enterprise/src/rowGrouping/rowGroupingUtils.ts b/packages/ag-grid-enterprise/src/rowGrouping/rowGroupingUtils.ts
index c3f726fa68b..cc2c51c44ee 100644
--- a/packages/ag-grid-enterprise/src/rowGrouping/rowGroupingUtils.ts
+++ b/packages/ag-grid-enterprise/src/rowGrouping/rowGroupingUtils.ts
@@ -34,7 +34,6 @@ export function setRowNodeGroup(rowNode: RowNode, beans: BeanCollection, group:
// if we used to be a group, and no longer, then close the node
if (rowNode.group && !group) {
- rowNode.expanded = false;
// Clear stale aggData when demoting from group to leaf.
const colModel = beans.colModel;
setAggData(rowNode, null, colModel);
diff --git a/packages/ag-grid-enterprise/src/rowHierarchy/baseExpansionService.ts b/packages/ag-grid-enterprise/src/rowHierarchy/baseExpansionService.ts
index fe7450534f4..e79df2a55fa 100644
--- a/packages/ag-grid-enterprise/src/rowHierarchy/baseExpansionService.ts
+++ b/packages/ag-grid-enterprise/src/rowHierarchy/baseExpansionService.ts
@@ -1,5 +1,5 @@
-import type { RowCtrl, RowGroupOpenedEvent, RowNode } from 'ag-grid-community';
-import { BeanStub, _createGlobalRowEvent, _setAriaExpanded } from 'ag-grid-community';
+import type { IsGroupOpenByDefaultParams, RowCtrl, RowGroupOpenedEvent, RowNode } from 'ag-grid-community';
+import { BeanStub, _addGridCommonParams, _createGlobalRowEvent, _setAriaExpanded } from 'ag-grid-community';
export abstract class BaseExpansionService extends BeanStub {
protected abstract dispatchExpandedEvent(event: RowGroupOpenedEvent, forceSync?: boolean): void;
@@ -28,7 +28,7 @@ export abstract class BaseExpansionService extends BeanStub {
return;
}
- rowNode.expanded = expanded;
+ rowNode._expanded = expanded;
rowNode.dispatchRowEvent('expandedChanged');
@@ -37,6 +37,27 @@ export abstract class BaseExpansionService extends BeanStub {
this.dispatchExpandedEvent(event, forceSync);
}
+ public defaultExpanded(rowNode: RowNode): boolean {
+ const beans = this.beans;
+ const gos = beans.gos;
+ const level = rowNode.level ?? 0;
+ // see AG-11476 isGroupOpenByDefault callback doesn't apply to master/detail grid
+ // We call isGroupOpenByDefault only for group nodes and not for master/detail leafs
+ const isGroupOpenByDefault = rowNode.group && gos.get('isGroupOpenByDefault');
+ if (!isGroupOpenByDefault) {
+ const groupDefaultExpanded = gos.get('groupDefaultExpanded');
+ return groupDefaultExpanded === -1 || level < groupDefaultExpanded;
+ }
+ const params = _addGridCommonParams(gos, {
+ rowNode,
+ field: rowNode.field!,
+ key: rowNode.key!,
+ level,
+ rowGroupColumn: rowNode.rowGroupColumn!,
+ });
+ return !!isGroupOpenByDefault(params);
+ }
+
public isExpandable(rowNode: RowNode): boolean {
if (rowNode.footer) {
return false;
diff --git a/packages/ag-grid-enterprise/src/rowHierarchy/clientSideExpansionService.ts b/packages/ag-grid-enterprise/src/rowHierarchy/clientSideExpansionService.ts
index 008205f68e3..6e59a84a033 100644
--- a/packages/ag-grid-enterprise/src/rowHierarchy/clientSideExpansionService.ts
+++ b/packages/ag-grid-enterprise/src/rowHierarchy/clientSideExpansionService.ts
@@ -35,7 +35,7 @@ export class ClientSideExpansionService
return;
}
- node.expanded = rowIdsToExpandSet.has(id);
+ node._expanded = rowIdsToExpandSet.has(id);
});
this.onGroupExpandedOrCollapsed();
}
@@ -62,6 +62,37 @@ export class ClientSideExpansionService
return this.getInternalExpansionState();
}
+ public isExpanded(rowNode: RowNode): boolean {
+ // Footer nodes use their own _expanded backing field directly (copied from group at creation time).
+ // This preserves the snapshot semantics from when expanded was a plain field.
+ if (rowNode.footer) {
+ return !!rowNode._expanded;
+ }
+ if (!(rowNode.group || rowNode.master) || (rowNode.leafGroup && this.beans.colModel.isPivotMode())) {
+ return false; // Not expandable, so always return false
+ }
+ let value = rowNode._expanded;
+ if (value === null) {
+ // Lazy resolution of the default expansion state via the enterprise expansion service.
+ value = this.defaultExpanded(rowNode) ?? false;
+ rowNode._expanded = value;
+ }
+ return !!value;
+ }
+
+ public resetExpansion(): void {
+ const { rowModel } = this.beans;
+
+ rowModel.forEachNode((node) => {
+ if (!node.group && !node.master) {
+ return;
+ }
+ node._expanded = null; // null triggers lazy default resolution in the expanded getter
+ });
+
+ this.onGroupExpandedOrCollapsed();
+ }
+
public expandAll(expand: boolean): void {
const { gos, rowModel, colModel, eventSvc } = this.beans;
const usingTreeData = gos.get('treeData');
@@ -73,7 +104,7 @@ export class ClientSideExpansionService
}
for (const rowNode of rowNodes) {
const actionRow = () => {
- rowNode.expanded = expand;
+ rowNode._expanded = expand;
recursiveExpandOrCollapse(rowNode.childrenAfterGroup);
};
diff --git a/packages/ag-grid-enterprise/src/rowHierarchy/flattenUtils.ts b/packages/ag-grid-enterprise/src/rowHierarchy/flattenUtils.ts
index 97e6e832ad5..635d6db9bb5 100644
--- a/packages/ag-grid-enterprise/src/rowHierarchy/flattenUtils.ts
+++ b/packages/ag-grid-enterprise/src/rowHierarchy/flattenUtils.ts
@@ -57,19 +57,30 @@ export function _shouldRowBeRendered(
isRemovedSingleChildrenGroup: boolean,
isRemovedLowestSingleChildrenGroup: boolean | undefined
): boolean {
- const isSkippedLeafNode = skipLeafNodes && !isParent;
+ if (skipLeafNodes && !isParent) {
+ return false;
+ }
+ if (isRemovedSingleChildrenGroup || isRemovedLowestSingleChildrenGroup) {
+ return false;
+ }
// hide open parents means when group is open, we don't show it. we also need to make sure the
// group is expandable in the first place (as leaf groups are not expandable if pivot mode is on).
// the UI will never allow expanding leaf groups, however the user might via the API (or menu option 'expand all row groups')
+ if (!details.hideOpenParents) {
+ return true;
+ }
+
+ if (rowNode.master || rowNode.level === -1) {
+ return true;
+ }
+
const neverAllowToExpand = skipLeafNodes && rowNode.leafGroup;
- const isHiddenOpenParent = details.hideOpenParents && rowNode.expanded && !rowNode.master && !neverAllowToExpand;
+ // rowNode.expanded evaluated LAST because it has side effects
+ if (!neverAllowToExpand && rowNode.expanded) {
+ return false;
+ }
- return (
- !isSkippedLeafNode &&
- !isHiddenOpenParent &&
- !isRemovedSingleChildrenGroup &&
- !isRemovedLowestSingleChildrenGroup
- );
+ return true;
}
diff --git a/packages/ag-grid-enterprise/src/rowHierarchy/rendering/groupCellRendererCtrl.ts b/packages/ag-grid-enterprise/src/rowHierarchy/rendering/groupCellRendererCtrl.ts
index 4a4f34f0c9a..a598125044d 100644
--- a/packages/ag-grid-enterprise/src/rowHierarchy/rendering/groupCellRendererCtrl.ts
+++ b/packages/ag-grid-enterprise/src/rowHierarchy/rendering/groupCellRendererCtrl.ts
@@ -208,11 +208,11 @@ export class GroupCellRendererCtrl extends BeanStub implements IGroupCellRendere
return;
}
- const expanded = this.displayedNode.expanded;
+ const expanded = !!this.displayedNode.expanded;
comp.setExpandedDisplayed(expanded);
comp.setContractedDisplayed(!expanded);
- _setAriaExpanded(eGridCell, !!this.displayedNode.expanded);
+ _setAriaExpanded(eGridCell, expanded);
};
const onExpandableChanged = () => {
diff --git a/packages/ag-grid-enterprise/src/rowHierarchy/rowHierarchyUtils.ts b/packages/ag-grid-enterprise/src/rowHierarchy/rowHierarchyUtils.ts
index b9b32ed24c8..bfd90cba121 100644
--- a/packages/ag-grid-enterprise/src/rowHierarchy/rowHierarchyUtils.ts
+++ b/packages/ag-grid-enterprise/src/rowHierarchy/rowHierarchyUtils.ts
@@ -1,9 +1,7 @@
import type {
Bean,
- BeanCollection,
GridOptions,
GridOptionsService,
- IRowNode,
NestedDataGetter,
RefreshModelParams,
RowNode,
@@ -60,29 +58,3 @@ export function _isHiddenParent(node: RowNode, ancestor: RowNode, gos: GridOptio
}
return currentNode === ancestor;
}
-
-export const _getRowDefaultExpanded = (
- beans: BeanCollection,
- rowNode: IRowNode,
- level: number,
- group = rowNode.group
-): boolean => {
- const gos = beans.gos;
- // see AG-11476 isGroupOpenByDefault callback doesn't apply to master/detail grid
- // We call isGroupOpenByDefault only for group nodes and not for master/detail leafs
- const isGroupOpenByDefault = group && gos.get('isGroupOpenByDefault');
- if (!isGroupOpenByDefault) {
- const groupDefaultExpanded = gos.get('groupDefaultExpanded');
- return groupDefaultExpanded === -1 || level < groupDefaultExpanded;
- }
- const params = {
- api: beans.gridApi,
- context: beans.gridOptions.context,
- rowNode,
- field: rowNode.field!,
- key: rowNode.key!,
- level,
- rowGroupColumn: rowNode.rowGroupColumn!,
- };
- return isGroupOpenByDefault(params) == true;
-};
diff --git a/packages/ag-grid-enterprise/src/rowHierarchy/stickyRowFeature.ts b/packages/ag-grid-enterprise/src/rowHierarchy/stickyRowFeature.ts
index 36a2b4be146..abb22d53e1b 100644
--- a/packages/ag-grid-enterprise/src/rowHierarchy/stickyRowFeature.ts
+++ b/packages/ag-grid-enterprise/src/rowHierarchy/stickyRowFeature.ts
@@ -174,7 +174,7 @@ export class StickyRowFeature extends BeanStub implements IStickyRowFeature {
return false;
}
const alreadySticking = newStickyRows.has(row);
- return !alreadySticking && row.expanded;
+ return !alreadySticking && !!row.expanded;
}
return false;
diff --git a/packages/ag-grid-enterprise/src/serverSideRowModel/blocks/blockUtils.ts b/packages/ag-grid-enterprise/src/serverSideRowModel/blocks/blockUtils.ts
index 7fa476a084e..678533008d2 100644
--- a/packages/ag-grid-enterprise/src/serverSideRowModel/blocks/blockUtils.ts
+++ b/packages/ag-grid-enterprise/src/serverSideRowModel/blocks/blockUtils.ts
@@ -404,11 +404,11 @@ export class BlockUtils extends BeanStub implements NamedBean {
public checkOpenByDefault(rowNode: RowNode): void {
const expanded = !!this.expansionSvc?.isNodeExpanded(rowNode);
- const oldExpanded = rowNode.expanded;
+ const oldExpanded = rowNode._expanded;
if (!!oldExpanded !== expanded) {
rowNode.setExpanded(expanded);
} else if (oldExpanded === undefined) {
- rowNode.expanded = expanded; // Initial state, don't fire event
+ rowNode._expanded = expanded; // Initial state, don't fire event
}
}
}
diff --git a/packages/ag-grid-enterprise/src/serverSideRowModel/services/serverSideExpansionService.ts b/packages/ag-grid-enterprise/src/serverSideRowModel/services/serverSideExpansionService.ts
index 9a63122de4e..c347d5e0c78 100644
--- a/packages/ag-grid-enterprise/src/serverSideRowModel/services/serverSideExpansionService.ts
+++ b/packages/ag-grid-enterprise/src/serverSideRowModel/services/serverSideExpansionService.ts
@@ -37,9 +37,14 @@ export class ServerSideExpansionService
this.storeFactory = beans.ssrmStoreFactory as StoreFactory;
}
+ private setStrategy(strategy: ExpandStrategy | ExpandAllStrategy): void {
+ this.destroyBean(this.strategy as any);
+ this.strategy = this.createManagedBean(strategy);
+ }
+
public postConstruct(): void {
const setDefaultExpand = () => {
- this.strategy = this.createManagedBean(new ExpandStrategy());
+ this.setStrategy(new ExpandStrategy());
};
this.addManagedEventListeners({
@@ -53,7 +58,7 @@ export class ServerSideExpansionService
// reset strategy if explicitly disabled, otherwise state is fine to remain until new
// select all value is set/removed
if (!p.currentValue) {
- this.strategy = this.createManagedBean(new ExpandStrategy());
+ this.setStrategy(new ExpandStrategy());
this.updateAllNodes();
this.dispatchStateUpdatedEvent();
}
@@ -67,9 +72,7 @@ export class ServerSideExpansionService
const isExpandAllStrategy = this.isExpandAllStrategy(this.strategy);
if (isExpandAllState !== isExpandAllStrategy) {
- this.strategy = isExpandAllState
- ? this.createManagedBean(new ExpandAllStrategy())
- : this.createManagedBean(new ExpandStrategy());
+ this.setStrategy(isExpandAllState ? new ExpandAllStrategy() : new ExpandStrategy());
}
this.strategy.setExpandedState(state as any); // cast to any, as we know the type is correct due to the previous assertion
this.dispatchStateUpdatedEvent();
@@ -89,6 +92,19 @@ export class ServerSideExpansionService
});
}
+ public isExpanded(rowNode: RowNode): boolean {
+ let value = rowNode._expanded;
+ if (value === null) {
+ value = this.defaultExpanded(rowNode);
+ rowNode._expanded = value;
+ }
+ // This could be returning undefined which is currently
+ // handled via coercion in SSRM. When fixing this to always
+ // return a boolean must validate that the undefined state is
+ // correctly handled.
+ return value as boolean;
+ }
+
public isNodeExpanded(node: RowNode): boolean {
return this.strategy.isRowExpanded(node);
}
@@ -100,12 +116,20 @@ export class ServerSideExpansionService
this.updateExpandedState(node);
}
+ public resetExpansion(): void {
+ this.setStrategy(new ExpandStrategy());
+ this.updateAllNodes();
+ this.dispatchStateUpdatedEvent();
+ }
+
public expandAll(expanded: boolean): void {
const ssrmExpandAllAffectsAllRows = this.beans.gos.get('ssrmExpandAllAffectsAllRows');
// if allowed, swap to expand all strategy
const shouldUseExpandAllStrategy = !this.isExpandAllStrategy(this.strategy) && ssrmExpandAllAffectsAllRows;
- this.strategy = shouldUseExpandAllStrategy ? new ExpandAllStrategy() : this.strategy;
+ if (shouldUseExpandAllStrategy) {
+ this.setStrategy(new ExpandAllStrategy());
+ }
this.strategy.expandAll(expanded);
this.updateAllNodes();
this.dispatchStateUpdatedEvent();
diff --git a/packages/ag-grid-enterprise/src/treeData/treeGroupStrategy.ts b/packages/ag-grid-enterprise/src/treeData/treeGroupStrategy.ts
index 238cec67dab..44c0c8702f2 100644
--- a/packages/ag-grid-enterprise/src/treeData/treeGroupStrategy.ts
+++ b/packages/ag-grid-enterprise/src/treeData/treeGroupStrategy.ts
@@ -9,7 +9,6 @@ import { BeanStub, RowNode, _removeFromArray, _warn } from 'ag-grid-community';
import { setRowNodeGroup } from '../rowGrouping/rowGroupingUtils';
import type { IRowGroupingStrategy } from '../rowHierarchy/rowHierarchyUtils';
-import { _getRowDefaultExpanded } from '../rowHierarchy/rowHierarchyUtils';
import { fieldGetter } from './fieldAccess';
// The approach used here avoids complex incremental updates by using linear passes and a final traversal.
@@ -20,7 +19,7 @@ import { fieldGetter } from './fieldAccess';
// This guarantees correct parent-child relationships without requiring sorting or post-processing.
//
// No new arrays are allocated for childrenAfterGroup — existing arrays are reused.
-// The treeNodeFlags field encodes temporary state, child counters, and expanded status.
+// The treeNodeFlags field encodes temporary state and child counters.
// The treeParent field tracks hierarchy changes and supports re-parenting (e.g., drag-and-drop).
// Setting a node treeParent to a desired node and then executing grouping without full reload will generate a valid tree.
//
@@ -34,9 +33,6 @@ const FLAG_CHANGED = 0x40000000;
/** Toggling this flag is used to mark a filler node as used or already processed */
const FLAG_MARKED_FILLER = 0x20000000;
-/** This is the only flag that stays in the node, to indicate that the expanded state was initialized */
-const FLAG_EXPANDED_INITIALIZED = 0x10000000;
-
/** Mask used to keep track of the number of children in a node */
const MASK_CHILDREN_LEN = 0x0fffffff; // This equates to 268,435,455 maximum children per parent, more than enough
@@ -219,8 +215,8 @@ export class TreeGroupStrategy extends BeanStub implements IRowGrou
}
if (parentChanged && oldParent) {
- if (oldParent.destroyed && maybeExpandFromRemovedParent(parent, oldParent)) {
- parentFlags |= FLAG_EXPANDED_INITIALIZED;
+ if (oldParent.destroyed) {
+ maybeExpandFromRemovedParent(parent, oldParent);
}
oldParent.treeNodeFlags |= FLAG_CHANGED;
}
@@ -360,7 +356,7 @@ export class TreeGroupStrategy extends BeanStub implements IRowGrou
const len = children?.length ?? 0;
let flags = row.treeNodeFlags;
- row.treeNodeFlags = flags & FLAG_EXPANDED_INITIALIZED;
+ row.treeNodeFlags = 0;
row.level = level;
// Update group state and children markers
@@ -376,21 +372,16 @@ export class TreeGroupStrategy extends BeanStub implements IRowGrou
const canBeExpanded = len !== 0 || row.master;
if (!canBeExpanded) {
- if (row.expanded) {
- row.expanded = false;
- }
- if ((flags & FLAG_EXPANDED_INITIALIZED) !== 0) {
- row.treeNodeFlags &= ~FLAG_EXPANDED_INITIALIZED;
- }
- } else if ((flags & FLAG_EXPANDED_INITIALIZED) === 0) {
- row.treeNodeFlags |= FLAG_EXPANDED_INITIALIZED;
- row.expanded = _getRowDefaultExpanded(this.beans, row, level); // Initialize the expanded state
+ row._expanded = undefined;
+ } else {
+ // null triggers lazy default resolution in the expanded getter
+ row._expanded ??= null;
}
if (collapsed && row.rowIndex !== null) {
row.clearRowTopAndRowIndex(); // Mark row hidden if collapsed
}
- collapsed ||= row.expanded === false;
+ collapsed ||= !row.expanded;
++level; // Increment level as it is passed down to children
flags &= FLAG_CHILDREN_CHANGED;
@@ -861,19 +852,16 @@ type DuplicatePathsMap = Map[]>;
/**
* If parent is a new filler node, copy the expanded flag from old removed parent.
- * Returns true if the expanded flag was copied.
*/
-const maybeExpandFromRemovedParent = (parent: RowNode, oldParent: RowNode): boolean => {
+const maybeExpandFromRemovedParent = (parent: RowNode, oldParent: RowNode): void => {
if (
- (oldParent.treeNodeFlags & FLAG_EXPANDED_INITIALIZED) !== 0 &&
- (parent.treeNodeFlags & FLAG_EXPANDED_INITIALIZED) === 0 &&
+ oldParent._expanded !== undefined &&
+ parent._expanded === undefined &&
parent.treeParent !== null &&
parent.sourceRowIndex < 0
) {
- parent.expanded = oldParent.expanded;
- return true;
+ parent._expanded = oldParent._expanded;
}
- return false;
};
const updateNodeKey = (node: RowNode, key: string): void => {
diff --git a/packages/ag-grid-enterprise/src/version.ts b/packages/ag-grid-enterprise/src/version.ts
index 353202c0311..63095b37ec2 100644
--- a/packages/ag-grid-enterprise/src/version.ts
+++ b/packages/ag-grid-enterprise/src/version.ts
@@ -1,2 +1,2 @@
// DO NOT UPDATE MANUALLY: Generated from script during build time
-export const VERSION = '35.1.0-beta.20260316.108';
+export const VERSION = '35.1.0-beta.20260317.135';
diff --git a/packages/ag-grid-react/package.json b/packages/ag-grid-react/package.json
index 5613bf37d3c..9b415d56318 100644
--- a/packages/ag-grid-react/package.json
+++ b/packages/ag-grid-react/package.json
@@ -1,6 +1,6 @@
{
"name": "ag-grid-react",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"description": "AG Grid React Component",
"main": "./dist/package/index.cjs.js",
"types": "./dist/types/src/index.d.ts",
@@ -31,7 +31,7 @@
"devDependencies": {
"@babel/runtime": "^7.27.1",
"prop-types": "^15.6.2",
- "ag-grid-community": "35.1.0-beta.20260316.108",
+ "ag-grid-community": "35.1.0-beta.20260317.135",
"@babel/plugin-proposal-throw-expressions": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
"@types/react": "~18.3.26",
@@ -44,7 +44,7 @@
},
"dependencies": {
"prop-types": "^15.8.1",
- "ag-grid-community": "35.1.0-beta.20260316.108"
+ "ag-grid-community": "35.1.0-beta.20260317.135"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
diff --git a/packages/ag-grid-vue3/package.json b/packages/ag-grid-vue3/package.json
index 357c12fb599..7c2ea3b95a9 100644
--- a/packages/ag-grid-vue3/package.json
+++ b/packages/ag-grid-vue3/package.json
@@ -1,7 +1,7 @@
{
"name": "ag-grid-vue3",
"description": "AG Grid Vue 3 Component",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"author": "Sean Landsman ",
"license": "MIT",
"files": [
@@ -44,7 +44,7 @@
"build-only:watch": "vite build --watch"
},
"dependencies": {
- "ag-grid-community": "35.1.0-beta.20260316.108"
+ "ag-grid-community": "35.1.0-beta.20260317.135"
},
"devDependencies": {
"vue": "^3.5.0",
diff --git a/plugins/ag-grid-generate-code-reference-files/package.json b/plugins/ag-grid-generate-code-reference-files/package.json
index 7c4cd65b021..c16a71faac6 100644
--- a/plugins/ag-grid-generate-code-reference-files/package.json
+++ b/plugins/ag-grid-generate-code-reference-files/package.json
@@ -1,6 +1,6 @@
{
"name": "ag-grid-generate-code-reference-files",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"private": true,
"dependencies": {
"ag-shared": "0.0.1",
diff --git a/plugins/ag-grid-generate-example-files/package.json b/plugins/ag-grid-generate-example-files/package.json
index 922c0c95128..7f5aa6d9910 100644
--- a/plugins/ag-grid-generate-example-files/package.json
+++ b/plugins/ag-grid-generate-example-files/package.json
@@ -1,10 +1,10 @@
{
"name": "ag-grid-generate-example-files",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"private": true,
"dependencies": {
"ag-shared": "0.0.1",
- "ag-grid-community": "35.1.0-beta.20260316.108",
+ "ag-grid-community": "35.1.0-beta.20260317.135",
"glob": "8.0.3",
"typescript": "~5.4.5",
"cheerio": "^1.0.0",
diff --git a/plugins/ag-grid-task-autogen/package.json b/plugins/ag-grid-task-autogen/package.json
index e66597440d7..eda247f9e81 100644
--- a/plugins/ag-grid-task-autogen/package.json
+++ b/plugins/ag-grid-task-autogen/package.json
@@ -1,6 +1,6 @@
{
"name": "ag-grid-task-autogen",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"private": true,
"dependencies": {
"@nx/devkit": "20.3.1",
diff --git a/testing/accessibility/package.json b/testing/accessibility/package.json
index d7d11a7ec0e..3de5a2db2a3 100644
--- a/testing/accessibility/package.json
+++ b/testing/accessibility/package.json
@@ -1,6 +1,6 @@
{
"name": "ag-grid-accessibility",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"scripts": {
"download-examples": "curl --retry 5 -retry-all-errors https://grid-staging.ag-grid.com/debug/all-examples.json > ./all-examples.json",
"download-examples-local": "curl https://localhost:4610/debug/all-examples.json > ./all-examples.json",
@@ -18,11 +18,11 @@
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
- "ag-grid-angular": "35.1.0-beta.20260316.108",
- "ag-grid-community": "35.1.0-beta.20260316.108",
- "ag-grid-enterprise": "35.1.0-beta.20260316.108",
- "ag-charts-community": "13.1.0-beta.20260316",
- "ag-charts-enterprise": "13.1.0-beta.20260316",
+ "ag-grid-angular": "35.1.0-beta.20260317.135",
+ "ag-grid-community": "35.1.0-beta.20260317.135",
+ "ag-grid-enterprise": "35.1.0-beta.20260317.135",
+ "ag-charts-community": "13.1.0-beta.20260317",
+ "ag-charts-enterprise": "13.1.0-beta.20260317",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
diff --git a/testing/angular-tests/package.json b/testing/angular-tests/package.json
index eb0f36b9f42..c9cbaae421b 100644
--- a/testing/angular-tests/package.json
+++ b/testing/angular-tests/package.json
@@ -1,6 +1,6 @@
{
"name": "ag-grid-angular-tests",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"private": true,
"scripts": {
"test:e2e": "jest --no-cache"
@@ -11,8 +11,8 @@
"@angular/core": "^21.0.0",
"@angular/platform-browser": "^21.0.0",
"@angular/platform-browser-dynamic": "^21.0.0",
- "ag-grid-angular": "35.1.0-beta.20260316.108",
- "ag-grid-community": "35.1.0-beta.20260316.108",
+ "ag-grid-angular": "35.1.0-beta.20260317.135",
+ "ag-grid-community": "35.1.0-beta.20260317.135",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
diff --git a/testing/behavioural/package.json b/testing/behavioural/package.json
index 020a72b1876..cd4c6009fbe 100644
--- a/testing/behavioural/package.json
+++ b/testing/behavioural/package.json
@@ -1,6 +1,6 @@
{
"name": "ag-behavioural-testing",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"private": true,
"description": "Behavioural unit testing for ag-Grid",
"dependencies": {
@@ -8,9 +8,9 @@
},
"type": "module",
"devDependencies": {
- "ag-grid-community": "35.1.0-beta.20260316.108",
- "ag-grid-enterprise": "35.1.0-beta.20260316.108",
- "ag-grid-react": "35.1.0-beta.20260316.108",
+ "ag-grid-community": "35.1.0-beta.20260317.135",
+ "ag-grid-enterprise": "35.1.0-beta.20260317.135",
+ "ag-grid-react": "35.1.0-beta.20260317.135",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@testing-library/dom": "^10.4.0",
diff --git a/testing/behavioural/src/cell-editing/benchmarks/getvalue.bench.ts b/testing/behavioural/src/cell-editing/benchmarks/getvalue.bench.ts
new file mode 100644
index 00000000000..c5d177d685c
--- /dev/null
+++ b/testing/behavioural/src/cell-editing/benchmarks/getvalue.bench.ts
@@ -0,0 +1,62 @@
+import { bench, describe } from 'vitest';
+
+import type { GridApi, IRowNode } from 'ag-grid-community';
+import { ClientSideRowModelModule, RowApiModule } from 'ag-grid-community';
+
+import { SimplePRNG, TestGridsManager } from '../../test-utils';
+
+interface IData {
+ name: string;
+ id: string;
+}
+
+function buildRandomData(numberOfRows: number): IData[] {
+ const prng = new SimplePRNG(0x12345678);
+ const result = new Array(numberOfRows);
+ for (let i = 0; i < numberOfRows; i++) {
+ result[i] = { name: prng.nextString(10), id: i.toString() };
+ }
+ return result;
+}
+
+describe('getValue profiling', () => {
+ const rowCount = 2000;
+
+ const gridsManager = new TestGridsManager({
+ benchmark: true,
+ modules: [ClientSideRowModelModule, RowApiModule],
+ });
+
+ const baseRowData = buildRandomData(rowCount);
+ const api: GridApi = gridsManager.createGrid('G', {
+ columnDefs: [{ field: 'name' }],
+ rowData: baseRowData,
+ getRowId: ({ data }) => data.id,
+ });
+
+ const rowNodes: IRowNode[] = [];
+ api.forEachNode((n) => rowNodes.push(n));
+
+ bench(`getDataValue`, () => {
+ for (let i = 0; i < rowCount; ++i) {
+ rowNodes[i].getDataValue('name');
+ }
+ });
+
+ bench(`getCellValue`, () => {
+ for (let i = 0; i < rowCount; ++i) {
+ api.getCellValue({ rowNode: rowNodes[i], colKey: 'name', useFormatter: false });
+ }
+ });
+
+ bench(`direct data.name `, () => {
+ let sum = 0;
+ for (let i = 0; i < rowCount; ++i) {
+ const val = (rowNodes[i] as any).data.name;
+ if (val) {
+ sum++;
+ }
+ }
+ return sum as any;
+ });
+});
diff --git a/testing/behavioural/src/drag-n-drop/grouping/managed-row-group-drag-edit-basic.test.ts b/testing/behavioural/src/drag-n-drop/grouping/managed-row-group-drag-edit-basic.test.ts
index ff578733817..ed213eb369b 100644
--- a/testing/behavioural/src/drag-n-drop/grouping/managed-row-group-drag-edit-basic.test.ts
+++ b/testing/behavioural/src/drag-n-drop/grouping/managed-row-group-drag-edit-basic.test.ts
@@ -90,13 +90,14 @@ describe('drag refreshAfterGroupEdit multi-step interactions', () => {
`;
const assertIntermediateStep = async (expectedParentId: string, snapshot: string, label: string) => {
- await asyncSetTimeout(0);
- const rowDragMoveEvents = dispatcher.rowDragMoveEvents;
- expect(rowDragMoveEvents.some((event) => event.rowsDrop?.newParent?.id === expectedParentId)).toBe(true);
- const latestRowsDrop = rowDragMoveEvents[rowDragMoveEvents.length - 1]?.rowsDrop;
- expect(latestRowsDrop?.allowed).toBe(true);
- expect(latestRowsDrop?.moved).toBe(true);
await waitFor(async () => {
+ const rowDragMoveEvents = dispatcher.rowDragMoveEvents;
+ expect(rowDragMoveEvents.some((event) => event.rowsDrop?.newParent?.id === expectedParentId)).toBe(
+ true
+ );
+ const latestRowsDrop = rowDragMoveEvents[rowDragMoveEvents.length - 1]?.rowsDrop;
+ expect(latestRowsDrop?.allowed).toBe(true);
+ expect(latestRowsDrop?.moved).toBe(true);
const intermediateRows = new GridRows(api, label);
await intermediateRows.check(snapshot);
const draggedRowElement = getRowHtmlElement(api, '2');
@@ -109,10 +110,8 @@ describe('drag refreshAfterGroupEdit multi-step interactions', () => {
await dispatcher.start('2');
await waitFor(() => expect(dispatcher.getDragGhostLabel()).toBe('A2'));
await dispatcher.move('3', { yOffsetPercent: 0.4 });
- await asyncSetTimeout(15);
await assertIntermediateStep('row-group-group-B', intermediateHoverLeaf, 'after hover over group B leaf');
await dispatcher.move('row-group-group-C', { center: true });
- await asyncSetTimeout(8);
await assertIntermediateStep('row-group-group-C', intermediateHoverLeaf, 'after hover over group C group node');
await dispatcher.move('6', { yOffsetPercent: 0.9 });
await dispatcher.finish();
diff --git a/testing/behavioural/src/grid-state/grid-state.test.ts b/testing/behavioural/src/grid-state/grid-state.test.ts
index 08d8626058f..739f072bb90 100644
--- a/testing/behavioural/src/grid-state/grid-state.test.ts
+++ b/testing/behavioural/src/grid-state/grid-state.test.ts
@@ -1,7 +1,7 @@
-import type { GridState } from 'ag-grid-community';
+import type { GridState, IServerSideDatasource, IServerSideGetRowsParams } from 'ag-grid-community';
import { AllEnterpriseModule } from 'ag-grid-enterprise';
-import { TestGridsManager, asyncSetTimeout } from '../test-utils';
+import { TestGridsManager, asyncSetTimeout, waitForNoLoadingRows } from '../test-utils';
describe('StateService - Grid State Management', () => {
const gridsManager = new TestGridsManager({
@@ -257,26 +257,30 @@ describe('StateService - Grid State Management', () => {
expect(api.getState().rowSelection).toEqual(['0', '2']);
});
- test('should capture row group expansion state - TODO FIX', async () => {
+ test('CSRM: getState captures rowGroupExpansion, setState restores it', async () => {
const columnDefs = [
{ field: 'id', hide: false },
{ field: 'sport', hide: false, rowGroup: true },
{ field: 'name', hide: false },
{ field: 'age', hide: false },
];
- const api = gridsManager.createGrid('myGrid', {
+ const api = gridsManager.createGrid('source', {
columnDefs,
rowData: defaultRowData,
+ getRowId: ({ data }) => data.id,
});
+ // Initially all groups collapsed
expect(api.getState().rowGroupExpansion).toEqual({
collapsedRowGroupIds: [],
expandedRowGroupIds: [],
});
+ expect(api.getState().ssrmRowGroupExpansion).toBeUndefined();
api.expandAll();
- expect(api.getState().rowGroupExpansion).toEqual({
+ const savedState = api.getState();
+ expect(savedState.rowGroupExpansion).toEqual({
collapsedRowGroupIds: [],
expandedRowGroupIds: [
'row-group-sport-Football',
@@ -286,6 +290,199 @@ describe('StateService - Grid State Management', () => {
'row-group-sport-Swimming',
],
});
+ expect(savedState.ssrmRowGroupExpansion).toBeUndefined();
+
+ // Restore into a fresh grid
+ const api2 = gridsManager.createGrid('target', {
+ columnDefs,
+ rowData: defaultRowData,
+ getRowId: ({ data }) => data.id,
+ initialState: savedState,
+ });
+
+ expect(api2.getState().rowGroupExpansion).toEqual(savedState.rowGroupExpansion);
+ });
+
+ test('SSRM: getState captures rowGroupExpansion, setState restores it', async () => {
+ const datasource: IServerSideDatasource = {
+ getRows({ request, success }: IServerSideGetRowsParams) {
+ if (request.groupKeys.length === 0) {
+ success({
+ rowData: [
+ { id: 'ie', country: 'Ireland' },
+ { id: 'fr', country: 'France' },
+ ],
+ });
+ } else {
+ success({
+ rowData: [{ id: `${request.groupKeys[0]}-leaf`, country: request.groupKeys[0], medals: 5 }],
+ });
+ }
+ },
+ };
+
+ const api = gridsManager.createGrid('ssrmSource', {
+ columnDefs: [{ field: 'country', rowGroup: true, hide: true }, { field: 'medals' }],
+ rowModelType: 'serverSide',
+ serverSideDatasource: datasource,
+ getRowId: ({ data }) => data.id,
+ });
+
+ await waitForNoLoadingRows(api);
+
+ // Without ssrmExpandAllAffectsAllRows, SSRM uses rowGroupExpansion (same as CSRM)
+ expect(api.getState().rowGroupExpansion).toEqual({ expandedRowGroupIds: [], collapsedRowGroupIds: [] });
+ expect(api.getState().ssrmRowGroupExpansion).toBeUndefined();
+
+ // Expand Ireland
+ api.setRowNodeExpanded(api.getRowNode('ie')!, true);
+ await waitForNoLoadingRows(api);
+
+ const savedState = api.getState();
+ expect(savedState.rowGroupExpansion).toEqual({
+ expandedRowGroupIds: ['ie'],
+ collapsedRowGroupIds: [],
+ });
+ expect(savedState.ssrmRowGroupExpansion).toBeUndefined();
+
+ // Collapse Ireland
+ api.setRowNodeExpanded(api.getRowNode('ie')!, false);
+ expect(api.getRowNode('ie')!.expanded).toBe(false);
+
+ // Restore the saved state
+ api.setState(savedState);
+ await waitForNoLoadingRows(api);
+
+ expect(api.getRowNode('ie')!.expanded).toBe(true);
+ expect(api.getState().rowGroupExpansion).toEqual(savedState.rowGroupExpansion);
+ });
+
+ test('SSRM expandAll strategy: getState captures RowGroupBulkExpansionState, setState restores it', async () => {
+ const datasource: IServerSideDatasource = {
+ getRows({ request, success }: IServerSideGetRowsParams) {
+ if (request.groupKeys.length === 0) {
+ success({
+ rowData: [
+ { id: 'ie', key: 'Ireland', country: 'Ireland', group: true },
+ { id: 'fr', key: 'France', country: 'France', group: true },
+ ],
+ });
+ } else {
+ success({
+ rowData: [{ id: `${request.groupKeys[0]}-leaf`, country: request.groupKeys[0], medals: 1 }],
+ });
+ }
+ },
+ };
+
+ const gridOpts = {
+ columnDefs: [{ field: 'country', rowGroup: true, hide: true }, { field: 'medals' }],
+ rowModelType: 'serverSide' as const,
+ serverSideDatasource: datasource,
+ getRowId: ({ data }: any) => data.id,
+ ssrmExpandAllAffectsAllRows: true,
+ };
+
+ const api = gridsManager.createGrid('ssrmBulkSource', gridOpts);
+ await waitForNoLoadingRows(api);
+
+ // Expand all — switches to ExpandAllStrategy
+ api.expandAll();
+ await waitForNoLoadingRows(api);
+
+ // Collapse France individually — recorded as an exception in ExpandAllStrategy
+ api.setRowNodeExpanded(api.getRowNode('fr')!, false);
+ await asyncSetTimeout(0); // allow debounced state update to flush
+
+ const savedState = api.getState();
+ expect(savedState.ssrmRowGroupExpansion).toEqual({
+ expandAll: true,
+ invertedRowGroupIds: ['fr'],
+ });
+ expect(savedState.rowGroupExpansion).toBeUndefined();
+
+ // Collapse all to reset state
+ api.collapseAll();
+ expect(api.getRowNode('ie')!.expanded).toBe(false);
+ expect(api.getRowNode('fr')!.expanded).toBe(false);
+
+ // Restore saved state — previously broken (ssrmRowGroupExpansion was ignored)
+ api.setState(savedState);
+ await waitForNoLoadingRows(api);
+
+ // Ireland should be expanded (expandAll: true), France collapsed (in invertedRowGroupIds)
+ expect(api.getRowNode('ie')!.expanded).toBe(true);
+ expect(api.getRowNode('fr')!.expanded).toBe(false);
+ });
+
+ test('SSRM expandAll strategy: initialState with ssrmRowGroupExpansion restores on grid creation', async () => {
+ // Tests the gridInitializing path: ssrmRowGroupExpansion must be checked (not just
+ // rowGroupExpansion) when deciding whether to call setRowGroupExpansionState.
+ const datasource: IServerSideDatasource = {
+ getRows({ request, success }: IServerSideGetRowsParams) {
+ if (request.groupKeys.length === 0) {
+ success({
+ rowData: [
+ { id: 'ie', country: 'Ireland' },
+ { id: 'fr', country: 'France' },
+ ],
+ });
+ } else {
+ success({
+ rowData: [{ id: `${request.groupKeys[0]}-leaf`, country: request.groupKeys[0], medals: 1 }],
+ });
+ }
+ },
+ };
+
+ const gridOpts = {
+ columnDefs: [{ field: 'country', rowGroup: true, hide: true }, { field: 'medals' }],
+ rowModelType: 'serverSide' as const,
+ serverSideDatasource: datasource,
+ getRowId: ({ data }: any) => data.id,
+ ssrmExpandAllAffectsAllRows: true,
+ initialState: {
+ ssrmRowGroupExpansion: { expandAll: true, invertedRowGroupIds: ['fr'] },
+ },
+ };
+
+ const api = gridsManager.createGrid('ssrmBulkInit', gridOpts);
+ await waitForNoLoadingRows(api);
+
+ // Ireland should be expanded (expandAll: true), France collapsed (in invertedRowGroupIds)
+ expect(api.getRowNode('ie')!.expanded).toBe(true);
+ expect(api.getRowNode('fr')!.expanded).toBe(false);
+ });
+
+ test('CSRM: getState reflects new node IDs after group column change via setGridOption', async () => {
+ // columnRowGroupChanged fires while changeEventsDispatching=true (before CSRM re-groups),
+ // so expansion state must be refreshed on newColumnsLoaded instead.
+ const api = gridsManager.createGrid('csrmRegroup', {
+ columnDefs: [
+ { field: 'sport', rowGroup: true, hide: true },
+ { field: 'name', rowGroup: false, hide: true },
+ { field: 'age' },
+ ],
+ rowData: defaultRowData,
+ getRowId: ({ data }) => data.id,
+ });
+
+ // expandAll uses the synchronous onGroupExpandedOrCollapsed path
+ api.expandAll();
+ await asyncSetTimeout(0);
+ expect(api.getState().rowGroupExpansion?.expandedRowGroupIds).toContain('row-group-sport-Football');
+
+ // Switch grouping to name — all sport-based IDs are gone
+ api.setGridOption('columnDefs', [
+ { field: 'sport', rowGroup: false, hide: true },
+ { field: 'name', rowGroup: true, hide: true },
+ { field: 'age' },
+ ]);
+ await asyncSetTimeout(0);
+
+ // Cached state must reflect the new name-based IDs, not stale sport-based IDs
+ const expansion = api.getState().rowGroupExpansion;
+ expect(expansion?.expandedRowGroupIds).not.toContain('row-group-sport-Football');
});
test('should capture pagination state', async () => {
diff --git a/testing/behavioural/src/grouping-data/grouping-change-column.test.ts b/testing/behavioural/src/grouping-data/grouping-change-column.test.ts
index b78bfd3ddfb..f0c5fbf3cc5 100644
--- a/testing/behavioural/src/grouping-data/grouping-change-column.test.ts
+++ b/testing/behavioural/src/grouping-data/grouping-change-column.test.ts
@@ -1,12 +1,12 @@
import type { GridOptions } from 'ag-grid-community';
-import { ClientSideRowModelModule } from 'ag-grid-community';
-import { RowGroupingModule } from 'ag-grid-enterprise';
+import { ClientSideRowModelModule, GridStateModule, RowSelectionModule } from 'ag-grid-community';
+import { PivotModule, RowGroupingModule } from 'ag-grid-enterprise';
-import { GridRows, TestGridsManager } from '../test-utils';
+import { GridRows, TestGridsManager, asyncSetTimeout } from '../test-utils';
describe('ag-grid grouping simple data', () => {
const gridsManager = new TestGridsManager({
- modules: [ClientSideRowModelModule, RowGroupingModule],
+ modules: [ClientSideRowModelModule, GridStateModule, RowGroupingModule],
});
beforeEach(() => {
@@ -56,4 +56,1254 @@ describe('ag-grid grouping simple data', () => {
· └── LEAF id:0 1:"bob"
`);
});
+
+ test('expanding groups then adding a deeper group column preserves expansion state', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020 },
+ { id: '2', country: 'Ireland', year: 2021 },
+ { id: '3', country: 'France', year: 2020 },
+ ];
+
+ const api = gridsManager.createGrid('myGrid', {
+ columnDefs: [{ field: 'country', rowGroup: true, hide: true }, { field: 'year' }],
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland')!, true, false, true);
+
+ await new GridRows(api, 'Ireland expanded').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ LEAF_GROUP id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├── LEAF id:1 country:"Ireland" year:2020
+ │ └── LEAF id:2 country:"Ireland" year:2021
+ └─┬ LEAF_GROUP collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ // Flush the debounced state update (rowExpansionStateChanged is debounced with 0ms)
+ await asyncSetTimeout(0);
+
+ // Expansion state debounce has settled — Ireland is in the expanded set
+ expect(api.getState().rowGroupExpansion).toEqual({
+ expandedRowGroupIds: ['row-group-country-Ireland'],
+ collapsedRowGroupIds: [],
+ });
+
+ // Add year as a second grouping column
+ api.setGridOption('columnDefs', [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: true, hide: true },
+ ]);
+
+ // Ireland should remain expanded; new year sub-groups follow groupDefaultExpanded (collapsed by default)
+ await new GridRows(api, 'after adding year as group column').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ // Flush the debounced state update
+ await asyncSetTimeout(0);
+
+ // Ireland's ID is unchanged so it remains in the expanded set after the column change
+ expect(api.getState().rowGroupExpansion).toEqual({
+ expandedRowGroupIds: ['row-group-country-Ireland'],
+ collapsedRowGroupIds: [],
+ });
+ });
+
+ test('expanding groups then removing the deepest group column preserves expansion state', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020 },
+ { id: '2', country: 'Ireland', year: 2021 },
+ { id: '3', country: 'France', year: 2020 },
+ ];
+
+ const api = gridsManager.createGrid('myGrid', {
+ columnDefs: [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: true, hide: true },
+ ],
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland')!, true, false, true);
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland-year-2020')!, true, false, true);
+
+ await new GridRows(api, 'Ireland and Ireland/2020 expanded').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ // Flush the debounced state update
+ await asyncSetTimeout(0);
+
+ // Expansion state debounce has settled — both expanded nodes are in the set
+ expect(api.getState().rowGroupExpansion).toEqual({
+ expandedRowGroupIds: ['row-group-country-Ireland', 'row-group-country-Ireland-year-2020'],
+ collapsedRowGroupIds: [],
+ });
+
+ // Remove year from grouping - only country remains
+ api.setGridOption('columnDefs', [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: false },
+ ]);
+
+ // Ireland should remain expanded; France should remain collapsed
+ await new GridRows(api, 'after removing year group column').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ LEAF_GROUP id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├── LEAF id:1 country:"Ireland" year:2020
+ │ └── LEAF id:2 country:"Ireland" year:2021
+ └─┬ LEAF_GROUP collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ // Flush the debounced state update
+ await asyncSetTimeout(0);
+
+ // Year sub-group nodes are gone — only Ireland remains in the expanded set
+ expect(api.getState().rowGroupExpansion).toEqual({
+ expandedRowGroupIds: ['row-group-country-Ireland'],
+ collapsedRowGroupIds: [],
+ });
+ });
+
+ test('removing the top group column resets all expansion to default', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020 },
+ { id: '2', country: 'Ireland', year: 2021 },
+ { id: '3', country: 'France', year: 2020 },
+ ];
+
+ const api = gridsManager.createGrid('myGrid', {
+ columnDefs: [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: true, hide: true },
+ ],
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland')!, true, false, true);
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland-year-2020')!, true, false, true);
+
+ await new GridRows(api, 'Ireland and Ireland/2020 expanded').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ // Flush the debounced state update
+ await asyncSetTimeout(0);
+
+ // Expansion state debounce has settled — both expanded nodes are in the set
+ expect(api.getState().rowGroupExpansion).toEqual({
+ expandedRowGroupIds: ['row-group-country-Ireland', 'row-group-country-Ireland-year-2020'],
+ collapsedRowGroupIds: [],
+ });
+
+ // Remove country (top level) from grouping — only year remains
+ api.setGridOption('columnDefs', [
+ { field: 'country', rowGroup: false },
+ { field: 'year', rowGroup: true, hide: true },
+ ]);
+
+ // All year-level IDs change (old: row-group-country-Ireland-year-2020, new: row-group-year-2020)
+ // so no saved IDs match — all groups fall back to groupDefaultExpanded (collapsed)
+ await new GridRows(api, 'after removing top group column').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ LEAF_GROUP collapsed id:row-group-year-2020 ag-Grid-AutoColumn:2020
+ │ ├── LEAF hidden id:1 country:"Ireland" year:2020
+ │ └── LEAF hidden id:3 country:"France" year:2020
+ └─┬ LEAF_GROUP collapsed id:row-group-year-2021 ag-Grid-AutoColumn:2021
+ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ `);
+
+ // Flush the debounced state update
+ await asyncSetTimeout(0);
+
+ // All year-level IDs changed so none matched — state resets to empty
+ expect(api.getState().rowGroupExpansion).toEqual({
+ expandedRowGroupIds: [],
+ collapsedRowGroupIds: [],
+ });
+ });
+
+ test('removing a middle group column does not preserve deeper level expansion state', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020, sport: 'Football' },
+ { id: '2', country: 'Ireland', year: 2020, sport: 'Rugby' },
+ { id: '3', country: 'Ireland', year: 2021, sport: 'Football' },
+ { id: '4', country: 'France', year: 2020, sport: 'Football' },
+ ];
+
+ const api = gridsManager.createGrid('myGrid', {
+ columnDefs: [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: true, hide: true },
+ { field: 'sport', rowGroup: true, hide: true },
+ ],
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ // Expand Ireland → 2020 → Football (all three levels)
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland')!, true, false, true);
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland-year-2020')!, true, false, true);
+ api.setRowNodeExpanded(
+ api.getRowNode('row-group-country-Ireland-year-2020-sport-Football')!,
+ true,
+ false,
+ true
+ );
+
+ await new GridRows(api, 'three levels expanded').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ filler id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ ├─┬ LEAF_GROUP id:row-group-country-Ireland-year-2020-sport-Football ag-Grid-AutoColumn:"Football"
+ │ │ │ └── LEAF id:1 country:"Ireland" year:2020 sport:"Football"
+ │ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2020-sport-Rugby ag-Grid-AutoColumn:"Rugby"
+ │ │ · └── LEAF hidden id:2 country:"Ireland" year:2020 sport:"Rugby"
+ │ └─┬ filler collapsed id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2021-sport-Football ag-Grid-AutoColumn:"Football"
+ │ · · └── LEAF hidden id:3 country:"Ireland" year:2021 sport:"Football"
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ filler collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-year-2020-sport-Football ag-Grid-AutoColumn:"Football"
+ · · · └── LEAF hidden id:4 country:"France" year:2020 sport:"Football"
+ `);
+
+ // Remove the middle group column (year)
+ api.setGridOption('columnDefs', [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: false },
+ { field: 'sport', rowGroup: true, hide: true },
+ ]);
+
+ // Country-level expansion IS preserved (Ireland expanded, France collapsed).
+ // Sport-level expansion is NOT preserved because IDs encode the full ancestor path:
+ // before: row-group-country-Ireland-year-2020-sport-Football
+ // after: row-group-country-Ireland-sport-Football
+ // The IDs no longer match, so sport groups fall back to groupDefaultExpanded (collapsed).
+ await new GridRows(api, 'after removing middle group column').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-sport-Football ag-Grid-AutoColumn:"Football"
+ │ │ ├── LEAF hidden id:1 country:"Ireland" year:2020 sport:"Football"
+ │ │ └── LEAF hidden id:3 country:"Ireland" year:2021 sport:"Football"
+ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-sport-Rugby ag-Grid-AutoColumn:"Rugby"
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2020 sport:"Rugby"
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-sport-Football ag-Grid-AutoColumn:"Football"
+ · · └── LEAF hidden id:4 country:"France" year:2020 sport:"Football"
+ `);
+ });
+
+ test('swapping 2nd and 3rd group columns resets expansion at those levels', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020, sport: 'Football' },
+ { id: '2', country: 'Ireland', year: 2020, sport: 'Rugby' },
+ { id: '3', country: 'Ireland', year: 2021, sport: 'Football' },
+ { id: '4', country: 'France', year: 2020, sport: 'Football' },
+ ];
+
+ const api = gridsManager.createGrid('myGrid', {
+ columnDefs: [
+ { field: 'country', rowGroupIndex: 0, hide: true },
+ { field: 'year', rowGroupIndex: 1, hide: true },
+ { field: 'sport', rowGroupIndex: 2, hide: true },
+ ],
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ // Expand Ireland → 2020 → Football
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland')!, true, false, true);
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland-year-2020')!, true, false, true);
+ api.setRowNodeExpanded(
+ api.getRowNode('row-group-country-Ireland-year-2020-sport-Football')!,
+ true,
+ false,
+ true
+ );
+
+ await new GridRows(api, 'three levels expanded').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ filler id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ ├─┬ LEAF_GROUP id:row-group-country-Ireland-year-2020-sport-Football ag-Grid-AutoColumn:"Football"
+ │ │ │ └── LEAF id:1 country:"Ireland" year:2020 sport:"Football"
+ │ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2020-sport-Rugby ag-Grid-AutoColumn:"Rugby"
+ │ │ · └── LEAF hidden id:2 country:"Ireland" year:2020 sport:"Rugby"
+ │ └─┬ filler collapsed id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2021-sport-Football ag-Grid-AutoColumn:"Football"
+ │ · · └── LEAF hidden id:3 country:"Ireland" year:2021 sport:"Football"
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ filler collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-year-2020-sport-Football ag-Grid-AutoColumn:"Football"
+ · · · └── LEAF hidden id:4 country:"France" year:2020 sport:"Football"
+ `);
+
+ // Swap year and sport column order
+ api.setGridOption('columnDefs', [
+ { field: 'country', rowGroupIndex: 0, hide: true },
+ { field: 'year', rowGroupIndex: 2, hide: true },
+ { field: 'sport', rowGroupIndex: 1, hide: true },
+ ]);
+
+ // Country-level expansion is preserved (Ireland expanded, France collapsed).
+ // Levels 2 and 3 reset because IDs change when column order changes:
+ // old: row-group-country-Ireland-year-2020-sport-Football
+ // new: row-group-country-Ireland-sport-Football-year-2020
+ await new GridRows(api, 'after swapping year and sport').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ filler collapsed id:row-group-country-Ireland-sport-Football ag-Grid-AutoColumn:"Football"
+ │ │ ├─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-sport-Football-year-2020 ag-Grid-AutoColumn:2020
+ │ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020 sport:"Football"
+ │ │ └─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-sport-Football-year-2021 ag-Grid-AutoColumn:2021
+ │ │ · └── LEAF hidden id:3 country:"Ireland" year:2021 sport:"Football"
+ │ └─┬ filler collapsed id:row-group-country-Ireland-sport-Rugby ag-Grid-AutoColumn:"Rugby"
+ │ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-sport-Rugby-year-2020 ag-Grid-AutoColumn:2020
+ │ · · └── LEAF hidden id:2 country:"Ireland" year:2020 sport:"Rugby"
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ filler collapsed hidden id:row-group-country-France-sport-Football ag-Grid-AutoColumn:"Football"
+ · · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-sport-Football-year-2020 ag-Grid-AutoColumn:2020
+ · · · └── LEAF hidden id:4 country:"France" year:2020 sport:"Football"
+ `);
+ });
+
+ test('swapping group columns with isGroupOpenByDefault expands via callback at reset levels', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020, sport: 'Football' },
+ { id: '2', country: 'Ireland', year: 2020, sport: 'Rugby' },
+ { id: '3', country: 'Ireland', year: 2021, sport: 'Football' },
+ { id: '4', country: 'France', year: 2020, sport: 'Football' },
+ ];
+
+ const api = gridsManager.createGrid('myGrid', {
+ columnDefs: [
+ { field: 'country', rowGroupIndex: 0, hide: true },
+ { field: 'year', rowGroupIndex: 1, hide: true },
+ { field: 'sport', rowGroupIndex: 2, hide: true },
+ ],
+ rowData,
+ getRowId: (params) => params.data.id,
+ // Expand sport="Football" groups regardless of level
+ isGroupOpenByDefault: (params) => params.field === 'sport' && params.key === 'Football',
+ });
+
+ // Initially: sport=Football groups are expanded, others collapsed.
+ // Manually expand Ireland and Ireland/2020 as well.
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland')!, true, false, true);
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland-year-2020')!, true, false, true);
+
+ await new GridRows(api, 'initial state').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ filler id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ ├─┬ LEAF_GROUP id:row-group-country-Ireland-year-2020-sport-Football ag-Grid-AutoColumn:"Football"
+ │ │ │ └── LEAF id:1 country:"Ireland" year:2020 sport:"Football"
+ │ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2020-sport-Rugby ag-Grid-AutoColumn:"Rugby"
+ │ │ · └── LEAF hidden id:2 country:"Ireland" year:2020 sport:"Rugby"
+ │ └─┬ filler collapsed id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └─┬ LEAF_GROUP hidden id:row-group-country-Ireland-year-2021-sport-Football ag-Grid-AutoColumn:"Football"
+ │ · · └── LEAF hidden id:3 country:"Ireland" year:2021 sport:"Football"
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ filler collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └─┬ LEAF_GROUP hidden id:row-group-country-France-year-2020-sport-Football ag-Grid-AutoColumn:"Football"
+ · · · └── LEAF hidden id:4 country:"France" year:2020 sport:"Football"
+ `);
+
+ // Swap year and sport column order
+ api.setGridOption('columnDefs', [
+ { field: 'country', rowGroupIndex: 0, hide: true },
+ { field: 'year', rowGroupIndex: 2, hide: true },
+ { field: 'sport', rowGroupIndex: 1, hide: true },
+ ]);
+
+ // Country-level expansion is preserved (Ireland expanded, France collapsed).
+ // Levels 2 and 3 IDs change, so saved IDs don't match — but isGroupOpenByDefault
+ // fires for sport="Football" nodes, expanding them at their new level.
+ await new GridRows(api, 'after swap with isGroupOpenByDefault').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ filler id:row-group-country-Ireland-sport-Football ag-Grid-AutoColumn:"Football"
+ │ │ ├─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-sport-Football-year-2020 ag-Grid-AutoColumn:2020
+ │ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020 sport:"Football"
+ │ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-sport-Football-year-2021 ag-Grid-AutoColumn:2021
+ │ │ · └── LEAF hidden id:3 country:"Ireland" year:2021 sport:"Football"
+ │ └─┬ filler collapsed id:row-group-country-Ireland-sport-Rugby ag-Grid-AutoColumn:"Rugby"
+ │ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-sport-Rugby-year-2020 ag-Grid-AutoColumn:2020
+ │ · · └── LEAF hidden id:2 country:"Ireland" year:2020 sport:"Rugby"
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ filler hidden id:row-group-country-France-sport-Football ag-Grid-AutoColumn:"Football"
+ · · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-sport-Football-year-2020 ag-Grid-AutoColumn:2020
+ · · · └── LEAF hidden id:4 country:"France" year:2020 sport:"Football"
+ `);
+ });
+
+ test('groupDefaultExpanded -1 with added group column keeps all groups expanded', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020 },
+ { id: '2', country: 'Ireland', year: 2021 },
+ { id: '3', country: 'France', year: 2020 },
+ ];
+
+ const api = gridsManager.createGrid('myGrid', {
+ columnDefs: [{ field: 'country', rowGroup: true, hide: true }, { field: 'year' }],
+ groupDefaultExpanded: -1,
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ await new GridRows(api, 'initial - all countries expanded').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ LEAF_GROUP id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├── LEAF id:1 country:"Ireland" year:2020
+ │ └── LEAF id:2 country:"Ireland" year:2021
+ └─┬ LEAF_GROUP id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └── LEAF id:3 country:"France" year:2020
+ `);
+
+ // Add year as a second grouping column
+ api.setGridOption('columnDefs', [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: true, hide: true },
+ ]);
+
+ // All country groups should remain expanded; new year sub-groups should also be expanded
+ // because groupDefaultExpanded: -1 applies to newly created group nodes
+ await new GridRows(api, 'after adding year as group column').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF id:2 country:"Ireland" year:2021
+ └─┬ filler id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF id:3 country:"France" year:2020
+ `);
+ });
+
+ test('adding a group column above existing resets all expansion to default', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020 },
+ { id: '2', country: 'Ireland', year: 2021 },
+ { id: '3', country: 'France', year: 2020 },
+ ];
+
+ const api = gridsManager.createGrid('myGrid', {
+ columnDefs: [{ field: 'country' }, { field: 'year', rowGroupIndex: 0, hide: true }],
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ api.setRowNodeExpanded(api.getRowNode('row-group-year-2020')!, true, false, true);
+
+ await new GridRows(api, 'year 2020 expanded').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ LEAF_GROUP id:row-group-year-2020 ag-Grid-AutoColumn:2020
+ │ ├── LEAF id:1 country:"Ireland" year:2020
+ │ └── LEAF id:3 country:"France" year:2020
+ └─┬ LEAF_GROUP collapsed id:row-group-year-2021 ag-Grid-AutoColumn:2021
+ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ `);
+
+ // Add country as a new top-level group column above year
+ api.setGridOption('columnDefs', [
+ { field: 'country', rowGroupIndex: 0, hide: true },
+ { field: 'year', rowGroupIndex: 1, hide: true },
+ ]);
+
+ // Year-level IDs change (old: row-group-year-2020, new: row-group-country-Ireland-year-2020)
+ // so no saved IDs match — all groups fall back to groupDefaultExpanded (collapsed)
+ await new GridRows(api, 'after adding country above year').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler collapsed id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+ });
+
+ test('adding a group column in the middle does not preserve deeper level expansion state', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020, sport: 'Football' },
+ { id: '2', country: 'Ireland', year: 2020, sport: 'Rugby' },
+ { id: '3', country: 'Ireland', year: 2021, sport: 'Football' },
+ { id: '4', country: 'France', year: 2020, sport: 'Football' },
+ ];
+
+ const api = gridsManager.createGrid('myGrid', {
+ columnDefs: [
+ { field: 'country', rowGroupIndex: 0, hide: true },
+ { field: 'year' },
+ { field: 'sport', rowGroupIndex: 1, hide: true },
+ ],
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland')!, true, false, true);
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland-sport-Football')!, true, false, true);
+
+ await new GridRows(api, 'Ireland and Ireland/Football expanded').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP id:row-group-country-Ireland-sport-Football ag-Grid-AutoColumn:"Football"
+ │ │ ├── LEAF id:1 country:"Ireland" year:2020 sport:"Football"
+ │ │ └── LEAF id:3 country:"Ireland" year:2021 sport:"Football"
+ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-sport-Rugby ag-Grid-AutoColumn:"Rugby"
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2020 sport:"Rugby"
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-sport-Football ag-Grid-AutoColumn:"Football"
+ · · └── LEAF hidden id:4 country:"France" year:2020 sport:"Football"
+ `);
+
+ // Add year as a middle group column between country and sport
+ api.setGridOption('columnDefs', [
+ { field: 'country', rowGroupIndex: 0, hide: true },
+ { field: 'year', rowGroupIndex: 1, hide: true },
+ { field: 'sport', rowGroupIndex: 2, hide: true },
+ ]);
+
+ // Country-level expansion is preserved (Ireland expanded, France collapsed).
+ // Sport-level IDs change (old: row-group-country-Ireland-sport-Football,
+ // new: row-group-country-Ireland-year-2020-sport-Football) so sport expansion resets.
+ await new GridRows(api, 'after adding year in the middle').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ filler collapsed id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ ├─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2020-sport-Football ag-Grid-AutoColumn:"Football"
+ │ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020 sport:"Football"
+ │ │ └─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2020-sport-Rugby ag-Grid-AutoColumn:"Rugby"
+ │ │ · └── LEAF hidden id:2 country:"Ireland" year:2020 sport:"Rugby"
+ │ └─┬ filler collapsed id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2021-sport-Football ag-Grid-AutoColumn:"Football"
+ │ · · └── LEAF hidden id:3 country:"Ireland" year:2021 sport:"Football"
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ filler collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-year-2020-sport-Football ag-Grid-AutoColumn:"Football"
+ · · · └── LEAF hidden id:4 country:"France" year:2020 sport:"Football"
+ `);
+ });
+
+ test('groupDefaultExpanded -1 with explicit collapse preserves collapsed state on column change', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020 },
+ { id: '2', country: 'Ireland', year: 2021 },
+ { id: '3', country: 'France', year: 2020 },
+ ];
+
+ const api = gridsManager.createGrid('myGrid', {
+ columnDefs: [{ field: 'country', rowGroup: true, hide: true }, { field: 'year' }],
+ groupDefaultExpanded: -1,
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ await new GridRows(api, 'initial - all expanded').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ LEAF_GROUP id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├── LEAF id:1 country:"Ireland" year:2020
+ │ └── LEAF id:2 country:"Ireland" year:2021
+ └─┬ LEAF_GROUP id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └── LEAF id:3 country:"France" year:2020
+ `);
+
+ // Explicitly collapse France
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-France')!, false, false, true);
+
+ await new GridRows(api, 'France collapsed').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ LEAF_GROUP id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├── LEAF id:1 country:"Ireland" year:2020
+ │ └── LEAF id:2 country:"Ireland" year:2021
+ └─┬ LEAF_GROUP collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ // Add year as a second grouping column
+ api.setGridOption('columnDefs', [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: true, hide: true },
+ ]);
+
+ // Ireland stays expanded (saved expanded ID match). France stays collapsed (saved collapsed ID match).
+ // New year sub-groups follow groupDefaultExpanded: -1 (expanded).
+ await new GridRows(api, 'after adding year — France stays collapsed').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF id:2 country:"Ireland" year:2021
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+ });
+
+ describe('resetRowGroupExpansion', () => {
+ test('resets all groups back to collapsed (default groupDefaultExpanded: 0)', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020 },
+ { id: '2', country: 'Ireland', year: 2021 },
+ { id: '3', country: 'France', year: 2020 },
+ ];
+
+ const api = gridsManager.createGrid('myGrid', {
+ columnDefs: [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: true, hide: true },
+ ],
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ // Expand Ireland and Ireland/2020
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland')!, true, false, true);
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland-year-2020')!, true, false, true);
+
+ await new GridRows(api, 'Ireland and Ireland/2020 expanded').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ // Flush the debounced state update
+ await asyncSetTimeout(0);
+
+ // Expansion state debounce has settled — both expanded nodes are in the set
+ expect(api.getState().rowGroupExpansion).toEqual({
+ expandedRowGroupIds: ['row-group-country-Ireland', 'row-group-country-Ireland-year-2020'],
+ collapsedRowGroupIds: [],
+ });
+
+ api.resetRowGroupExpansion();
+
+ // All groups should be collapsed (groupDefaultExpanded defaults to 0)
+ await new GridRows(api, 'after resetRowGroupExpansion').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler collapsed id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ // Flush the debounced state update
+ await asyncSetTimeout(0);
+
+ // Expansion state debounce settled — reset returns state to empty
+ expect(api.getState().rowGroupExpansion).toEqual({
+ expandedRowGroupIds: [],
+ collapsedRowGroupIds: [],
+ });
+ });
+
+ test('resets to groupDefaultExpanded: -1 (all expanded)', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020 },
+ { id: '2', country: 'Ireland', year: 2021 },
+ { id: '3', country: 'France', year: 2020 },
+ ];
+
+ const api = gridsManager.createGrid('myGrid', {
+ columnDefs: [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: true, hide: true },
+ ],
+ groupDefaultExpanded: -1,
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ // Collapse France and Ireland/2020
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-France')!, false, false, true);
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland-year-2020')!, false, false, true);
+
+ await new GridRows(api, 'France collapsed, Ireland/2020 collapsed').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF id:2 country:"Ireland" year:2021
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ api.resetRowGroupExpansion();
+
+ // All groups should be expanded (groupDefaultExpanded: -1)
+ await new GridRows(api, 'after resetRowGroupExpansion — all expanded').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF id:2 country:"Ireland" year:2021
+ └─┬ filler id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF id:3 country:"France" year:2020
+ `);
+ });
+
+ test('resets to groupDefaultExpanded: 1 (first level expanded only)', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020 },
+ { id: '2', country: 'Ireland', year: 2021 },
+ { id: '3', country: 'France', year: 2020 },
+ ];
+
+ const api = gridsManager.createGrid('myGrid', {
+ columnDefs: [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: true, hide: true },
+ ],
+ groupDefaultExpanded: 1,
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ // Collapse Ireland and expand Ireland/2020 (overrides both levels)
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland')!, false, false, true);
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland-year-2020')!, true, false, true);
+
+ await new GridRows(api, 'Ireland collapsed, Ireland/2020 expanded (override)').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler collapsed id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP hidden id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ api.resetRowGroupExpansion();
+
+ // Level 0 expanded, level 1 collapsed (groupDefaultExpanded: 1)
+ await new GridRows(api, 'after resetRowGroupExpansion — level 0 expanded, level 1 collapsed').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+ });
+
+ test('re-evaluates isGroupOpenByDefault callback', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020 },
+ { id: '2', country: 'Ireland', year: 2021 },
+ { id: '3', country: 'France', year: 2020 },
+ ];
+
+ const api = gridsManager.createGrid('myGrid', {
+ columnDefs: [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: true, hide: true },
+ ],
+ rowData,
+ getRowId: (params) => params.data.id,
+ // Only Ireland should be expanded by default
+ isGroupOpenByDefault: (params) => params.key === 'Ireland',
+ });
+
+ await new GridRows(api, 'initial — Ireland expanded via callback').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ // Override: collapse Ireland, expand France and Ireland/2020
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland')!, false, false, true);
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-France')!, true, false, true);
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland-year-2020')!, true, false, true);
+
+ await new GridRows(api, 'user overrides applied').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler collapsed id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP hidden id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ api.resetRowGroupExpansion();
+
+ // Callback re-evaluated: Ireland expanded, France collapsed (back to initial state)
+ await new GridRows(api, 'after resetRowGroupExpansion — callback re-evaluated').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+ });
+
+ test('resets preserved expansion state after column change', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020 },
+ { id: '2', country: 'Ireland', year: 2021 },
+ { id: '3', country: 'France', year: 2020 },
+ ];
+
+ const api = gridsManager.createGrid('myGrid', {
+ columnDefs: [{ field: 'country', rowGroup: true, hide: true }, { field: 'year' }],
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ // Expand Ireland
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland')!, true, false, true);
+
+ // Add year as group column — Ireland stays expanded (preserved)
+ api.setGridOption('columnDefs', [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: true, hide: true },
+ ]);
+
+ // columnRowGroupChanged updates state synchronously — Ireland's ID is unchanged so it remains expanded
+ expect(api.getState().rowGroupExpansion).toEqual({
+ expandedRowGroupIds: ['row-group-country-Ireland'],
+ collapsedRowGroupIds: [],
+ });
+
+ await new GridRows(api, 'Ireland preserved expanded after column change').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ api.resetRowGroupExpansion();
+
+ // Reset clears the preserved expansion — all groups return to default (collapsed)
+ await new GridRows(api, 'after resetRowGroupExpansion — all collapsed').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler collapsed id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ // Flush the debounced state update
+ await asyncSetTimeout(0);
+
+ // Expansion state debounce settled — reset returns state to empty
+ expect(api.getState().rowGroupExpansion).toEqual({
+ expandedRowGroupIds: [],
+ collapsedRowGroupIds: [],
+ });
+ });
+ });
+
+ describe('pivot mode', () => {
+ const pivotGridsManager = new TestGridsManager({
+ modules: [ClientSideRowModelModule, GridStateModule, RowGroupingModule, PivotModule],
+ });
+
+ beforeEach(() => {
+ pivotGridsManager.reset();
+ });
+
+ afterEach(() => {
+ pivotGridsManager.reset();
+ });
+
+ test('pivot mode without active pivot — add deeper group column preserves non-leaf expansion, leaf groups forced collapsed', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020, sport: 'Football' },
+ { id: '2', country: 'Ireland', year: 2021, sport: 'Rugby' },
+ { id: '3', country: 'France', year: 2020, sport: 'Football' },
+ ];
+
+ const api = pivotGridsManager.createGrid('myGrid', {
+ columnDefs: [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: true, hide: true },
+ ],
+ pivotMode: true,
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ // Country is non-leaf (filler), year is LEAF_GROUP → forced collapsed in pivot mode
+ await new GridRows(api, 'initial — all collapsed in pivot mode').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler collapsed id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ // Expand Ireland (non-leaf group)
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland')!, true, false, true);
+
+ await new GridRows(api, 'Ireland expanded, year leaf groups forced collapsed').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ // Add sport as 3rd group column
+ api.setGridOption('columnDefs', [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: true, hide: true },
+ { field: 'sport', rowGroup: true, hide: true },
+ ]);
+
+ // Ireland preserved expanded (non-leaf, snapshot match).
+ // Year now non-leaf → stays collapsed (snapshot match).
+ // Sport is new leaf group → forced collapsed in pivot mode.
+ await new GridRows(api, 'after adding sport — leaf groups forced collapsed').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ filler collapsed id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2020-sport-Football ag-Grid-AutoColumn:"Football"
+ │ │ · └── LEAF hidden id:1 country:"Ireland" year:2020 sport:"Football"
+ │ └─┬ filler collapsed id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2021-sport-Rugby ag-Grid-AutoColumn:"Rugby"
+ │ · · └── LEAF hidden id:2 country:"Ireland" year:2021 sport:"Rugby"
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ filler collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-year-2020-sport-Football ag-Grid-AutoColumn:"Football"
+ · · · └── LEAF hidden id:3 country:"France" year:2020 sport:"Football"
+ `);
+ });
+
+ test('pivot mode with active pivot — add deeper group column preserves non-leaf expansion, leaf groups forced collapsed', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020, sport: 'Football', medals: 2 },
+ { id: '2', country: 'Ireland', year: 2021, sport: 'Rugby', medals: 3 },
+ { id: '3', country: 'France', year: 2020, sport: 'Football', medals: 4 },
+ ];
+
+ const gridRowsOptions = {
+ forcedColumns: ['ag-Grid-AutoColumn', 'pivot_sport_Football_medals', 'pivot_sport_Rugby_medals'],
+ };
+
+ const api = pivotGridsManager.createGrid('myGrid', {
+ columnDefs: [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'sport', pivot: true, hide: true },
+ { field: 'medals', aggFunc: 'sum', hide: true },
+ { field: 'year' },
+ ],
+ pivotMode: true,
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ // Country is LEAF_GROUP → forced collapsed in pivot mode
+ // Secondary pivot columns created from sport values
+ await new GridRows(api, 'initial — leaf groups forced collapsed with pivot', gridRowsOptions).check(`
+ ROOT id:ROOT_NODE_ID pivot_sport_Football_medals:6 pivot_sport_Rugby_medals:3
+ ├─┬ LEAF_GROUP collapsed id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland" pivot_sport_Football_medals:2 pivot_sport_Rugby_medals:3
+ │ ├── LEAF hidden id:1 pivot_sport_Football_medals:2 pivot_sport_Rugby_medals:2
+ │ └── LEAF hidden id:2 pivot_sport_Football_medals:3 pivot_sport_Rugby_medals:3
+ └─┬ LEAF_GROUP collapsed id:row-group-country-France ag-Grid-AutoColumn:"France" pivot_sport_Football_medals:4 pivot_sport_Rugby_medals:null
+ · └── LEAF hidden id:3 pivot_sport_Football_medals:4 pivot_sport_Rugby_medals:4
+ `);
+
+ // Expand Ireland (leaf group in pivot — user override)
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland')!, true, false, true);
+
+ // Add year as 2nd group column — country becomes non-leaf
+ api.setGridOption('columnDefs', [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: true, hide: true },
+ { field: 'sport', pivot: true, hide: true },
+ { field: 'medals', aggFunc: 'sum', hide: true },
+ ]);
+
+ // Ireland preserved expanded (non-leaf now, snapshot match).
+ // Year groups are new leaf groups → forced collapsed in pivot mode.
+ await new GridRows(api, 'after adding year — non-leaf preserved, leaf collapsed', gridRowsOptions).check(`
+ ROOT id:ROOT_NODE_ID pivot_sport_Football_medals:6 pivot_sport_Rugby_medals:3
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland" pivot_sport_Football_medals:2 pivot_sport_Rugby_medals:3
+ │ ├─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020 pivot_sport_Football_medals:2 pivot_sport_Rugby_medals:null
+ │ │ └── LEAF hidden id:1 pivot_sport_Football_medals:2 pivot_sport_Rugby_medals:2
+ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021 pivot_sport_Football_medals:null pivot_sport_Rugby_medals:3
+ │ · └── LEAF hidden id:2 pivot_sport_Football_medals:3 pivot_sport_Rugby_medals:3
+ └─┬ filler collapsed id:row-group-country-France ag-Grid-AutoColumn:"France" pivot_sport_Football_medals:4 pivot_sport_Rugby_medals:null
+ · └─┬ LEAF_GROUP collapsed hidden id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020 pivot_sport_Football_medals:4 pivot_sport_Rugby_medals:null
+ · · └── LEAF hidden id:3 pivot_sport_Football_medals:4 pivot_sport_Rugby_medals:4
+ `);
+ });
+
+ test('resetRowGroupExpansion in pivot mode forces leaf groups collapsed, re-evaluates non-leaf defaults', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020, sport: 'Football' },
+ { id: '2', country: 'Ireland', year: 2021, sport: 'Rugby' },
+ { id: '3', country: 'France', year: 2020, sport: 'Football' },
+ ];
+
+ const api = pivotGridsManager.createGrid('myGrid', {
+ columnDefs: [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: true, hide: true },
+ ],
+ pivotMode: true,
+ groupDefaultExpanded: -1,
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ // groupDefaultExpanded: -1 expands non-leaf groups; leaf groups forced collapsed in pivot
+ await new GridRows(api, 'initial — non-leaf expanded, leaf forced collapsed').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ // Collapse Ireland, expand year leaf groups (user overrides)
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland')!, false, false, true);
+ api.setRowNodeExpanded(api.getRowNode('row-group-country-Ireland-year-2020')!, true, false, true);
+
+ // Leaf groups in pivot mode are never expanded (getter returns false),
+ // so setRowNodeExpanded(true) has no visible effect on leaf groups.
+ await new GridRows(api, 'user overrides: Ireland collapsed, year 2020 expanded').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler collapsed id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed hidden id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+
+ api.resetRowGroupExpansion();
+
+ // Non-leaf groups re-evaluated against groupDefaultExpanded: -1 → expanded.
+ // Leaf groups forced collapsed in pivot mode regardless.
+ await new GridRows(api, 'after reset — non-leaf expanded, leaf forced collapsed').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF hidden id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP collapsed id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF hidden id:2 country:"Ireland" year:2021
+ └─┬ filler id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP collapsed id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF hidden id:3 country:"France" year:2020
+ `);
+ });
+ });
+
+ describe('selection during group column changes', () => {
+ const gridsManagerWithSelection = new TestGridsManager({
+ modules: [ClientSideRowModelModule, GridStateModule, RowSelectionModule, RowGroupingModule],
+ });
+
+ beforeEach(() => {
+ gridsManagerWithSelection.reset();
+ });
+
+ afterEach(() => {
+ gridsManagerWithSelection.reset();
+ });
+
+ test('group and leaf selections are preserved when adding a deeper group column', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020 },
+ { id: '2', country: 'Ireland', year: 2021 },
+ { id: '3', country: 'France', year: 2020 },
+ { id: '4', country: 'France', year: 2021 },
+ ];
+
+ const api = gridsManagerWithSelection.createGrid('myGrid', {
+ columnDefs: [{ field: 'country', rowGroup: true, hide: true }, { field: 'year' }],
+ rowSelection: { mode: 'multiRow' },
+ groupDefaultExpanded: -1,
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ // Select a group node and some leaf nodes
+ api.setNodesSelected({
+ nodes: [api.getRowNode('row-group-country-Ireland')!, api.getRowNode('1')!, api.getRowNode('3')!],
+ newValue: true,
+ });
+
+ await new GridRows(api, 'initial selection').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ LEAF_GROUP selected id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├── LEAF selected id:1 country:"Ireland" year:2020
+ │ └── LEAF id:2 country:"Ireland" year:2021
+ └─┬ LEAF_GROUP id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · ├── LEAF selected id:3 country:"France" year:2020
+ · └── LEAF id:4 country:"France" year:2021
+ `);
+
+ // Add year as a second grouping column — triggers shotgunResetEverything
+ api.setGridOption('columnDefs', [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: true, hide: true },
+ ]);
+
+ // Leaf selections (id:1, id:3) and the Ireland group selection are preserved
+ // because group nodes with the same ID are reused during the regroup.
+ await new GridRows(api, 'after adding year group column').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler selected id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF selected id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF id:2 country:"Ireland" year:2021
+ └─┬ filler id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · ├─┬ LEAF_GROUP id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · │ └── LEAF selected id:3 country:"France" year:2020
+ · └─┬ LEAF_GROUP id:row-group-country-France-year-2021 ag-Grid-AutoColumn:2021
+ · · └── LEAF id:4 country:"France" year:2021
+ `);
+ });
+
+ test('group selections are preserved for reused nodes but lost for destroyed nodes when removing a group column', async () => {
+ const rowData = [
+ { id: '1', country: 'Ireland', year: 2020 },
+ { id: '2', country: 'Ireland', year: 2021 },
+ { id: '3', country: 'France', year: 2020 },
+ ];
+
+ const api = gridsManagerWithSelection.createGrid('myGrid', {
+ columnDefs: [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: true, hide: true },
+ ],
+ rowSelection: { mode: 'multiRow' },
+ groupDefaultExpanded: -1,
+ rowData,
+ getRowId: (params) => params.data.id,
+ });
+
+ // Select group nodes and leaf nodes
+ api.setNodesSelected({
+ nodes: [
+ api.getRowNode('row-group-country-Ireland')!,
+ api.getRowNode('row-group-country-Ireland-year-2020')!,
+ api.getRowNode('1')!,
+ api.getRowNode('3')!,
+ ],
+ newValue: true,
+ });
+
+ await new GridRows(api, 'initial selection').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ filler selected id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├─┬ LEAF_GROUP selected id:row-group-country-Ireland-year-2020 ag-Grid-AutoColumn:2020
+ │ │ └── LEAF selected id:1 country:"Ireland" year:2020
+ │ └─┬ LEAF_GROUP id:row-group-country-Ireland-year-2021 ag-Grid-AutoColumn:2021
+ │ · └── LEAF id:2 country:"Ireland" year:2021
+ └─┬ filler id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └─┬ LEAF_GROUP id:row-group-country-France-year-2020 ag-Grid-AutoColumn:2020
+ · · └── LEAF selected id:3 country:"France" year:2020
+ `);
+
+ // Remove year from grouping
+ api.setGridOption('columnDefs', [
+ { field: 'country', rowGroup: true, hide: true },
+ { field: 'year', rowGroup: false },
+ ]);
+
+ // Leaf selections preserved. Ireland group selection preserved (same ID reused).
+ // Year sub-group selections lost (those nodes are destroyed).
+ await new GridRows(api, 'after removing year group column').check(`
+ ROOT id:ROOT_NODE_ID
+ ├─┬ LEAF_GROUP selected id:row-group-country-Ireland ag-Grid-AutoColumn:"Ireland"
+ │ ├── LEAF selected id:1 country:"Ireland" year:2020
+ │ └── LEAF id:2 country:"Ireland" year:2021
+ └─┬ LEAF_GROUP id:row-group-country-France ag-Grid-AutoColumn:"France"
+ · └── LEAF selected id:3 country:"France" year:2020
+ `);
+ });
+ });
});
diff --git a/testing/behavioural/src/row-data/row-node-expanded.test.ts b/testing/behavioural/src/row-data/row-node-expanded.test.ts
new file mode 100644
index 00000000000..3b739643c50
--- /dev/null
+++ b/testing/behavioural/src/row-data/row-node-expanded.test.ts
@@ -0,0 +1,46 @@
+import type { RowNode } from 'ag-grid-community';
+import { ClientSideRowModelModule } from 'ag-grid-community';
+
+import { GridRows, TestGridsManager } from '../test-utils';
+
+describe('RowNode.expanded getter without expansionSvc', () => {
+ const gridsManager = new TestGridsManager({
+ modules: [ClientSideRowModelModule],
+ });
+
+ afterEach(() => {
+ gridsManager.reset();
+ });
+
+ test('root node expanded returns true without expansionSvc', async () => {
+ const api = gridsManager.createGrid('G', {
+ columnDefs: [{ field: 'name' }],
+ rowData: [{ name: 'Alice' }, { name: 'Bob' }],
+ });
+
+ const gridRows = new GridRows(api, 'root node expanded');
+ const rootNode = gridRows.rootRowNode!;
+
+ expect(rootNode).toBeDefined();
+ expect(rootNode.id).toBe('ROOT_NODE_ID');
+ expect(rootNode.level).toBe(-1);
+ expect(rootNode._expanded).toBe(true);
+ expect(rootNode.expanded).toBe(true);
+ });
+
+ test('leaf nodes expanded returns false without expansionSvc', async () => {
+ const api = gridsManager.createGrid('G', {
+ columnDefs: [{ field: 'name' }],
+ rowData: [{ name: 'Alice' }],
+ getRowId: ({ data }) => data.name,
+ });
+
+ const leafNode = api.getRowNode('Alice')!;
+
+ expect(leafNode).toBeDefined();
+ expect(leafNode.group).toBe(false);
+ expect((leafNode as RowNode)._expanded).toBeUndefined();
+ // Without expansionSvc, the getter falls back to `_expanded as boolean` which is undefined (falsy)
+ expect(leafNode.expanded).toEqual(false);
+ });
+});
diff --git a/testing/behavioural/src/services/server-side-expansion-service.test.ts b/testing/behavioural/src/services/server-side-expansion-service.test.ts
index 1bdc8131652..c8f3612606c 100644
--- a/testing/behavioural/src/services/server-side-expansion-service.test.ts
+++ b/testing/behavioural/src/services/server-side-expansion-service.test.ts
@@ -30,7 +30,9 @@ describe('ServerSideExpansionService', () => {
expansionService['serverSideRowModel'] = beans.serverSideRowModel as any;
expansionService['eventSvc'] = beans.eventSvc;
expansionService['beans'] = beans;
+ expansionService['createBean'] = (bean: any) => bean;
expansionService['createManagedBean'] = (bean: any) => bean;
+ expansionService['destroyBean'] = () => undefined;
expansionService['addManagedEventListeners'] = () => [];
expansionService['addManagedPropertyListener'] = () => () => null;
expansionService.postConstruct();
diff --git a/testing/behavioural/src/test-utils/gridRows/gridRows.ts b/testing/behavioural/src/test-utils/gridRows/gridRows.ts
index abbcab8b766..2c023ae50f8 100644
--- a/testing/behavioural/src/test-utils/gridRows/gridRows.ts
+++ b/testing/behavioural/src/test-utils/gridRows/gridRows.ts
@@ -265,6 +265,12 @@ export class GridRows {
attempt.loadErrors();
lastError = attempt.#tryCheck(diagramSnapshot);
if (!lastError) {
+ if (i > 0) {
+ console.error(
+ `GridRows flaky check detected for "${this.label}" — passed only after retrying with delays. ` +
+ `Add \`await asyncSetTimeout(N)\` before this check to avoid intermittent failures.`
+ );
+ }
return this;
}
if (i < retryDelays.length) {
@@ -273,13 +279,6 @@ export class GridRows {
}
}
- if (attempt !== this) {
- console.error(
- `GridRows flaky check detected for "${this.label}" — passed only after retrying with delays. ` +
- `Add \`await asyncSetTimeout(N)\` before this check to avoid intermittent failures.`
- );
- }
-
addDiagramToError(lastError, attempt.makeDiagram(false), this.label);
Error.captureStackTrace(lastError, this.check);
throw lastError;
diff --git a/testing/behavioural/src/test-utils/gridRows/rows-validation/gridRowsValidator.ts b/testing/behavioural/src/test-utils/gridRows/rows-validation/gridRowsValidator.ts
index 61e95617f07..e74698e1464 100644
--- a/testing/behavioural/src/test-utils/gridRows/rows-validation/gridRowsValidator.ts
+++ b/testing/behavioural/src/test-utils/gridRows/rows-validation/gridRowsValidator.ts
@@ -54,7 +54,6 @@ export class GridRowsValidator {
const rowErrors = this.errors.get(root);
rowErrors.expectValueEqual('id', root.id, csrm ? 'ROOT_NODE_ID' : undefined);
rowErrors.expectValueEqual('level', root.level, -1);
- rowErrors.expectValueEqual('expanded', root.expanded, undefined);
rowErrors.add(!!root.key && 'Root node has key ' + root.key);
rowErrors.add(root.destroyed && 'Root node is destroyed');
rowErrors.add(root.rowIndex !== null && 'Root node has rowIndex ' + root.rowIndex);
diff --git a/testing/behavioural/src/version.ts b/testing/behavioural/src/version.ts
index 353202c0311..63095b37ec2 100644
--- a/testing/behavioural/src/version.ts
+++ b/testing/behavioural/src/version.ts
@@ -1,2 +1,2 @@
// DO NOT UPDATE MANUALLY: Generated from script during build time
-export const VERSION = '35.1.0-beta.20260316.108';
+export const VERSION = '35.1.0-beta.20260317.135';
diff --git a/testing/csp/package.json b/testing/csp/package.json
index ff01dea0b76..e0e485a8a83 100644
--- a/testing/csp/package.json
+++ b/testing/csp/package.json
@@ -1,6 +1,6 @@
{
"name": "ag-grid-csp",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"description": "CSP testing for AG Grid",
"main": "index.js",
"scripts": {},
diff --git a/testing/module-size-angular/package.json b/testing/module-size-angular/package.json
index 374be546b35..3db61c92dc2 100644
--- a/testing/module-size-angular/package.json
+++ b/testing/module-size-angular/package.json
@@ -1,6 +1,6 @@
{
"name": "ag-grid-module-size-angular",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"scripts": {
"ng": "ng",
"start": "ng serve",
@@ -20,11 +20,11 @@
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
- "ag-grid-angular": "35.1.0-beta.20260316.108",
- "ag-grid-community": "35.1.0-beta.20260316.108",
- "ag-grid-enterprise": "35.1.0-beta.20260316.108",
- "ag-charts-community": "13.1.0-beta.20260316",
- "ag-charts-enterprise": "13.1.0-beta.20260316",
+ "ag-grid-angular": "35.1.0-beta.20260317.135",
+ "ag-grid-community": "35.1.0-beta.20260317.135",
+ "ag-grid-enterprise": "35.1.0-beta.20260317.135",
+ "ag-charts-community": "13.1.0-beta.20260317",
+ "ag-charts-enterprise": "13.1.0-beta.20260317",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
diff --git a/testing/module-size/package.json b/testing/module-size/package.json
index bace13437cd..927574276eb 100644
--- a/testing/module-size/package.json
+++ b/testing/module-size/package.json
@@ -1,7 +1,7 @@
{
"name": "ag-grid-module-size",
"private": true,
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"scripts": {
"dev": "vite",
"cp-app": "cp ./src/App_Src.tsx ./src/App_AUTO.tsx",
@@ -14,11 +14,11 @@
"test:e2e": "run-s \"module-combinations -- {1}\" module-validate --"
},
"dependencies": {
- "ag-grid-react": "35.1.0-beta.20260316.108",
- "ag-grid-community": "35.1.0-beta.20260316.108",
- "ag-grid-enterprise": "35.1.0-beta.20260316.108",
- "ag-charts-community": "13.1.0-beta.20260316",
- "ag-charts-enterprise": "13.1.0-beta.20260316",
+ "ag-grid-react": "35.1.0-beta.20260317.135",
+ "ag-grid-community": "35.1.0-beta.20260317.135",
+ "ag-grid-enterprise": "35.1.0-beta.20260317.135",
+ "ag-charts-community": "13.1.0-beta.20260317",
+ "ag-charts-enterprise": "13.1.0-beta.20260317",
"ag-shared": "0.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
diff --git a/testing/public-recipes/e2e/package.json b/testing/public-recipes/e2e/package.json
index f0381527730..dfbe9b74e19 100644
--- a/testing/public-recipes/e2e/package.json
+++ b/testing/public-recipes/e2e/package.json
@@ -1,12 +1,12 @@
{
"name": "ag-grid-public-e2e-testing-recipes",
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"description": "Public E2E testing recipes for AG Grid",
"main": "index.js",
"scripts": {},
"license": "MIT",
"devDependencies": {
- "ag-grid-community": "35.1.0-beta.20260316.108",
+ "ag-grid-community": "35.1.0-beta.20260317.135",
"playwright": "^1.56.0",
"@playwright/test": "^1.56.0",
"@types/node": "^24.0.3"
diff --git a/testing/vue3-tests/package.json b/testing/vue3-tests/package.json
index a39251e6fd6..5838b27da1c 100644
--- a/testing/vue3-tests/package.json
+++ b/testing/vue3-tests/package.json
@@ -1,7 +1,7 @@
{
"name": "ag-grid-vue3-tests",
"private": true,
- "version": "35.1.0-beta.20260316.108",
+ "version": "35.1.0-beta.20260317.135",
"type": "module",
"scripts": {
"dev": "vite",
@@ -15,9 +15,9 @@
"dependencies": {
"vue": "^3.5.0",
"vue-router": "^4.5.0",
- "ag-grid-community": "35.1.0-beta.20260316.108",
- "ag-grid-enterprise": "35.1.0-beta.20260316.108",
- "ag-grid-vue3": "35.1.0-beta.20260316.108",
+ "ag-grid-community": "35.1.0-beta.20260317.135",
+ "ag-grid-enterprise": "35.1.0-beta.20260317.135",
+ "ag-grid-vue3": "35.1.0-beta.20260317.135",
"decimal.js": "^10.4.3"
},
"devDependencies": {
diff --git a/yarn.lock b/yarn.lock
index 12c88bf1e3e..089c86a27c5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9186,61 +9186,61 @@ adm-zip@^0.5.10:
resolved "http://52.50.158.57:4873/adm-zip/-/adm-zip-0.5.16.tgz#0b5e4c779f07dedea5805cdccb1147071d94a909"
integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==
-ag-charts-angular@13.1.0-beta.20260316:
- version "13.1.0-beta.20260316"
- resolved "https://registry.ag-grid.com/ag-charts-angular/-/ag-charts-angular-13.1.0-beta.20260316.tgz#9b12a7fde20fb890f91d04c707a43f172bbbd036"
- integrity sha512-kmaMfU69ABo+1jCc2dRNjvfbtJCPuyV1+HeIg6idrZcI9a+w2rKz+wuhkjKbNHszshEVANpZ2czlrhRl1fFGAg==
+ag-charts-angular@13.1.0-beta.20260317:
+ version "13.1.0-beta.20260317"
+ resolved "https://registry.ag-grid.com/ag-charts-angular/-/ag-charts-angular-13.1.0-beta.20260317.tgz#849515c4450b0d6813d3c0c3aaf8a62031e1812e"
+ integrity sha512-168ttJyl3Yd8jSZKslgJj9al2AouE9jkYdyJNC+lYjWPwUA3WghH87VJ6YwXvPqkwQAzOdeFyuDe87AiTC1aow==
dependencies:
- ag-charts-community "13.1.0-beta.20260316"
+ ag-charts-community "13.1.0-beta.20260317"
tslib "^2.3.0"
-ag-charts-community@13.1.0-beta.20260316:
- version "13.1.0-beta.20260316"
- resolved "https://registry.ag-grid.com/ag-charts-community/-/ag-charts-community-13.1.0-beta.20260316.tgz#e0da49c8d7e707ed9eca10b705723864a9cd8cc5"
- integrity sha512-4NjGJCSEbLgIVQmVFJGsNCE6d+6Okn3HM+ynR03+m/V8Ut9FupoS8KDW6HCRIhR+neHMgyB8Mekv2eIwaRD1UA==
+ag-charts-community@13.1.0-beta.20260317:
+ version "13.1.0-beta.20260317"
+ resolved "https://registry.ag-grid.com/ag-charts-community/-/ag-charts-community-13.1.0-beta.20260317.tgz#0d641b4d3f670b5af98d9b19dde0e0825c3f09f9"
+ integrity sha512-UufsabNwr8rIecxqpOkhRjP4lvoK7EKrCWGHNIhVmuW8Nmd2WvS4F2e9cema13jey4g8MsZhj+eXeuFGzE2K9g==
dependencies:
- ag-charts-core "13.1.0-beta.20260316"
- ag-charts-locale "13.1.0-beta.20260316"
- ag-charts-types "13.1.0-beta.20260316"
+ ag-charts-core "13.1.0-beta.20260317"
+ ag-charts-locale "13.1.0-beta.20260317"
+ ag-charts-types "13.1.0-beta.20260317"
-ag-charts-core@13.1.0-beta.20260316:
- version "13.1.0-beta.20260316"
- resolved "https://registry.ag-grid.com/ag-charts-core/-/ag-charts-core-13.1.0-beta.20260316.tgz#4b4998f683f0cf5e25dcd25ca5b7ae07ce4c5305"
- integrity sha512-mjxNwFOq7djJTg7j/VoUIOY+s0a+iCEErwsjSsmr7ZFL2wgfM97v3vqPLp8HR3rS4lfawkx5qtrNK1Dola4EWw==
+ag-charts-core@13.1.0-beta.20260317:
+ version "13.1.0-beta.20260317"
+ resolved "https://registry.ag-grid.com/ag-charts-core/-/ag-charts-core-13.1.0-beta.20260317.tgz#bc88a29d8b546c587acbbef0aac7222bdb1a0ce2"
+ integrity sha512-rE17H78NfyfiS2MUP2QuwWwfQDy7K+VY1uIUaHZN36Lqzg6EjIGZslXyVafSCKL2PzgbrueDzN9aaAjhpUideg==
dependencies:
- ag-charts-types "13.1.0-beta.20260316"
+ ag-charts-types "13.1.0-beta.20260317"
-ag-charts-enterprise@13.1.0-beta.20260316:
- version "13.1.0-beta.20260316"
- resolved "https://registry.ag-grid.com/ag-charts-enterprise/-/ag-charts-enterprise-13.1.0-beta.20260316.tgz#9f1a714ede7e90fc4496ee3f56bfd2729cc460fe"
- integrity sha512-fPoxS9Dg4KBRpSm8o4mfHcOMkRUxS39hLCaJZDV773kyWf9filvmss7nNwNgfrCr7aGbvhR996m4wY2Jrqiq/g==
+ag-charts-enterprise@13.1.0-beta.20260317:
+ version "13.1.0-beta.20260317"
+ resolved "https://registry.ag-grid.com/ag-charts-enterprise/-/ag-charts-enterprise-13.1.0-beta.20260317.tgz#04c6ce46c3fecfb274714256a200e0437a225904"
+ integrity sha512-EKwOnIxOllFsCDZLrumoc9sq2vfI3XY2tTtL0CGvtCCTPOBKeRaGwX3VXSal17KfMPs34v/AhPhJDXvWaI1zjA==
dependencies:
- ag-charts-community "13.1.0-beta.20260316"
- ag-charts-core "13.1.0-beta.20260316"
+ ag-charts-community "13.1.0-beta.20260317"
+ ag-charts-core "13.1.0-beta.20260317"
-ag-charts-locale@13.1.0-beta.20260316:
- version "13.1.0-beta.20260316"
- resolved "https://registry.ag-grid.com/ag-charts-locale/-/ag-charts-locale-13.1.0-beta.20260316.tgz#f96ed40b04adbc75db68d07369181629973845d0"
- integrity sha512-i1zYD/RWuhVK+Ffd8QbuGVMnfbIZ2p9i/7JGwMYnhPNxht305MFZbIXCh1wiCJqaQYGe6kiOJJ0zn9p/nLqGvg==
+ag-charts-locale@13.1.0-beta.20260317:
+ version "13.1.0-beta.20260317"
+ resolved "https://registry.ag-grid.com/ag-charts-locale/-/ag-charts-locale-13.1.0-beta.20260317.tgz#24e78e0b69dd42bcf598bd3b3e3efb0d90f8678f"
+ integrity sha512-sSdIwX2T/vY0+/6ESgxGIOq9f5LLRDQHYCQad2vqqE/zfRS01Lx968Gqlvy5fhQU0L2drivxz3mvxHIK4b7dMw==
-ag-charts-react@13.1.0-beta.20260316:
- version "13.1.0-beta.20260316"
- resolved "https://registry.ag-grid.com/ag-charts-react/-/ag-charts-react-13.1.0-beta.20260316.tgz#44e864cb9a3eebbdd8dd7033931a39bb9b6340dd"
- integrity sha512-wTv7M+9obU1NECIpEqQ1OBpXr4AaiSrGO9s761FYRCS1q4IkYxAXYZNCRhdEcbTlzzUcOhbXChAR95aJwcD/oA==
+ag-charts-react@13.1.0-beta.20260317:
+ version "13.1.0-beta.20260317"
+ resolved "https://registry.ag-grid.com/ag-charts-react/-/ag-charts-react-13.1.0-beta.20260317.tgz#617d002caa0398588bb18f75310b906bd3a936ca"
+ integrity sha512-Chbg20g8ds+MwoCXyHDh7ZnLcECuGsg8tOY79yux09NRjFA177aZxMG85QyJ9wKMUo4M5N8FRQEsvcyhSGJe9w==
dependencies:
- ag-charts-community "13.1.0-beta.20260316"
+ ag-charts-community "13.1.0-beta.20260317"
-ag-charts-types@13.1.0-beta.20260316:
- version "13.1.0-beta.20260316"
- resolved "https://registry.ag-grid.com/ag-charts-types/-/ag-charts-types-13.1.0-beta.20260316.tgz#d360bd47bc21ff977333f7eee49e1fe8197a483d"
- integrity sha512-W44kNoCmpWLv9h66RtcAOGdGl746l+kM5YtgJoLs0Faq+eBy0b9woiqxQV20LeEfVaTwcWHp1zroHE5zlzeDEg==
+ag-charts-types@13.1.0-beta.20260317:
+ version "13.1.0-beta.20260317"
+ resolved "https://registry.ag-grid.com/ag-charts-types/-/ag-charts-types-13.1.0-beta.20260317.tgz#34862f2c558db6dc4500e08215db834c53d6fc84"
+ integrity sha512-EcDingQ3VDuRtOMKsUsGNejCRvWE91VXswuAwg6z4WnAJxeFaU5nQuLoYSVE63kkgX83Ciw1epq57AKFJWJNVg==
-ag-charts-vue3@13.1.0-beta.20260316:
- version "13.1.0-beta.20260316"
- resolved "https://registry.ag-grid.com/ag-charts-vue3/-/ag-charts-vue3-13.1.0-beta.20260316.tgz#9550c8a3b80ad5cfe305ba1bbdfc559b9b53742f"
- integrity sha512-Lbem16r/o+eN25eIgQUabla1jqx46thWvsp+4+Zk9MLP6bjhvY36vl4aS3FMjGUJ6JyL5f82G8nFz3ref78t5g==
+ag-charts-vue3@13.1.0-beta.20260317:
+ version "13.1.0-beta.20260317"
+ resolved "https://registry.ag-grid.com/ag-charts-vue3/-/ag-charts-vue3-13.1.0-beta.20260317.tgz#41401eb165ebf9ee3082fdee4e093ebced31cb04"
+ integrity sha512-prBELHLc3CtVRvmfp0fGA+1yb4nG8aYzhYSFXO7IfRD7QjyUD04J2yLUJI1mLzBuSAkWw7GqHk8XXOymjlJxnA==
dependencies:
- ag-charts-community "13.1.0-beta.20260316"
+ ag-charts-community "13.1.0-beta.20260317"
agent-base@6, agent-base@^6.0.2:
version "6.0.2"
@@ -23029,16 +23029,7 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
-"string-width-cjs@npm:string-width@^4.2.0":
- version "4.2.3"
- resolved "http://52.50.158.57:4873/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "http://52.50.158.57:4873/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -23143,7 +23134,7 @@ stringify-entities@^4.0.0:
character-entities-html4 "^2.0.0"
character-entities-legacy "^3.0.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "http://52.50.158.57:4873/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -23157,13 +23148,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- version "6.0.1"
- resolved "http://52.50.158.57:4873/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
strip-ansi@^7.0.1, strip-ansi@^7.1.0:
version "7.1.2"
resolved "http://52.50.158.57:4873/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba"
@@ -25633,7 +25617,7 @@ wordwrap@^1.0.0:
resolved "http://52.50.158.57:4873/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "http://52.50.158.57:4873/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -25669,15 +25653,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "http://52.50.158.57:4873/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
wrap-ansi@^9.0.0:
version "9.0.2"
resolved "http://52.50.158.57:4873/wrap-ansi/-/wrap-ansi-9.0.2.tgz#956832dea9494306e6d209eb871643bb873d7c98"