iter(g290): 抽出 trackShift 并接到下拉弹层

This commit is contained in:
HIsheR
2026-10-26 23:20:05 +08:00
parent da166af7c3
commit 9e72607952
6 changed files with 51 additions and 36 deletions
+11 -27
View File
@@ -1,6 +1,6 @@
import { effect, h, signal } from '@ui-arch/frame';
import type { TooltipProps } from './types.js';
import { flipSide } from './position.js';
import { flipSide, trackShift } from './position.js';
let seq = 0; // 全局自增序号:保证每个气泡 id 唯一,供 aria-describedby 关联
@@ -57,30 +57,10 @@ export function tooltip(props: TooltipProps, trigger: HTMLElement): HTMLElement
);
const root = h('span', { class: 'ua-tip-wrap' }, trigger, bubble);
// —— 出场期间的位置跟随:气泡本身是 wrap 内的绝对定位(滚动时跟着走),但翻转判定看的是视口,
// 页面或任意滚动容器一滚就可能变成「朝上放不下」——所以出场时挂监听,收起时摘掉(不在关闭态空转) ——
let bound = false; // 监听是否已挂:重复出场不重复绑定
let raf = 0; // rAF 节流句柄:滚动事件高频,一帧最多重算一次
// —— 出场期间的位置跟随:气泡是 wrap 内的绝对定位(滚动时跟着走),但翻转判定看的是视口,
// 页面或任意滚动容器一滚就可能变成「朝上放不下」——出场时挂监听,收起时摘掉 ——
let releaseShift: (() => void) | null = null; // 位移跟随的释放函数
const reflow = (): void => place(flipSide(trigger, bubble, props.placement ?? 'top'));
const onShift = (): void => {
if (raf) return;
raf = requestAnimationFrame(() => {
raf = 0;
reflow();
});
};
const bind = (): void => {
if (bound) return;
bound = true;
window.addEventListener('scroll', onShift, true); // 捕获阶段:滚动容器的 scroll 不冒泡,只有捕获能收到
window.addEventListener('resize', onShift);
};
const unbind = (): void => {
if (!bound) return;
bound = false;
window.removeEventListener('scroll', onShift, true);
window.removeEventListener('resize', onShift);
};
root.addEventListener('pointerenter', (e: PointerEvent) => { if (e.pointerType !== 'touch') show(); }); // 鼠标进入(触摸交给下面的点按)
root.addEventListener('pointerleave', (e: PointerEvent) => { if (e.pointerType !== 'touch') hide(); }); // 鼠标离开
@@ -90,9 +70,13 @@ export function tooltip(props: TooltipProps, trigger: HTMLElement): HTMLElement
root.addEventListener('pointerdown', (e: PointerEvent) => { if (e.pointerType === 'touch') show(); });
root.addEventListener('pointerup', (e: PointerEvent) => { if (e.pointerType === 'touch') hideLater(); });
effect(() => {
if (!open()) return unbind(); // 收起即摘监听:气泡不在,重算方位没有意义
place(flipSide(trigger, bubble, props.placement ?? 'top')); // 出场先算一次:空间不足自动翻转
bind(); // 出场期间跟着滚动 / 缩放重算:wrap 定位不会飘,但「视口里还放得下吗」会过期
if (!open()) { // 收起即摘监听:气泡不在,重算方位没有意义
releaseShift?.();
releaseShift = null;
return;
}
reflow(); // 出场先算一次:空间不足自动翻转
releaseShift ??= trackShift(reflow); // 出场期间跟着滚动 / 缩放重算:视口空间会变
});
root.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape') { e.stopPropagation(); hide(); } // Esc 立即收起且不影响外层弹层