fix(grid): grid custom group headers follow sticky start columns during horizontal scrolling - #3592
fix(grid): grid custom group headers follow sticky start columns during horizontal scrolling#3592michal-sanoma wants to merge 18 commits into
Conversation
…3367-from-fork # Conflicts: # packages/components/grid/src/grid.spec.ts # packages/components/grid/src/grid.ts
🦋 Changeset detectedLatest commit: 5dfa538 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
🕸 Preview deploys |
There was a problem hiding this comment.
Pull request overview
Fixes grouped grid headers so custom headers remain aligned with sticky start columns during horizontal scrolling.
Changes:
- Makes inner group headers sticky while preserving full-width group rows.
- Adds a Storybook reproduction and regression test.
- Includes unrelated formatting-only cleanups.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
.changeset/young-baboons-retire.md |
Records the grid patch. |
packages/components/grid/src/grid.ts |
Implements sticky group headers. |
packages/components/grid/src/grid.spec.ts |
Adds regression coverage. |
packages/components/grid/src/stories/grouping.stories.ts |
Adds a reproduction story. |
packages/components/grid/src/stories/selection.stories.ts |
Formatting cleanup. |
packages/components/accordion/src/accordion.stories.ts |
Formatting cleanup. |
packages/components/card/src/card.scss |
Formatting cleanup. |
packages/components/icon/src/icon.scss |
Formatting cleanup. |
tools/implement-design-plugin/agents/visual-validator.md |
Formats a Markdown table. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Collapsing all groups makes the last group row appear shorter than it should be. This happens on larger monitor.
On Mac screen the same last group row is higher than it should be.
Larger screen:
Screen.Recording.2026-08-13.at.08.41.04.mov
Mac screen:
Screen.Recording.2026-08-13.at.08.57.30.mov
Should be better now |
@michal-sanoma it is! 🙌 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (2)
packages/components/grid/src/group-header.scss:26
- This fade is rendered whenever multiple sticky-start columns exist, including at
scrollLeft === 0. The existing sticky-boundary fade ingrid.scss:124-148is intentionally gated by[scrollable-start], so group rows now show a boundary shadow before horizontal scrolling while the other rows do not. Propagate the grid'sscrollable-startstate to the group header and only enable this pseudo-element in that state; the regression test should cover both the initial and scrolled states.
:host(.sticky-start-last) [part='group-heading-wrapper']::after {
background: linear-gradient(to right, rgb(79 79 79 / 10%), transparent);
content: '';
inline-size: 1rem;
inset: 0 -1rem 0 auto;
packages/components/grid/src/grid.scss:320
- When the final group is collapsed and the grid does not need a
tfoot, this leaves the group's cell border directly above the existingtbodybottom border, producing a double-width bottom edge. Preserve the group row's height with a transparent cell border when there is no footer, while retaining the visible separator when a sticky scrollbar footer exists.
tr:last-of-type:not([part~='group']) td {
// Hide the bottom border of the last row; tbody already has a bottom border
border-block-end: none;
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (4)
packages/components/grid/src/grid.spec.ts:899
- These related DOM-query declarations should be part of the same multiline
conststatement, following the repository convention used inpackages/components/grid/src/grid.ts:640-648.
const groupHeader = el.renderRoot.querySelector<GridGroupHeader>(
packages/components/grid/src/grid.ts:1314
- The suffix cannot be inferred from the number of visible sticky columns. If a leading sticky column is hidden, the remaining column can still have
stickyOrder === 'last', while this assigns the group headersticky-start-first; the data-cell boundary then shows a shadow but the group header does not. Derive the suffix from the last visible sticky column's assigned order so both use the same boundary styling.
return [`sticky-start-${columns.length > 1 ? 'last' : 'first'}`];
packages/components/grid/src/stories/grouping.stories.ts:290
- Please combine these related declarations into one multiline
const, following the repository convention used inpackages/components/grid/src/grid.ts:640-648.
const groupHeaderRenderer = (item: ListDataSourceGroupItem) => {
packages/components/grid/src/grid.spec.ts:873
- Please combine these related declarations into one multiline
const, following the repository convention used inpackages/components/grid/src/grid.ts:640-648.
This issue also appears on line 899 of the same file.
const groupHeaderRenderer = (item: ListDataSourceGroupItem<Person>) => {
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (2)
packages/components/grid/src/grid.ts:1326
- A column configured with
width="0"is rendered as 100px byrenderStyles()(col.width || '100'), but this calculation counts it as 0px. The sticky group heading then becomes narrower than the actual sticky cells. Use the same fallback semantics as the cell sizing rule.
const inlineSize = columns.reduce((acc, { width }) => acc + (width ?? 100), 0);
packages/components/grid/src/grid.ts:1315
- When the final sticky-start column is hidden, the preceding visible column can have no
stickyOrder. This fallback then marks the group headersticky-start-last, while the visibletd/thonly havesticky-start. Becausegrid.scssusestable:not(:has(.sticky-start-last)), the group-header class suppresses the table fallback shadow even though no data/header boundary cell renders one. Recompute sticky order from visible columns, or use group-specific class names/selectors so the group marker cannot affect the table-wide check.
`sticky-start-${columns.at(-1)?.stickyOrder ?? (columns.length > 1 ? 'last' : 'first')}`
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (1)
packages/components/grid/src/grid.ts:1326
- The
100pxfallback is inconsistent with the sticky offset calculation below. With two sticky start columns where the first haswidth="0", that cell renders at 100px, but the second column still receives a 0px sticky offset while this wrapper is sized for both 100px widths. Once scrolled, the cells overlap and the group heading no longer matches their sticky boundary. Use the same normalized column width for both calculations (and cover this multi-column case in the fallback test).
const inlineSize = columns.reduce((acc, { width }) => acc + (width || 100), 0);
Really cool finding, I've improved it |
…group_headers # Conflicts: # packages/components/grid/src/grid.ts # packages/components/grid/src/group-header.ts
| tbody.dispatchEvent(new Event('scroll')); | ||
|
|
||
| expect(getComputedStyle(headingWrapper!, '::after').content).to.equal('""'); | ||
| expect(groupCell!.getBoundingClientRect().width).to.be.greaterThan( |
There was a problem hiding this comment.
This is a test, so I think we should be able to check explicit value, no value greater than.
| expect( | ||
| getComputedStyle(row!.querySelector('td')!).getPropertyValue('border-block-end-color') | ||
| ).not.to.equal('rgba(0, 0, 0, 0)'); | ||
| expect(tfoot!.getBoundingClientRect().top).to.be.at.least( |
There was a problem hiding this comment.
This is a test, so I think we should be able to check explicit value.
There was a problem hiding this comment.
This one I would prefer to keep as is bc it's unstable when checking exact value on CI
| } | ||
| }; | ||
|
|
||
| export const StickyColumnsWithCustomGroupHeader: Story = { |
| } | ||
| }; | ||
|
|
||
| export const StickyColumnsWithCustomGroupHeader: Story = { |
| } | ||
| }; | ||
|
|
||
| export const StickyColumnsWithCustomGroupHeader: Story = { |
There was a problem hiding this comment.
Maybe it's worth to add such example to Angular storybook as well.



Summary
Fixes grouped grid headers so custom group headers follow sticky start columns during horizontal scrolling.
Changes
Copy of this PR - #3370
BEFORE
before.mov
AFTER
after.mov