| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293 |
- import { booleanAttribute, Component, effect, ElementRef, inject, input, signal, viewChild } from '@angular/core';
- import type { FloatPos } from '../../theme';
- import { FloatingPanelController } from '../../theme';
- export type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
- export type TooltipAlign = 'start' | 'center' | 'end';
- /**
- * 文字提示:用 ng-content 包裹触发元素,悬停/聚焦后延迟显示提示气泡。
- * align 控制 top/bottom 时的水平对齐。
- * autoFlip:当提示方向空间不足(如触发元素贴近视口顶部)时,自动翻转方向,避免被裁切。
- */
- @Component({
- selector: 'ui-tooltip',
- imports: [],
- templateUrl: './tooltip.html',
- styleUrl: './tooltip.scss',
- })
- export class UiTooltip {
- /** 提示文案 */
- readonly content = input('');
- /** 提示方向 */
- readonly position = input<TooltipPosition>('top');
- /** 水平对齐(top/bottom 时生效):start 靠左 / center 居中 / end 靠右 */
- readonly align = input<TooltipAlign>('center');
- /** 悬停后显示的延迟(毫秒) */
- readonly delay = input(200);
- /** 空间不足时自动翻转方向 */
- readonly autoFlip = input(true, { transform: booleanAttribute });
- protected readonly visible = signal(false);
- /** 面板 fixed 定位坐标(null=未定位,面板先隐藏避免闪位) */
- protected readonly panelPos = signal<FloatPos | null>(null);
- /** 实际渲染方向(自动翻转后) */
- protected readonly effectivePosition = signal<TooltipPosition>('top');
- private readonly host = inject(ElementRef<HTMLElement>);
- private readonly panelEl = viewChild<ElementRef<HTMLElement>>('panel');
- private controller: FloatingPanelController | null = null;
- private timer: ReturnType<typeof setTimeout> | undefined;
- protected readonly tooltipClass =
- 'pointer-events-none fixed z-[var(--z-popover)] whitespace-nowrap rounded-md bg-ink px-2.5 py-1.5 text-xs text-inverse shadow-pop animate-scale-in';
- constructor() {
- effect(() => {
- if (this.visible()) {
- this.panelPos.set(null);
- setTimeout(() => this.attachPanel(), 0);
- } else {
- this.controller?.destroy();
- this.controller = null;
- this.panelPos.set(null);
- }
- });
- }
- private attachPanel(): void {
- if (!this.visible()) return;
- const panel = this.panelEl()?.nativeElement;
- if (!panel) return;
- this.controller?.destroy();
- this.controller = new FloatingPanelController(this.host.nativeElement, panel, {
- side: this.effectivePosition(),
- align: this.align() === 'end' ? 'end' : 'start',
- centered: this.align() === 'center',
- gap: 8,
- });
- this.panelPos.set(this.controller.getPos());
- }
- show(): void {
- clearTimeout(this.timer);
- // 自动翻转:方向空间不足时反向,保证气泡不被视口边缘裁切
- let eff = this.position();
- if (this.autoFlip()) {
- const rect = this.host.nativeElement.getBoundingClientRect();
- const margin = 44;
- if (eff === 'top' && rect.top < margin) eff = 'bottom';
- else if (eff === 'bottom' && window.innerHeight - rect.bottom < margin) eff = 'top';
- else if (eff === 'left' && rect.left < margin) eff = 'right';
- else if (eff === 'right' && window.innerWidth - rect.right < margin) eff = 'left';
- }
- this.effectivePosition.set(eff);
- this.timer = setTimeout(() => this.visible.set(true), Math.max(0, this.delay()));
- }
- hide(): void {
- clearTimeout(this.timer);
- this.visible.set(false);
- }
- }
|