https://stackblitz.com/edit/angular-ebzjz5?file=src%2Fapp%2Fexpansion-overview-example.html
Steps to reproduce:
The view autoscrolls while draging the item to the bottom of the screen.
It doesn't autoscoll.
It does if i either delete the tab-group or if i delete the max-height of 100vh of the sidenav-content.
Please take a look at the stackblitz.
A possible workaround could be to delete the height of 100vh and set items in the sidenav to a fixed position so they are always visible. Not testet though.
i think this can be handled from the application end,
https://stackblitz.com/edit/angular-ebzjz5-pe3mev
Thanks for the suggestion.
However, your solution seems very hacky to me and the scrolling isn't smooth.
Edit: Just implemented my suggested workaround. Works fine but involves major changes in the app layout structure. Thats not so nice.
To some degree, this doesn't have anything to do with the sidenav or tab components. The main issue with your demo is you're trying to scroll an element that is not the CdkDropList element, nor the window.
One solution is to manually register the sidenav content as a scrollable parent of the droplist - something like this:
@ViewChild(MatSidenavContent , {read: ElementRef}) sideNavContentElement: ElementRef;
@ViewChild(CdkDropList) dropList: CdkDropList;
ngAfterViewInit() {
// register the sidenav content as a scrollable parent element
if (this.sideNavContentElement) {
this.dropList._dropListRef.withScrollableParents([this.sideNavContentElement.nativeElement])
}
}
https://stackblitz.com/edit/angular-ebzjz5-ftwbai?file=src%2Fapp%2Fexpansion-overview-example.ts
If you have a statically-defined number of droplists/sidenav contents then this is fine, but it's not a very scalable solution.
This pull request added the ability to define scrollable containers other than the droplist and window by adding a cdkScrollable directive to elements. Theoretically, you would be able to do this (adding cdkScrollable):
<mat-sidenav-content style="max-height: 100vh" cdkScrollable>
...
</mat-sidenav-content>
and the drop list would "automatically" pick up this element as a scrollable parent. Unfortunately, this doesn't work with your use case, because the drop list is projected (multiple levels) inside the sidenav content and registering the cdkScrollables happens inside the ngAfterContentInit() lifecycle hook. My knowledge on this is a little fuzzy, but the gist of the problem is that during ngAfterContentInit(), you cannot traverse up DOM elements across projection boundaries. Meaning this method of scroll-dispatcher.ts:
/** Returns all registered Scrollables that contain the provided element. */
getAncestorScrollContainers(elementRef: ElementRef): CdkScrollable[] {
const scrollingContainers: CdkScrollable[] = [];
this.scrollContainers.forEach((_subscription: Subscription, scrollable: CdkScrollable) => {
if (this._scrollableContainsElement(scrollable, elementRef)) {
scrollingContainers.push(scrollable);
}
});
return scrollingContainers;
}
will return an empty list of scrollContainers, despite your HTML actually having a cdkScrollable as an ancestor of the drop list.
You can find ways to get around this...for example, if not using Ivy, you can override ngAfterContentInit() and wrap the withScrollableParents() call inside a setTimeout to defer execution until the view is fully initialized:
CdkDropList.prototype.ngAfterViewInit = function(){
// @breaking-change 11.0.0 Remove null check for _scrollDispatcher once it's required.
if (this._scrollDispatcher) {
setTimeout(() => {const scrollableParents = this._scrollDispatcher
.getAncestorScrollContainers(this.element)
.map(scrollable => scrollable.getElementRef().nativeElement);
this._dropListRef.withScrollableParents(scrollableParents);
})
}
}
But really, I think the best solution is to use ngAfterViewInit() instead of ngAfterContentInit().
CdkDropList.prototype.ngAfterViewInit = function(){
// @breaking-change 11.0.0 Remove null check for _scrollDispatcher once it's required.
if (this._scrollDispatcher) {
const scrollableParents = this._scrollDispatcher
.getAncestorScrollContainers(this.element)
.map(scrollable => scrollable.getElementRef().nativeElement);
this._dropListRef.withScrollableParents(scrollableParents);
}
}
Using ngAfterViewInit() allows the cdkScrollable to be picked up across projection boundaries.
Example here:
https://stackblitz.com/edit/angular-ebzjz5-3csse8?file=src/app/expansion-overview-example.ts
(Notice how commenting out the prototype override does not allow the auto-scrolling, despite cdkScrollable being added to the <mat-sidenav-content>)
@crisbeto thoughts on switching the CdkDropList scrollableParents logic from ngAfterContentInit() to ngAfterViewInit()?
Thanks. In which version will this fix be released?
It'll be in the next patch release.
Released with Material 9.2.1
Works very well, thank you.
This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.
Read more about our automatic conversation locking policy.
_This action has been performed automatically by a bot._
Most helpful comment
To some degree, this doesn't have anything to do with the sidenav or tab components. The main issue with your demo is you're trying to scroll an element that is not the
CdkDropListelement, nor thewindow.One solution is to manually register the sidenav content as a scrollable parent of the droplist - something like this:
https://stackblitz.com/edit/angular-ebzjz5-ftwbai?file=src%2Fapp%2Fexpansion-overview-example.ts
If you have a statically-defined number of droplists/sidenav contents then this is fine, but it's not a very scalable solution.
This pull request added the ability to define scrollable containers other than the droplist and window by adding a
cdkScrollabledirective to elements. Theoretically, you would be able to do this (addingcdkScrollable):and the drop list would "automatically" pick up this element as a scrollable parent. Unfortunately, this doesn't work with your use case, because the drop list is projected (multiple levels) inside the sidenav content and registering the cdkScrollables happens inside the
ngAfterContentInit()lifecycle hook. My knowledge on this is a little fuzzy, but the gist of the problem is that duringngAfterContentInit(), you cannot traverse up DOM elements across projection boundaries. Meaning this method of scroll-dispatcher.ts:will return an empty list of scrollContainers, despite your HTML actually having a
cdkScrollableas an ancestor of the drop list.You can find ways to get around this...for example, if not using Ivy, you can override
ngAfterContentInit()and wrap thewithScrollableParents()call inside asetTimeoutto defer execution until the view is fully initialized:But really, I think the best solution is to use
ngAfterViewInit()instead ofngAfterContentInit().Using
ngAfterViewInit()allows thecdkScrollableto be picked up across projection boundaries.Example here:
https://stackblitz.com/edit/angular-ebzjz5-3csse8?file=src/app/expansion-overview-example.ts
(Notice how commenting out the prototype override does not allow the auto-scrolling, despite
cdkScrollablebeing added to the<mat-sidenav-content>)@crisbeto thoughts on switching the
CdkDropListscrollableParents logic fromngAfterContentInit()tongAfterViewInit()?