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('top'); /** 水平对齐(top/bottom 时生效):start 靠左 / center 居中 / end 靠右 */ readonly align = input('center'); /** 悬停后显示的延迟(毫秒) */ readonly delay = input(200); /** 空间不足时自动翻转方向 */ readonly autoFlip = input(true, { transform: booleanAttribute }); protected readonly visible = signal(false); /** 面板 fixed 定位坐标(null=未定位,面板先隐藏避免闪位) */ protected readonly panelPos = signal(null); /** 实际渲染方向(自动翻转后) */ protected readonly effectivePosition = signal('top'); private readonly host = inject(ElementRef); private readonly panelEl = viewChild>('panel'); private controller: FloatingPanelController | null = null; private timer: ReturnType | 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); } }