From 41129e7f17e24f13eb41cc84a8d988fecc36de59 Mon Sep 17 00:00:00 2001 From: tatakaisun <136896447+tatakaisun@users.noreply.github.com> Date: Thu, 21 May 2026 21:44:44 +0900 Subject: [PATCH] fix(grid): keep group headers sticky with sticky columns --- packages/components/grid/src/grid.spec.ts | 36 +++++++++++++- packages/components/grid/src/grid.ts | 35 ++++++++++++- .../grid/src/stories/grouping.stories.ts | 49 +++++++++++++++++++ 3 files changed, 118 insertions(+), 2 deletions(-) diff --git a/packages/components/grid/src/grid.spec.ts b/packages/components/grid/src/grid.spec.ts index 222e125121..b7b41dce31 100644 --- a/packages/components/grid/src/grid.spec.ts +++ b/packages/components/grid/src/grid.spec.ts @@ -1,4 +1,5 @@ import '@sl-design-system/button/register.js'; +import { ArrayListDataSource } from '@sl-design-system/data-source'; import '@sl-design-system/menu/register.js'; import { isPopoverOpen } from '@sl-design-system/shared'; import { type ToolBar } from '@sl-design-system/tool-bar'; @@ -13,7 +14,7 @@ import '../register.js'; import { type Grid, type SlActiveRowChangeEvent } from './grid.js'; import { waitForGridToRenderData } from './utils.js'; -type Person = { firstName: string; lastName: string; email?: string }; +type Person = { firstName: string; lastName: string; email?: string; group?: string }; describe('sl-grid', () => { let el: Grid; @@ -737,4 +738,37 @@ describe('sl-grid', () => { expect(toolBar!.items.filter(item => item.visible).length).to.equal(5); }); }); + + describe('group headers with sticky columns', () => { + it('should make the group header follow the sticky start columns', async () => { + const dataSource = new ArrayListDataSource( + [ + { firstName: 'John', lastName: 'Doe', group: 'A' }, + { firstName: 'Jane', lastName: 'Smith', group: 'A' } + ], + { groupBy: 'group' } + ); + + el = await fixture(html` + + + + + + `); + + await waitForGridToRenderData(el); + await el.updateComplete; + + const groupHeader = el.renderRoot.querySelector( + 'tbody tr[part~="group"] sl-grid-group-header' + ); + + expect(groupHeader).to.exist; + expect(groupHeader?.classList.contains('sticky-start-last')).to.be.true; + expect(groupHeader?.style.position).to.equal('sticky'); + expect(groupHeader?.style.insetInlineStart).to.equal('0px'); + expect(groupHeader?.style.inlineSize).to.equal('200px'); + }); + }); }); diff --git a/packages/components/grid/src/grid.ts b/packages/components/grid/src/grid.ts index 7b0d8f00fa..c9cbdc46e6 100644 --- a/packages/components/grid/src/grid.ts +++ b/packages/components/grid/src/grid.ts @@ -38,6 +38,7 @@ import { render } from 'lit'; import { property, query, state } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; import { GridColumnGroup } from './column-group.js'; import { GridColumn } from './column.js'; import { GridDragHandleColumn } from './drag-handle-column.js'; @@ -557,7 +558,7 @@ export class Grid extends ScopedElementsMixin(LitElement) { })} ${rows[rows.length - 1].map((col, index) => { return ` - :where(tbody td, thead tr th):nth-child(${index + 1}) { + :where(tbody tr:not([part~='group']) td, thead tr th):nth-child(${index + 1}) { flex-grow: ${col.grow}; inline-size: ${col.width || '100'}px; justify-content: ${col.align ?? 'start'}; @@ -630,6 +631,8 @@ export class Grid extends ScopedElementsMixin(LitElement) { draggable = !!this.#columnDefinitions.find( col => !col.hidden && col instanceof GridDragHandleColumn ), + groupHeaderClasses = this.#getGroupHeaderClasses(), + groupHeaderStyles = this.#getGroupHeaderStyles(), selectable = !!this.#columnDefinitions.find( col => !col.hidden && col instanceof GridSelectionColumn ); @@ -640,10 +643,12 @@ export class Grid extends ScopedElementsMixin(LitElement) { ) => this.#onGroupSelect(event, item)} @sl-toggle=${(event: SlToggleEvent) => this.#onGroupToggle(event, item)} + class=${ifDefined(groupHeaderClasses.join(' ') || undefined)} ?collapsed=${collapsed} ?drag-handle=${draggable} ?selectable=${selectable} .selected=${item.selected ?? 'none'} + style=${ifDefined(groupHeaderStyles)} > ${this.groupHeaderRenderer?.(item) ?? html` @@ -1208,6 +1213,28 @@ export class Grid extends ScopedElementsMixin(LitElement) { } } + #getGroupHeaderClasses(): string[] { + const columns = this.#getStickyStartColumns(); + + if (!columns.length) { + return []; + } + + return [`sticky-start-${columns.length > 1 ? 'last' : 'first'}`]; + } + + #getGroupHeaderStyles(): string | undefined { + const columns = this.#getStickyStartColumns(); + + if (!columns.length) { + return undefined; + } + + const inlineSize = columns.reduce((acc, { width }) => acc + (width ?? 100), 0); + + return `flex-grow: 0; inline-size: ${inlineSize}px; inset-inline-start: 0px; position: sticky;`; + } + /** Returns the left offset, taking any sticky columns into account. */ #getStickyColumnOffset(index: number): number { let columns: Array>; @@ -1221,6 +1248,12 @@ export class Grid extends ScopedElementsMixin(LitElement) { return columns.filter(col => !col.hidden).reduce((acc, { width = 0 }) => acc + width, 0); } + #getStickyStartColumns(): Array> { + return this.#columnDefinitions.filter( + col => !col.hidden && col.sticky && col.stickyPosition === 'start' + ); + } + #removeColumn(col: GridColumn): void { if (col instanceof GridSortColumn) { if (col.direction) { diff --git a/packages/components/grid/src/stories/grouping.stories.ts b/packages/components/grid/src/stories/grouping.stories.ts index 5e4de956e4..cc0792acae 100644 --- a/packages/components/grid/src/stories/grouping.stories.ts +++ b/packages/components/grid/src/stories/grouping.stories.ts @@ -280,3 +280,52 @@ export const CustomGroupHeader: Story = { `; } }; + +export const StickyColumnsWithCustomGroupHeader: Story = { + loaders: [async () => ({ students: (await getStudents()).students })], + render: (_, { loaded: { students } }) => { + const dataSource = new ArrayListDataSource(students as Student[], { + groupBy: 'school.id', + groupLabelPath: 'school.name' + }); + + const groupHeaderRenderer = (item: ListDataSourceGroupItem) => { + return html` + ${item.label} (${item.count}) + Add student + `; + }; + + return html` + +

+ This example shows grouped rows with a custom group header and sticky columns while + scrolling horizontally. +

+ + + + + + + + + + + `; + } +};