Angular7中ngbPopover排除滚动条的外部点击自动关闭实现
解决Angular 7中ngbPopover滚动条点击不关闭、外部点击关闭的问题
问题分析
需要实现:打开日期范围选择的popover时,点击页面/容器滚动条不关闭popover,点击popover外其他区域自动关闭;同时存在子组件无法监听父页面滚动、原@HostListener监听滚动无效的问题。
解决方案
通过自定义点击事件判断+Renderer2监听全局滚动/点击来实现需求,具体代码如下:
1. 组件TS代码修改
import { Component, OnInit, Renderer2, ViewChild, OnDestroy } from '@angular/core'; import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'app-date-range', templateUrl: './date-range.component.html', styleUrls: ['./date-range.component.css'] }) export class DateRangeComponent implements OnInit, OnDestroy { @ViewChild('popOver') popOver!: NgbPopover; defaultPlacement = 'bottom'; private scrollListener!: () => void; private clickListener!: () => void; constructor(private renderer: Renderer2) { } ngOnInit(): void { // 监听全局滚动事件(覆盖父页面/容器滚动) this.scrollListener = this.renderer.listen('window', 'scroll', () => { // 滚动时不执行关闭逻辑,仅做日志或其他操作 console.log('页面/容器滚动:', window.scrollX, window.scrollY); }); // 监听全局点击事件,自定义控制popover关闭 this.clickListener = this.renderer.listen('document', 'click', (event: MouseEvent) => { // popover未打开时直接返回 if (!this.popOver.isOpen()) return; // 判断点击是否在popover内部 const popoverElement = document.querySelector('.ngb-popover-window'); const isInsidePopover = popoverElement?.contains(event.target as Node); // 判断点击是否在滚动条区域 const isOnScrollbar = this.isClickOnScrollbar(event); // 既不在popover内,也不是滚动条点击时,关闭popover if (!isInsidePopover && !isOnScrollbar) { this.popOver.close(); } }); } ngOnDestroy(): void { // 解绑监听,避免内存泄漏 this.scrollListener(); this.clickListener(); } // 判断点击位置是否为滚动条 private isClickOnScrollbar(event: MouseEvent): boolean { // 检查页面垂直滚动条 const hasVScroll = document.documentElement.clientWidth < window.innerWidth; if (hasVScroll && event.clientX >= document.documentElement.clientWidth) { return true; } // 检查页面水平滚动条 const hasHScroll = document.documentElement.clientHeight < window.innerHeight; if (hasHScroll && event.clientY >= document.documentElement.clientHeight) { return true; } // 检查点击元素的父滚动容器的滚动条 const target = event.target as HTMLElement; const scrollParent = this.findScrollableParent(target); if (scrollParent) { const rect = scrollParent.getBoundingClientRect(); // 垂直滚动条判断 if (scrollParent.clientWidth < scrollParent.offsetWidth && event.clientX >= rect.left + scrollParent.clientWidth) { return true; } // 水平滚动条判断 if (scrollParent.clientHeight < scrollParent.offsetHeight && event.clientY >= rect.top + scrollParent.clientHeight) { return true; } } return false; } // 查找元素的滚动父容器 private findScrollableParent(element: HTMLElement): HTMLElement | null { let parent = element.parentElement; while (parent) { const style = window.getComputedStyle(parent); const isScrollable = style.overflowY === 'auto' || style.overflowY === 'scroll' || style.overflowX === 'auto' || style.overflowX === 'scroll'; if (isScrollable) return parent; parent = parent.parentElement; } return null; } }
2. 组件HTML代码修改
<div> <ng-template #popContent> <!-- 这里放置你的日期范围选择器内容 --> <div class="date-range-content"> 日期范围选择区域 <!-- 比如开始日期、结束日期输入框等 --> </div> </ng-template> <button type="button" class="btn btn-date-selector" [placement]="defaultPlacement" [ngbPopover]="popContent" [autoClose]="false" data-container="body" #popOver="ngbPopover"> 选择日期范围 </button> </div>
关键逻辑说明
滚动监听修复:
- 放弃
@HostListener,改用Renderer2直接监听window的滚动事件,确保子组件能捕获到父页面/任何容器的滚动操作。 - 在组件销毁时解绑监听,避免内存泄漏。
- 放弃
自定义关闭逻辑:
- 将
autoClose设为false,禁用ngbPopover的默认关闭行为,完全由自定义逻辑控制。 - 通过
document.querySelector('.ngb-popover-window')判断点击是否在popover内部。 - 编写
isClickOnScrollbar函数,分别判断页面滚动条和父容器滚动条的点击场景,确保点击滚动条时不关闭popover。
- 将
滚动父容器检测:
findScrollableParent函数递归查找点击元素的滚动父容器,覆盖局部滚动容器的滚动条点击判断场景。
内容的提问来源于stack exchange,提问作者Satyam thassu
相关产品推荐
相关产品推荐

