ngx-bootstrap Tooltip 延迟显示(delay)详解:从 use-case 规格到源码实现
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):
- 用户打开 Tooltip demo 页面;
- 用户点击 "Tooltip with delay" 子菜单(或滚动到该子菜单,见变体 2*);
- 用户看到按钮 "Tooltip with 0.5sec delay";
- 用户将鼠标悬停在按钮上时,
tooltip-container在 0.5 秒延迟后出现在按钮上方; - 用户将鼠标移出按钮时,
tooltip-container立即消失; - 模板源码应写成
[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();
}
机制拆解:
delayValue非 0 时走延迟分支:使用 RxJS 的timer(delayValue)创建一次性定时器,到点后执行showTooltip();delayValue为 0(默认)时即时显示:直接调用showTooltip(),无任何等待;- "移出立即消失"的实现:当设置了
[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] 时值得注意以下几点:
- 单位是毫秒:
[delay]="500"表示 0.5 秒,[delay]="1000"表示 1 秒;传0或不传则即时显示。 - 延迟只作用于"显示"方向:规格明确"移出即消失",源码中关闭事件会直接取消待触发的定时器,隐藏不受
delay约束。 - 与
triggers组合使用:延迟取消机制依赖关闭事件(如mouseleave)的注册,因此建议保留默认triggers="hover focus";若自定义triggers,需确保包含对应的关闭事件,否则延迟期间的 tooltip 无法被提前取消。 isDisabled与内容为空时不会显示:show()开头会检查this.isOpen || isDisabledValue || this._delayTimeoutId || !tooltipValue,命中任一条件直接 return(tooltip.directive.ts)。- 重复进入的防抖:
show()在已存在_delaySubscription时会先unsubscribe()再重建定时器,避免连续 hover 造成多次叠加触发。 - 资源清理:
ngOnDestroy()中会unsubscribe延迟订阅并disposeloader(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 统一调整全局默认值。