shrvic
(Sandi)
October 5, 2026, 1:57pm
1
Hello, I just tried to upgrade to the latest version, hoping all the issues have been addressed but found a new problem (see below). Not sure exactly when this broke but it used to work fine in 16.2.
Thanks!
Version: 18.1.1
Summary
When nestedHeaders is enabled with autoColumnSize not set to false, NestedHeaders grows columns via its ghost table, but never invalidates Walkontable’s column PositionCache. That cache only rebuilds when column count changes (PositionCache.isCurrent() checks count, not size), so any column whose offset was cached before the ghost table’s growth settled keeps a stale, pre-growth position forever. The scrollbar itself is sized correctly off the grown widths, but rendered column positions lag behind, so scrolling to the true end leaves a permanent blank gap, as if the last column(s) never rendered. See attached example : the autoColumnSize: true grid shows the gap; autoColumnSize: false (same data/columns) does not.
Why only this plugin
autoColumnSize’s own plugin avoids this: it writes measured widths to a tracked columnWidthsMap and calls columnIndexMapper.observeMapChange(this.columnWidthsMap, () => this.hot.view?.invalidateColumnWidthCache()) once in enablePlugin(). NestedHeaders’ _onModifyColWidth (in plugins/nestedHeaders/nestedHeaders.mjs) has no equivalent — it just returns a different width through the modifyColWidth hook on demand, with no cache invalidation:
_onModifyColWidth = (width, column) => {
if (this.hot.getSettings().autoColumnSize === false) return width;
const cachedWidth = this.ghostTable.getWidth(column) ?? 0;
return width > cachedWidth ? width : cachedWidth;
};
Suggested fix: NestedHeaders should call this.hot.view?.invalidateColumnWidthCache() whenever buildWidthsMap() produces a changed width, the same way autoColumnSize does.
cc @dhaber
Hi @shrvic , thanks for the detailed report! That made it easy to reproduce and confirm. I’ve created an issue here:
opened 03:18PM - 05 Oct 26 UTC
bug
triaged
### Describe the bug
With `nestedHeaders` enabled and `autoColumnSize` active (… the default), scrolling to the horizontal end of the grid leaves a permanent blank area on the right — in larger cases the entire viewport is blank, as if the last columns never render. The scrollbar range is correct; the rendered column positions are not.
This is a regression introduced in **18.0.0** and still present on current `develop`. Originally reported on the forum: https://forum.handsontable.com/t/9102
**Steps to reproduce**
1. Create a grid in a fixed-width container with ~26 columns of short values, nested headers whose labels are much wider than the data, and `autoColumnSize` left enabled:
```js
const container = document.getElementById('container');
new Handsontable(container, {
data: Array.from({ length: 10 }, (_, r) =>
Array.from({ length: 26 }, (_, c) => `${r}${String.fromCharCode(65 + c)}`)),
nestedHeaders: [
Array.from({ length: 13 }, (_, i) => ({ label: `Group ${i + 1}`, colspan: 2 })),
Array.from({ length: 26 }, (_, i) => `Very Long Column Header Label ${i + 1}`),
],
rowHeaders: true,
width: 760,
height: 400,
licenseKey: 'non-commercial-and-evaluation',
});
```
2. Scroll fully to the right.
3. The viewport shows a blank area instead of the last columns. The gap survives further scrolling and is only repaired by a full `hot.render()`.
Diagnostic showing the stale cache (run after init):
```js
// cached offsets were built from pre-growth 50px defaults…
hot.view._wt.wtViewport.columnWidthCache.getTotalSize(); // → 1300 (26 × 50)
// …while the live widths (and the scroll range) reflect the ghost-table growth
Array.from({ length: 26 }, (_, i) => hot.getColWidth(i)).reduce((a, b) => a + b); // → ~6284
```
**Root cause**
`NestedHeaders` grows column widths via its ghost table through the `modifyColWidth` hook (`#onModifyColWidth`), but never calls `hot.view.invalidateColumnWidthCache()`. Walkontable's column `PositionCache` (introduced in 18.0.0) is only rebuilt when `isCurrent()` fails, and `isCurrent()` checks the column count, not the sizes — so scroll-driven draws keep the stale pre-growth offsets forever, while `sumColumnWidths` (live walk) sizes the scroll range from the grown widths. The mismatch is the blank area.
This is the same class of bug as #13493, which established the invalidation contract for width writers: `AutoColumnSize` and `ManualColumnResize` fulfill it, `NestedHeaders` does not. (#13493 does not fix this case — reproduced on `develop` at `b0dc2e1`, which includes it.)
**Suggested fix**
Invalidate the column width cache whenever `ghostTable.buildWidthsMap()` produces changed widths, mirroring #13493.
**Verified workarounds** (for anyone hitting this meanwhile): set `autoColumnSize: false`, or call `hot.render()` once after the table settles — the full-render path rebuilds the cache.
### Video/Screenshots
The forum demo below shows it; with the snippet above, the viewport at full scroll is entirely blank except row headers.
### Provide a link to the demo with the bug reproduction
Reporter's demo (shows the gap with `autoColumnSize: true`, none with `false`): https://stackblitz.com/edit/js-p7gkjyhx?file=index.html
### Handsontable version
18.1.1 (regression since 18.0.0; also reproduces on develop build `0.0.0-next-b0dc2e1-20261002`; 17.0.0 and 16.2.0 are unaffected)
### Framework version
None (vanilla JS)
### Your environment
Chromium on macOS 15 — the stale cache is browser-independent
I’ll update this post with an ETA once we have a fix ready to deploy. For now, you can work around it with the autoSize: false as you mentioned, or keep autoColumnSize: true and call hot.render() once after the table settles.
UPDATE: This has been fixed on our develop branch
develop ← feature/DEV-3275_NestedHeaders-column-width-cache
opened 07:52AM - 09 Oct 26 UTC
### Context
The PR fixes a blank area at the horizontal scroll end of a grid wi… th `nestedHeaders`. The nested headers' ghost table grows columns to fit header labels wider than the data, and writes the grown widths through `modifyColWidth`. It never dropped Walkontable's column-width cache, which compares only the column count. The cache kept the narrow pre-growth sums while the scroll range used the grown widths, so scrolling to the right edge rendered the last columns far off-screen (1437px in the repro). `GhostTable#buildWidthsMap()` and `GhostTable#clear()` now call `hot.view?.invalidateColumnWidthCache()`, the same contract `AutoColumnSize`, `ManualColumnResize`, and `StretchColumns` already follow.
Fixes #13751.
### Test evidence (required for source changes)
- Unit tests added/modified (`*.unit.js` or `*.unit.ts`): `handsontable/src/plugins/nestedHeaders/utils/__tests__/ghostTable.unit.js`
- E2E tests added/modified (Playwright `tests/e2e/*.spec.ts`): `tests/e2e/nested-headers-grown-widths.spec.ts` (with `tests/fixtures/demo/nested-headers-grown-widths.html` and `tests/fixtures/pages/NestedHeadersGrownWidthsPage.ts`)
- Type tests (`*.types.ts`) updated if public API changed: none – no public API change
- For a bug fix – the spec that fails without this fix: `nested headers that grow the column widths › scrolled to the right edge, the grid renders its last column` (failed before the fix with the last cell 1437px outside the visible area, passes after)
- Demo page / recorded trace (for UI changes): none
- Visual spec added/modified (`visual-tests/tests/**/*.spec.ts`, only for pixels no DOM probe can express – in addition to, never instead of the E2E above): none
### Commands run
```bash
npm --prefix handsontable run build
cd tests && npx playwright test e2e/nested-headers-grown-widths.spec.ts e2e/nested-headers-long-label.spec.ts e2e/stretch-columns-container-resize.spec.ts --project=e2e-main
```
13 passed (e2e-main only; the other legs run in CI)
```bash
npm --prefix handsontable run test:unit.jest
```
Test Suites: 521 passed, 521 total. Tests: 7624 passed, 7624 total.
### Types of changes
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature or improvement (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Additional language file or change to the existing one (translations)
### Related issue(s):
1. #13751
### Affected project(s):
- [x] `handsontable`
- [ ] `@handsontable/angular-wrapper`
- [ ] `@handsontable/react-wrapper`
- [ ] `@handsontable/vue3`
### Checklist:
- [x] I have reviewed the guidelines about [Contributing to Handsontable](https://github.com/handsontable/handsontable/blob/master/CONTRIBUTING.md) and I confirm that my code follows the code style of this project.
- [x] I have signed the [Contributor License Agreement](https://cla.handsontable.com/sign) – one signature covers both Handsontable and HyperFormula; the `cla/signed` check on this PR confirms it.
- [ ] My change requires a change to the documentation.
- [ ] MANUAL QA NEEDED –
ClickUp task: https://app.clickup.com/t/9015210959/DEV-3275
---
> [!NOTE]
> **Low Risk**
> Targeted cache invalidation on an existing NestedHeaders code path; aligns with other width producers and is covered by new E2E and unit tests.
>
> **Overview**
> Fixes **#13751**: horizontal scroll could end on a blank gap when `nestedHeaders` widens columns for long header labels.
>
> **NestedHeaders** measures labels in a ghost table and applies widths via `modifyColWidth`, but Walkontable’s column-width prefix cache only invalidates on column **count** changes. Totals stayed at pre-growth widths while scroll range used the grown widths, so the last columns sat far off-screen at the right edge.
>
> `GhostTable#buildWidthsMap()` and `#clear()` now call `hot.view?.invalidateColumnWidthCache()`, matching **StretchColumns**, **ManualColumnResize**, and **AutoColumnSize**. Internal docs note NestedHeaders as a fourth `modifyColWidth` producer that must drop the cache.
>
> Adds a Playwright E2E (`nested-headers-grown-widths`) with fixture/page object, unit expectations on the invalidation calls, and a changelog entry.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit fab5a026ade293c5b2ba39cb7d93d4bc9bf1adc3. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
I’ll update again when it ships in the next release. Thanks again for reporting it!
shrvic
(Sandi)
October 9, 2026, 12:24pm
5
Nice! You’re welcome and thanks for keeping me in the loop.