Body
Hi Handsontable team,
We found an issue when using a custom flow to hide columns and rebuild nested headers.
This does not happen with the default Handsontable nested headers behavior.
It appears only in our case where we must rebuild and re-render nested headers dynamically.
Steps to reproduce
Hide a middle column (example: visual column index 4, column E).
Observe:
console shows correct hiddenVisual and next nested headers,
UI still hides/misaligns more than expected.
Expected
After hiding column E, only that column should be removed and parent group colspan should update correctly in UI.
Actual
Even though nestedHeaders passed to updateSettings() is correct, UI renders incorrect nested headers (more than one column/group appears affected).
Question
Is this a known limitation/bug in dynamic nested-header rebuild after hide/show?
Is there a required API/order beyond updateSettings() + render() to force full nested-header recalculation and repaint?
JS fiddle: https://jsfiddle.net/ex3quj02/1/
Handson version: 16.2
Hi @sagun.saluja1
Thank you for reaching out to us. We have this issue reported internally. I added your report there and will let you know as soon as I have any updates.
Hi Adrian, hope you’re well. I work with Sagun (the original poster in this thread).
From your reply, I understand that this issue was already identified internally, and you have added our report to that existing issue. Can I please check when this was originally logged, as we need to get an idea of how long it may take to fix, since it is affecting our possible options for some improvements we’re aiming to add to our existing customer-facing solution.
Many thanks, Blake.
Hi @blake.gilchrist
This issue isn’t on our roadmap yet, but I hope we will be able to fix it in the nearest future. I will update this topic as soon as I have more information.
Ok, thanks. We’ll look at alternative HoT configuration options in the meantime.
@blake.gilchrist
Thank you for your patience. Please let me know if there’s anything I can help you with.
Hi Adrian, hope you’re well. Just checking if there has been any progress on this issue, please? Thanks.
Hi @sagun.saluja1 and @blake.gilchrist , this issue has been fix with the v18 release. Please let me know if you still experience any issues after updating.
develop ← feature/DEV-294_NestedHeaders-Collapsible-Hidden-Columns
opened 12:32PM - 12 Jun 26 UTC
### Context
This PR makes nested headers work correctly together with **Collaps… ibleColumns**, **HiddenColumns**, **ManualColumnMove**, and column insertion/removal. The entry point was nested headers misaligning with the grid body when columns are hidden inside a collapsible header group (reported in dev-handsontable #500; a client showstopper since v8.0, worked in v7), but fixing it surfaced a cluster of related issues in the same area, all addressed here.
**Root cause of the original misalignment:** the header colspan was incrementally mutated by per-operation node modifiers (`hideColumn`/`showColumn`), and to avoid double-counting when collapse fired the hide observer, those modifiers early-returned whenever any ancestor was `collapsible`. That blanket guard silently dropped genuine external hides inside a collapsible group, so the group's colspan was never reduced - a group claiming colspan 3 over 2 visible columns made the header row one column wider than the body, shifting every header to its right off its data column.
**Core fix:** derive each header node's `colspan` / `crossHiddenColumns` / `isHidden` from a single visibility source (a `ColumnVisibility` port backed by the column index mapper) via `syncVisibilityOnTree`, instead of mutating colspan per operation. The derivation is source-agnostic - it does not matter whether a column is hidden by `CollapsibleColumns`, `HiddenColumns`, or any future source - so the plugins stay decoupled. This deletes the `hideColumn`/`showColumn` node modifiers and the collapsible guard, and runs on every column index mapper `cacheUpdated` (moves and hides) so move-while-hidden stays aligned too.
### What this PR changes
**Header alignment & visibility**
- Derive header colspan/visibility from one source (`syncVisibilityOnTree` + `ColumnVisibility` adapter) instead of per-operation mutation; removes the `hideColumn`/`showColumn` modifiers and the collapsible guard.
- Show the hidden-column indicator only on the header closest to the cells (the bottom header row), not on wider parent/group headers that would point at an internal or duplicated boundary.
**Column insert / remove**
- NestedHeaders now follows structural changes: `afterCreateCol` extends the header structure (a column inserted inside a span widens it and every spanning ancestor; a column inserted at a boundary becomes a standalone header) and `afterRemoveCol` shrinks, re-anchors, or drops headers that overlap the removed range.
- The collapsed state is preserved across these rebuilds (and across `mapState`/`mergeStateWith`) by re-running the collapse on the rebuilt tree - restoring both the `isCollapsed` indicator and the `clonedTree` needed to expand again - rather than restoring the flag alone.
**Collapse semantics**
- A collapse is treated as effective when it owns columns (`affectedColumns.length > 0`), not only when the colspan changes. So collapsing a group whose extra child is already hidden by HiddenColumns now records the collapse (the indicator shows collapsed), and the group stays collapsed even if HiddenColumns later shows that column - only expanding the group reveals it.
**Selection**
- Removing a column/row no longer leaves a single-cell selection stranded on a now-hidden index; the highlight snaps to the nearest visible one. Applied to both `shiftColumns` and `shiftRows` in core selection (benefits every hiding plugin). Wider ranges are left untouched - they legitimately keep hidden indices within their copy/fill bounds.
**Internal refactors (no behavior change)**
- `TreeNode` made generic (`TreeNode<T>`) so the nested-headers state can be typed end-to-end, removing ~12 `as`-casts at call sites with no runtime change.
- Gated the ghost-table width rebuild (`buildWidthsMap`) behind an actual hiding change so a pure column move no longer rebuilds and measures a detached DOM table; visibility derivation stays unconditional.
- Removed a discarded intermediate matrix generation in the collapse-preserving rebuild, and deduplicated the selection snap into a shared `snapToNearestVisible` helper.
### How has this been tested?
New and existing unit + E2E tests, run under `main`, `horizon`, and `classic` themes.
- **E2E (new):** `nestedHeaders/__tests__/plugins/collapsibleColumns.spec.js` and `.../alteringColumns.spec.js` cover: hide under a collapsible header, initial `hiddenColumns` config, collapse with a hidden child (incl. round-trip ownership and staying recoverable when a collapsed sub-group loses its only visible column), collapse/expand keeping an external hide, the full client combo (CollapsibleColumns + ManualColumnMove + HiddenColumns, with an actual move), `fixedColumnsStart` frozen-overlay colspan clamp, nested groups, adding/removing columns left/right of and inside collapsed groups, and selection adjustment after each alter (`toEqualCellRange`), plus a width-alignment regression for an early-collapsed group.
- **E2E (selection snap):** row/column equivalents in `hiddenColumns/__tests__/altering.spec.js` and `hiddenRows/__tests__/altering.spec.js` (single-cell snap + multi-column/row range left intact).
- **Unit:** `nestedHeaders/__tests__/stateManager` - matrix-generator assertions preserved (behavior unchanged), tests adapted to the `syncVisibilityOnTree` API, level-keyed collapse restore, and `affectedColumns` ordering.
- Regression-checked across `hiddenColumns`, `hiddenRows`, `manualColumnMove`, `selection`, and `collapsibleColumns` E2E suites with no failures; full unit suite green.
### 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. DEV-294
2. dev-handsontable #500
### 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://docs.google.com/forms/d/e/1FAIpQLScpMq4swMelvw3-onxC8Jl29m0fVp5hpf7d1yQVklqVjGjWGA/viewform?c=0&w=1)
- [ ] My change requires a change to the documentation.
ClickUp task: https://app.clickup.com/t/9015210959/DEV-294
---
> [!NOTE]
> **High Risk**
> Large changes across NestedHeaders, CollapsibleColumns, selection, and column alter paths that affect header/body layout and visibility for many plugin combinations; mitigated by extensive tests but still high blast radius in the grid core.
>
> **Overview**
> Fixes **DEV-294** misalignment when nested headers share collapsible groups with **HiddenColumns** (and related plugins). Header **colspan and visibility are derived in one pass** from the column index mapper (`syncVisibilityOnTree`) instead of incremental hide/show node modifiers and the old collapsible guard that dropped external hides.
>
> **Collapse & columns:** Groups can collapse when a child is already hidden; collapse state and **`visibleWhen`** rules survive column insert/remove and settings rebuilds. **`visibleWhen`** (`collapsed` / `expanded` / `always`) controls which group columns stay visible per collapse state via a dedicated hiding map in **CollapsibleColumns**. Hidden-column indicators render only on the bottom header row adjacent to cells.
>
> **Selection:** Single-cell selection snaps to the nearest visible column/row after remove when it would land on a hidden index (wider ranges unchanged).
>
> Adds docs/examples, typings for `nestedHeaders`, Angular docs typecheck **`SKIP_LATEST`**, and large E2E/unit coverage.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 4e59a4d627b8ce4991a4351eda7e446bf24ec041. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
1 Like
Hey Joseph. That’s great, thank you. We’ll look at upgrading to v18 ASAP and let you know if any further issues.
1 Like