My app is using Angular Material Components as a component library.
To supplement for layout I am importing Bootstrap 5 utilities.
The problem is that when my scrollToTop() function is called in my List Wrapper Class it pushes the div row in the Shell Template above it up by about 5px. It is enough that the buttons inside the first row have no margin with the my app header (which I am not going to add code for).
When I comment out element.scrollIntoView(true); The problem does not occur.
The scrollToTop() function is triggered as soon as the component is rendered due to the signal effect that is instantiated in the constructor.
Please note the CSS in Shell Component.
The application requirement is that the user should not have to use a scroll wheel on the list. It should keep the selected item in the view and at least one other item above the selected, if index is not zero, so the user can click to go up or down.
List Wrapper Class:
import { Component, computed, effect, ElementRef, inject, viewChildren } from '@angular/core';
import { MyStore } from '../../../stores/my-store.store';
@Component({
selector: 'app-my-list-wrapper',
standalone: true,
templateUrl: './my-list-wrapper.component.html'
})
export class MyListWrapperComponent {
listItems = viewChildren<ElementRef>('orderedListItem');
myStore = inject(MyStore);
readonly items = computed(() =>
this.myStore
.getItems()
.map((result) => result.model)
);
selected= computed(
() => this.myStore.selected()?.index - 1
);
constructor() {
effect(() => {
const index = this.selected();
if (index >= 0 && !!this.listItems()) {
setTimeout(() => this.scrollToTop(index), 0); // Ensure DOM is updated before scrolling
}
});
}
setCurrent(index: number) {
if (step < this.myStore.items().length && step >= 0) {
this.myStore.setSelected(
this.myStore.items()[index]
);
this.scrollToTop(index);
}
}
scrollToTop(index: number) {
if (index > 0) index--;
const element = this.listItems().at(index)?.nativeElement;
if (element) {
element.scrollIntoView(true);
} else {
console.error('List item not found at index:', index);
}
}
}
List Wrapper Template:
<div class="row">
<div class="col overflow-y-hidden">
@for (item of items(); track item; let index = $index) {
<div class="ordered-list-item" (click)="setCurrent(index)" #orderedListItem>
<div
class="ordered-list-item-content {{
index === selected() ? 'content-active' : ''
}}"
>
<div mat-card-avatar class="ordered-list-item-content-icon">
{{ index + 1 }}
</div>
<div class="ordered-list-item-content-title">
{{ item }}
</div>
</div>
</div>
}
</div>
</div>
Shell Class:
import { Component, OnInit, computed, inject } from '@angular/core';
import { MyStore } from '../../stores/my-store.store';
import { ActivatedRoute, Router } from '@angular/router';
import { MatDividerModule } from '@angular/material/divider';
import { MyOtherStore } from '../../stores/my-other-store.store';
import { MyListWrapperComponent } from './list-wrapper/list-wrapper.component';
import { MyItemDetailsComponent } from './item-details/item-details.component';
@Component({
selector: 'app-shell',
standalone: true,
imports: [
MatDividerModule,
MyListWrapperComponent,
MyItemDetailsComponent
],
templateUrl: './shell.component.html',
styleUrl: './shell.component.scss',
})
export class ShellComponent implements OnInit {
readonly route = inject(ActivatedRoute);
readonly router = inject(Router);
myStore = inject(MyStore);
myOtherStore = inject(MyOtherStore);
readonly selectedDetails = computed(() =>
this.myStore
.items()
.flatMap((result) => result.details)
);
selected = this.myOtherStore.selected;
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id') ?? 'error';
this.myStore.fetchItems(id);
}
}
Shell Template:
<div class="container-fluid overflow-hidden">
<div class="row">
<div class="col">
<div class="row align-items-center">
<div class="col-auto">
<button ... />
</div>
<div class="col">
<p>
{{ title }}
</p>
</div>
<div class="col-auto">
<button ... />
</div>
</div>
<div class="row">
<div class="col">
@if (selected().title != '') {
<h1>
{{ selected().title }}
</h1>
}
@else {
<h1>
No Selected
</h1>
}
</div>
</div>
</div>
</div>
<div class="row shell-body">
<div class="col-3 hl-container">
@if (selected().title != '') {
<app-my-list-wrapper />
}
@else {
<div class="w-100 h-100 bg-primary text-center">
<h2>My List Wrapper Goes Here</h2>
</div>
}
</div>
<div class="col-9 shell-body">
<app-item-details />
</div>
</div>
</div>
Shell CSS:
.hl-container {
height: calc(
90vh - 50px
); // 10vh is the height of the shell header, 50px is the height of the application header
overflow-y: scroll;
}
.hl-container::-webkit-scrollbar {
display: none;
}
What I already tried:
I was getting the selected element from a div with #scrollContainer for these.
scrollToTop(index: number) {
const itemElement = this.scrollContainer().nativeElement.querySelector(`#ordered-item-${index}`);
if (itemElement) {
const container = this.scrollContainer().nativeElement;
const itemOffsetTop = itemElement.offsetTop;
container.scrollTop = itemOffsetTop;
} else {
console.error('List item not found at index:', index);
}
}
scrollToTop(index: number) {
const itemElement = this.scrollContainer().nativeElement.querySelector(`#ordered-item-${index}`);
if (itemElement) {
const container = this.scrollContainer().nativeElement;
const itemOffsetTop = itemElement.offsetTop;
container.scrollTop = itemOffsetTop - container.offsetTop;
} else {
console.error('List item not found at index:', index);
}
}
scrollToTop(index: number) {
if (index > 0) index--;
const itemElement = this.scrollContainer().nativeElement.querySelector(`#ordered-item-${index}`);
if (itemElement) {
const container = this.scrollContainer().nativeElement;
const itemRect = itemElement.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const offset = itemRect.top - containerRect.top + container.scrollTop;
container.scrollTo({ top: offset, behavior: 'smooth' });
} else {
console.error('List item not found at index:', index);
}
}
scrollToTop(index: number) {
if (index > 0) index--;
const itemElement = this.scrollContainer().nativeElement.querySelector(`#ordered-item-${index}`);
if (itemElement) {
const container = this.scrollContainer().nativeElement;
const itemOffsetTop = itemElement.offsetTop;
container.scrollTo({ top: itemOffsetTop, behavior: 'smooth' });
} else {
console.error('List item not found at index:', index);
}
}
Hoping someone here can at least explain what is happening with element.scrollIntoView(true) if not provide a fix. That is the only thing that makes the selected element actually scroll.