ngx-bootstrap Tooltip 延迟显示(delay)详解:从 use-case 规格到源码实现

原创2026-10-12 11:42:231,363 阅读
文章标签:UI组件前端

ngx-bootstrap Tooltip 延迟显示(delay)详解:从 use-case 规格到源码实现

ngx-bootstrap 的 Tooltip 组件支持通过 delay 输入属性控制提示框的延迟显示。本文以仓库中 Tooltip with delay 用例规格 为核心骨架,结合源码、Demo 与单元测试,完整讲解 [delay]="500" 的用法、默认配置、底层定时器实现以及鼠标移出取消延迟的机制,帮助你在 Angular 应用中精准实现"悬停 0.5 秒后浮现提示、移开立即消失"的交互效果。

一、用例规格:Tooltip with delay 的预期行为

仓库中的规格文档 docs/spec/tooltip/tooltip.examples.tooltip-with-delay.use-case.md 以 Use Case 的形式定义了该功能的验收标准,其核心场景如下:

  • 主要参与者(Primary Actor):User(最终用户)
  • 适用范围(Scope):Ngx-bootstrap DEMO,兼容 Bootstrap 3 与 4
  • 目标(Goal):验证 Tooltip 的延迟显示行为符合预期

主成功场景(Main success scenario):

  1. 用户打开 Tooltip demo 页面;
  2. 用户点击 "Tooltip with delay" 子菜单(或滚动到该子菜单,见变体 2*);
  3. 用户看到按钮 "Tooltip with 0.5sec delay";
  4. 用户将鼠标悬停在按钮上时,tooltip-container 在 0.5 秒延迟后出现在按钮上方;
  5. 用户将鼠标移出按钮时,tooltip-container 立即消失;
  6. 模板源码应写成 [delay]="500"。

变体(Variations): 2*. 用户通过滚动而非点击的方式到达 "Tooltip with delay" 子菜单。

这份规格明确了两个关键验收点:显示方向上的 0.5 秒延迟,以及隐藏方向上无延迟、立即消失。这与 demo 提示文案 "Hold on cursor above button for 0,5 second or more to see delayed tooltip" 完全一致。

二、Demo 落地:[delay]="500" 的真实模板

规格第 6 条明确要求模板使用 <a href="https://link.gitcode.com/i/85bed0f069b5eb5e1340cb0ee241450e" target="_blank">delay]="500"。仓库中对应的 Demo 组件位于 [delay.ts,其模板 delay.html 内容如下:

<button type="button" class="btn btn-primary"
        tooltip="Vivamus sagittis lacus vel augue laoreet rutrum faucibus." [delay]="500">
  Tooltip with 0.5sec delay
</button>

关键点拆解:

  • tooltip="...":以字符串形式传入提示内容(相当于 [tooltip]="'...'");
  • [delay]="500":设置延迟为 500 毫秒,即 0.5 秒;
  • class="btn btn-primary":按钮使用 Bootstrap 按钮样式,适配 Bootstrap 3/4。

在 tooltip-section.list.ts 中,该 Demo 被注册到文档站点,锚点为 tooltip-delay,说明文案为 "Hold on cursor above button for 0,5 second or more to see delayed tooltip"。

安装前提:使用 Tooltip 指令前需引入模块。按 usage.md 的说明,可通过 TooltipModule 以模块方式导入:

import { TooltipModule } from 'ngx-bootstrap/tooltip';

@NgModule({
  imports: [TooltipModule, ...]
})
export class AppModule() {}

也可使用 Angular 16+ 的 standalone 写法:

@Component({
  standalone: true,
  imports: [TooltipModule, ...]
})
export class AppComponent() {}

此外文档站点还提供了快捷安装命令:ng add ngx-bootstrap --component tooltip。

三、delay 参数与全局默认配置

delay 是 TooltipDirective 暴露的核心输入属性。在 tooltip.directive.ts 中定义如下:

/**
 * Delay before showing the tooltip
 */
readonly delay = input(this._config.delay);

即:未显式指定时,delay 的值取自全局配置 TooltipConfig.delay。该默认值在 tooltip.config.ts 中定义:

@Injectable({ providedIn: 'root' })
export class TooltipConfig {
  /** sets disable adaptive position */
  adaptivePosition = true;
  /** tooltip placement, supported positions: 'top', 'bottom', 'left', 'right' */
  placement = 'top';
  /** array of event names which triggers tooltip opening */
  triggers = 'hover focus';
  /** a selector specifying the element the tooltip should be appended to. */
  container?: string;
  /** delay before showing the tooltip */
  delay = 0;
}

由此可以得到完整的参数行为矩阵:

配置方式 代码示例 显示延迟
指令级输入(推荐) [delay]="500" 500ms,仅作用于当前元素
全局配置默认值 TooltipConfig.delay = 0 0ms(默认即时显示)
未配置 不写 delay 0ms,即时显示

需要说明的是:TooltipConfig 是 providedIn: 'root' 的服务,你可以在应用启动时覆盖其 delay 值来全局调整默认延迟;而 [delay] 输入则用于局部覆盖。两者组合时,指令输入优先。

另外在源码中还存在一个已废弃的输入 tooltipPopupDelay(tooltip.directive.ts),在 show() 中通过 this.tooltipPopupDelay() ?? this.delay() 兼容读取,若被使用会打印警告 "tooltipPopupDelay is deprecated, use delay instead"。新代码请一律使用 delay。

四、源码实现:0.5 秒延迟究竟如何生效

要理解 <a href="https://link.gitcode.com/i/b5f9024976d994c9f4e32de53916ba14" target="_blank">delay]="500" 的完整生命周期,需要阅读 TooltipDirective.show() 的实现([tooltip.directive.ts)。

4.1 事件监听入口

在 ngOnInit() 中,指令通过 ComponentLoader.listen 注册触发事件:

this._tooltip.listen({
  triggers: this.triggers(),
  show: () => this.show()
});

默认 triggers = 'hover focus',因此鼠标悬停(hover)与聚焦(focus)都会进入 show()。事件的实际绑定由 component-loader.class.ts 中的 listenToTriggersV2 完成。

4.2 延迟分支:RxJS timer

show() 方法中与延迟相关的核心逻辑如下:

if (delayValue) {
  if (this._delaySubscription) {
    this._delaySubscription.unsubscribe();
  }

  this._delaySubscription = timer(delayValue).subscribe(() => {
    showTooltip();
    cancelDelayedTooltipShowing();
  });

  if (triggersValue) {
    parseTriggers(triggersValue).forEach((trigger: Trigger) => {
      if (!trigger.close) {
        return;
      }
      this._tooltipCancelShowFn = this._renderer.listen(this._elementRef.nativeElement, trigger.close, () => {
        this._delaySubscription?.unsubscribe();
        cancelDelayedTooltipShowing();
      });
    });
  }
} else {
  showTooltip();
}

机制拆解:

  1. delayValue 非 0 时走延迟分支:使用 RxJS 的 timer(delayValue) 创建一次性定时器,到点后执行 showTooltip();
  2. delayValue 为 0(默认)时即时显示:直接调用 showTooltip(),无任何等待;
  3. "移出立即消失"的实现:当设置了 [delay]="500" 时,指令会解析 triggers(如 hover focus),找出其中的关闭事件(如 mouseleave/blur),并为其注册监听:一旦关闭事件发生,立即 unsubscribe() 取消尚未触发的定时器——这就是规格第 5 条"移出鼠标后 tooltip 立即消失"的底层保证。即使 timer 已触发、tooltip 已显示,随后的 hide() 也会将其隐藏,其中还包含了 tooltipFadeDuration() 毫秒的淡出过渡(默认 150ms)。

4.3 真正的"显示"动作

showTooltip() 内部通过 ComponentLoader 完成容器挂载与定位:

this._tooltip
  .attach(TooltipContainerComponent)
  .to(containerValue)
  .position({ attachment: placementValue })
  .show({
    content: tooltipValue,
    placement: placementValue,
    containerClass: this.containerClass(),
    id: `tooltip-${this.tooltipId}`
  });

这里会动态创建 tooltip-container.component.ts 中的 bs-tooltip-container 组件,默认以 placement(默认 top,即规格中"出现在按钮上方")定位,并通过 component-loader.class.ts 的 PositioningService 完成位置计算。

4.4 隐藏路径

hide()(tooltip.directive.ts)首先清除可能残留的延迟定时器,然后通过 _tooltip.hide() 移除容器。规格第 5 条要求"移出即消失",对应触发链为:mouseleave → 取消/触发隐藏 → ComponentLoader.hide() → 从 DOM 移除 tooltip-container。

五、测试验证:单元测试如何锁定行为

仓库在 tooltip.directive.spec.ts 中对该行为有直接的自动化验证,其中与延迟相关的关键用例:

it('tooltip should be displayed after specified delay', fakeAsync(() => {
  const element: HTMLElement = fixture.debugElement.nativeElement;
  const tooltipElement = element.querySelector('#test-tooltip1') as HTMLElement;
  context.delay = 1000;
  fixture.detectChanges();
  tooltipElement.focus();
  fixture.detectChanges();
  tick(1100);
  fixture.detectChanges();
  expect(element.querySelector('.tooltip-inner')).not.toBeNull();
}));

该测试的验证逻辑:

  • 将 delay 动态设置为 1000(与 Demo 的 500 同理,只是数值不同);
  • 触发 focus 事件进入 show();
  • 使用 fakeAsync + tick(1100) 模拟时间推进 1100ms;
  • 断言 .tooltip-inner 已出现在 DOM 中。

另一个用例 tooltip should be displayed by mouseenter event 同样先 fireEvent(tooltipElement, 'mouseenter'),再 tick(context.delay) 推进时间后断言显示,覆盖了 hover 触发路径。测试组件模板中的输入元素使用了 [delay] = "delay" 与 triggers="focus mouseenter",与生产 Demo 的用法一致。

这套测试从侧面印证了规格第 4 条"悬停后延迟出现"的预期:延迟期间 tooltip 尚未挂载,时间推进越过 delay 值后才出现。

六、最佳实践与边界情况

结合规格与源码,在使用 [delay] 时值得注意以下几点:

  1. 单位是毫秒:[delay]="500" 表示 0.5 秒,[delay]="1000" 表示 1 秒;传 0 或不传则即时显示。
  2. 延迟只作用于"显示"方向:规格明确"移出即消失",源码中关闭事件会直接取消待触发的定时器,隐藏不受 delay 约束。
  3. 与 triggers 组合使用:延迟取消机制依赖关闭事件(如 mouseleave)的注册,因此建议保留默认 triggers="hover focus";若自定义 triggers,需确保包含对应的关闭事件,否则延迟期间的 tooltip 无法被提前取消。
  4. isDisabled 与内容为空时不会显示:show() 开头会检查 this.isOpen || isDisabledValue || this._delayTimeoutId || !tooltipValue,命中任一条件直接 return(tooltip.directive.ts)。
  5. 重复进入的防抖:show() 在已存在 _delaySubscription 时会先 unsubscribe() 再重建定时器,避免连续 hover 造成多次叠加触发。
  6. 资源清理:ngOnDestroy() 中会 unsubscribe 延迟订阅并 dispose loader(tooltip.directive.ts),组件销毁时无需手动处理。

七、小结

"Tooltip with delay" 是 ngx-bootstrap Tooltip 组件中一个轻量但完整的功能切片:从 use-case 规格 定义的"悬停 0.5 秒延迟出现、移出立即消失",到 Demo 模板 中一行 <a href="https://link.gitcode.com/i/18fe85e2e146a3d2f8ebd320ddc2a1aa" target="_blank">delay]="500" 的声明式用法,再到 [TooltipDirective 中 timer + 关闭事件取消的底层实现,以及 单元测试 的自动化锁定,形成了完整可追溯的闭环。开发者只需一个输入属性即可获得与 Bootstrap 3/4 兼容、且具备源码级可靠性的延迟提示交互,并可借助 TooltipConfig.delay 统一调整全局默认值。

登录后查看全文
ngx-bootstrap