9

I'm trying to use CdkConnectedOverlay to display an overlay when a button is clicked. It's mostly working, but the overlay is not re-positioning on scroll. I'm sure there's something small I'm missing, but I can't for the life of me figure it out.

I'm using Angular 7.2.8 and Angular CDK 7.3.3

Thought it might be related to missing cdk styles (similar to this), but I imported those; if the styles were missing, I would expect it not to display correctly in the first place. Mine just doesn't re-position on scroll.

Template:

<button
  (click)="isOpen = !isOpen"
  cdkOverlayOrigin
  #trigger="cdkOverlayOrigin"
>Show</button>

<ng-template
  cdkConnectedOverlay
  [cdkConnectedOverlayOrigin]="trigger"
  [cdkConnectedOverlayOpen]="isOpen"
>
 Popover content
</ng-template>

Component:

@Component ( {
  selector: 'app-popover',
  templateUrl: './popover.component.html',
  styleUrls: [ './popover.component.css' ],
  changeDetection: ChangeDetectionStrategy.OnPush,
} )
export class PopoverComponent {
  isOpen: boolean = false;
}

And a Plunkr showing the issue: https://stackblitz.com/edit/angular-7-popover

Update:

The scroll re-positioning issue only occurs when the popover is in an element that overflows its parent with overflow: auto. If the page is overflowing, then it works fine. You can see this behavior with the following template

<div style="height: 300px; overflow-y: auto">
  <!-- Scroll re-positioning does not work when scrolling in here -->
  <div style="height: 100px"></div>
  <app-popover>
    Popover content
  </app-popover>
  <div style="height: 400px"></div>
</div>

<div style="height: 100px;"></div>
<!-- Scroll re-positioning works when scrolling here -->
<app-popover>
  Popover content
</app-popover>
<div style="height: 1200px;"></div>

I've also updated the stackblitz to show this issue.

Asad Ali Choudhry
  • 4,985
  • 4
  • 31
  • 36
cory
  • 93
  • 1
  • 5

1 Answers1

20

cdk documentation is not easy to understand sometimes and there are hidden gems all over it :)

here it states that;

Each strategy will typically inject ScrollDispatcher (from @angular/cdk/scrolling) to be notified of when scrolling takes place.

what i understand from this sentence is thatOverlay repositions itself in response to events from ScrollDispatcher. so, where do those scroll events come from?

unfortunately there isn't any information about this in docs. So i took a look in the codes and found this

  /** Sets up the global scroll listeners. */
  private _addGlobalListener() {
    this._globalSubscription = this._ngZone.runOutsideAngular(() => {
      return fromEvent(window.document, 'scroll').subscribe(() => this._scrolled.next());
    });
}

which means ScrollDispatcher listens for scroll events on window by default.

in your case above it responds to events when window is scrolled but not an inner container. this conforms to information we gathered so far.

here we can make the conclusion that Overlay is not notified of scroll events that take place within the inner container and all we need to do is register inner container with ScrollDispatcher

at this point cdkScrollable directive comes into rescue and placing cdkScrollable on the scrolling inner container solves the problem.

<div cdkScrollable style="height: 300px; overflow-y: auto">

here is a working demo

ysf
  • 4,634
  • 3
  • 27
  • 29
  • 2
    Ah, that makes sense now! I also came across this issue on Github about the same time you posted this answer, which is saying essentially the same thing. https://github.com/angular/components/issues/6157 – cory Jun 28 '19 at 20:05
  • 4
    For those who need it, in order to have `cdkScrollable` work, you will need to import ScrollingModule. `import {ScrollingModule} from '@angular/cdk/scrolling';` ..... `imports: [ ....., ScrollingModule]` – vipa Oct 27 '19 at 12:47