Add section headers to the virtual list
Angular · Angular · intermediate · modification
Adds sticky section headers to the virtual list. Rows are now either items (48px) or shorter section headers (32px), so totalHeight sums per-kind heights and firstVisibleIndex branches on the row kind while walking down to the first partially-visible row. Verified against a grouped contacts list.
firstVisibleIndex is a signal computed re-read on every scroll event to drive the rendered window; if it doesn't return, change detection never completes and the tab freezes.
Requirements
- firstVisibleIndex returns the 0-based index of the first row at least partially inside the viewport, by walking rows top-down and consuming each row that sits fully above the viewport top.
- Rows come in two heights: an item is ITEM_HEIGHT (48px), a section header is HEADER_HEIGHT (32px). rows is a non-empty array; each row is a header or an item.
- scrollTop is an integer pixel offset the scroll container clamps to [0, totalHeight - viewportHeight], so it never points past the content.
- The computed must terminate and return an index for every valid scrollTop, including offsets whose viewport top falls inside a section header.
Files touched
- src/app/contacts/virtual-list.component.ts
--- src/app/contacts/virtual-list.component.ts
id: number;
label: string;
+ kind: 'header' | 'item';
}
-const ROW_HEIGHT = 48;
+const ITEM_HEIGHT = 48;
+const HEADER_HEIGHT = 32;
@Component({
scrollTop = signal(0);
- totalHeight = computed(() => this.rows().length * ROW_HEIGHT);
+ totalHeight = computed(() =>
+ this.rows().reduce(
+ (sum, row) => sum + (row.kind === 'header' ? HEADER_HEIGHT : ITEM_HEIGHT),
+ 0,
+ ),
+ );
- // Index of the first row at least partially inside the viewport.
+ // Index of the first row at least partially inside the viewport. Walks the
+ // rows top-down, consuming each row that sits fully above the viewport top,
+ // and stops at the first row the viewport top falls inside.
firstVisibleIndex = computed(() => {
+ const rows = this.rows();
let remaining = this.scrollTop();
let index = 0;
while (remaining > 0) {