From f88e57f1b33bd95a583673cb19abf20b1cf558a4 Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 23:20:55 +0800 Subject: [PATCH] =?UTF-8?q?iter(g291):=20=E4=B8=8B=E6=8B=89=E6=97=8F?= =?UTF-8?q?=E4=B8=83=E5=A4=84=E5=BC=B9=E5=B1=82=E6=8E=A5=E4=B8=8A=E6=BB=9A?= =?UTF-8?q?=E5=8A=A8=E8=B7=9F=E9=9A=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/skin/ui/src/autoComplete.ts | 13 +++++++++++-- packages/skin/ui/src/cascader.ts | 13 +++++++++++-- packages/skin/ui/src/colorPicker.ts | 13 +++++++++++-- packages/skin/ui/src/datePicker.ts | 13 +++++++++++-- packages/skin/ui/src/menu.ts | 13 +++++++++++-- packages/skin/ui/src/multiSelect.ts | 13 +++++++++++-- packages/skin/ui/src/timePicker.ts | 13 +++++++++++-- 7 files changed, 77 insertions(+), 14 deletions(-) diff --git a/packages/skin/ui/src/autoComplete.ts b/packages/skin/ui/src/autoComplete.ts index c3b17d2..7bc8e46 100644 --- a/packages/skin/ui/src/autoComplete.ts +++ b/packages/skin/ui/src/autoComplete.ts @@ -1,7 +1,7 @@ import { effect, h, signal } from '@ui-arch/frame'; import type { Signal } from '@ui-arch/frame'; import { bindOutside, pushOverlay } from './overlay.js'; -import { flip } from './position.js'; +import { flip, trackShift } from './position.js'; import type { AutoCompleteOption, AutoCompleteProps } from './types.js'; let seq = 0; // 实例序号:给弹层列表一个稳定 id,供 aria-controls 关联 @@ -108,6 +108,15 @@ export function autoComplete(props: AutoCompleteProps): HTMLElement { if (open()) releaseEsc ??= pushOverlay(() => { open(false); input.focus(); }); // 入栈逐层关闭 else { releaseEsc?.(); releaseEsc = null; } }); - effect(() => { if (open()) place(flip(input, pop, 'bottom')); }); // 空间不足自动翻转 + let releaseShift: (() => void) | null = null; // 展开期间的位移跟随释放函数(滚动 / 缩放重算翻转) +effect(() => { + if (!open()) { // 收起即摘监听:弹层不在,重算方位没有意义 + releaseShift?.(); + releaseShift = null; + return; + } + place(flip(input, pop, 'bottom')); // 打开先算一次:空间不足自动翻转 + releaseShift ??= trackShift(() => place(flip(input, pop, 'bottom'))); // 展开期间跟着滚动重算:视口空间会变 + }); // 空间不足自动翻转 return root; } diff --git a/packages/skin/ui/src/cascader.ts b/packages/skin/ui/src/cascader.ts index 37f4bcf..a3a169b 100644 --- a/packages/skin/ui/src/cascader.ts +++ b/packages/skin/ui/src/cascader.ts @@ -1,7 +1,7 @@ import { effect, h, signal } from '@ui-arch/frame'; import type { Signal } from '@ui-arch/frame'; import { bindOutside, pushOverlay } from './overlay.js'; -import { flip } from './position.js'; +import { flip, trackShift } from './position.js'; import type { CascaderOption, CascaderProps } from './types.js'; /*** 沿路径解析节点链:路径失效时在断点截断,避免脏数据把列算错 ***/ @@ -138,6 +138,15 @@ export function cascader(props: CascaderProps): HTMLElement { if (open()) releaseEsc ??= pushOverlay(() => { open(false); trigger.focus(); }); // 入栈:与弹窗/抽屉共用逐层关闭 else { releaseEsc?.(); releaseEsc = null; } }); - effect(() => { if (open()) place(flip(trigger, pop, 'bottom')); }); // 空间不足自动翻转 + let releaseShift: (() => void) | null = null; // 展开期间的位移跟随释放函数(滚动 / 缩放重算翻转) +effect(() => { + if (!open()) { // 收起即摘监听:弹层不在,重算方位没有意义 + releaseShift?.(); + releaseShift = null; + return; + } + place(flip(trigger, pop, 'bottom')); // 打开先算一次:空间不足自动翻转 + releaseShift ??= trackShift(() => place(flip(trigger, pop, 'bottom'))); // 展开期间跟着滚动重算:视口空间会变 + }); // 空间不足自动翻转 return root; } diff --git a/packages/skin/ui/src/colorPicker.ts b/packages/skin/ui/src/colorPicker.ts index 4a20821..12f8614 100644 --- a/packages/skin/ui/src/colorPicker.ts +++ b/packages/skin/ui/src/colorPicker.ts @@ -1,7 +1,7 @@ import { effect, h, signal } from '@ui-arch/frame'; import type { Signal } from '@ui-arch/frame'; import { bindOutside, pushOverlay } from './overlay.js'; -import { flip } from './position.js'; +import { flip, trackShift } from './position.js'; import type { ColorPickerProps } from './types.js'; // 缺省色板:直接取语义令牌,天然跟随暗色主题切换,不写死任何十六进制 @@ -91,6 +91,15 @@ export function colorPicker(props: ColorPickerProps): HTMLElement { if (open()) releaseEsc ??= pushOverlay(() => { open(false); trigger.focus(); }); // 入栈逐层关闭 else { releaseEsc?.(); releaseEsc = null; } }); - effect(() => { if (open()) place(flip(trigger, pop, 'bottom')); }); + let releaseShift: (() => void) | null = null; // 展开期间的位移跟随释放函数(滚动 / 缩放重算翻转) +effect(() => { + if (!open()) { // 收起即摘监听:弹层不在,重算方位没有意义 + releaseShift?.(); + releaseShift = null; + return; + } + place(flip(trigger, pop, 'bottom')); // 打开先算一次:空间不足自动翻转 + releaseShift ??= trackShift(() => place(flip(trigger, pop, 'bottom'))); // 展开期间跟着滚动重算:视口空间会变 + }); return root; } diff --git a/packages/skin/ui/src/datePicker.ts b/packages/skin/ui/src/datePicker.ts index 63f529a..d106650 100644 --- a/packages/skin/ui/src/datePicker.ts +++ b/packages/skin/ui/src/datePicker.ts @@ -1,7 +1,7 @@ import { effect, h, signal } from '@ui-arch/frame'; import type { Signal } from '@ui-arch/frame'; import { bindOutside, pushOverlay } from './overlay.js'; -import { flip } from './position.js'; +import { flip, trackShift } from './position.js'; import type { DatePickerProps, DatePreset } from './types.js'; import { fmt, pad, parse, shiftDay, today } from './dateKit.js'; @@ -170,6 +170,15 @@ export function datePicker(props: DatePickerProps): HTMLElement { if (open()) releaseEsc ??= pushOverlay(() => { open(false); trigger.focus(); }); // 入栈逐层关闭 else { releaseEsc?.(); releaseEsc = null; } }); - effect(() => { if (open()) place(flip(trigger, pop, 'bottom')); }); // 空间不足自动翻转 + let releaseShift: (() => void) | null = null; // 展开期间的位移跟随释放函数(滚动 / 缩放重算翻转) +effect(() => { + if (!open()) { // 收起即摘监听:弹层不在,重算方位没有意义 + releaseShift?.(); + releaseShift = null; + return; + } + place(flip(trigger, pop, 'bottom')); // 打开先算一次:空间不足自动翻转 + releaseShift ??= trackShift(() => place(flip(trigger, pop, 'bottom'))); // 展开期间跟着滚动重算:视口空间会变 + }); // 空间不足自动翻转 return root; } diff --git a/packages/skin/ui/src/menu.ts b/packages/skin/ui/src/menu.ts index fb45f59..a4d7a92 100644 --- a/packages/skin/ui/src/menu.ts +++ b/packages/skin/ui/src/menu.ts @@ -1,6 +1,6 @@ import { effect, h, signal } from '@ui-arch/frame'; import type { MenuItem, MenuProps } from './types.js'; -import { flip } from './position.js'; +import { flip, trackShift } from './position.js'; import { bindOutside, pushOverlay } from './overlay.js'; /*** 下拉菜单:触发器 + 自绘弹层,项支持禁用 / 危险 / 分隔线;点项 / 点外部 / Esc 关闭,↑↓ 环形导航 + Home/End 跳转 + 键入快跳 ***/ @@ -101,6 +101,15 @@ export function menu(props: MenuProps): HTMLElement { releaseEsc = null; } }); - effect(() => { if (open()) place(flip(trigger, pop, 'bottom')); }); // 每次打开重算方位:空间不足自动翻转 + let releaseShift: (() => void) | null = null; // 展开期间的位移跟随释放函数(滚动 / 缩放重算翻转) +effect(() => { + if (!open()) { // 收起即摘监听:弹层不在,重算方位没有意义 + releaseShift?.(); + releaseShift = null; + return; + } + place(flip(trigger, pop, 'bottom')); // 打开先算一次:空间不足自动翻转 + releaseShift ??= trackShift(() => place(flip(trigger, pop, 'bottom'))); // 展开期间跟着滚动重算:视口空间会变 + }); // 每次打开重算方位:空间不足自动翻转 return root; } diff --git a/packages/skin/ui/src/multiSelect.ts b/packages/skin/ui/src/multiSelect.ts index 2e2aa12..249c2d8 100644 --- a/packages/skin/ui/src/multiSelect.ts +++ b/packages/skin/ui/src/multiSelect.ts @@ -1,7 +1,7 @@ import { effect, h, signal } from '@ui-arch/frame'; import type { Signal } from '@ui-arch/frame'; import { bindOutside, pushOverlay } from './overlay.js'; -import { flip } from './position.js'; +import { flip, trackShift } from './position.js'; import type { MultiSelectProps } from './types.js'; /*** 多选下拉:标签式回显 + 可搜索弹层,value 数组信号双向绑定,到 max 未选项自动禁用 ***/ @@ -119,6 +119,15 @@ export function multiSelect(props: MultiSelectProps): HTMLElement { if (open()) releaseEsc ??= pushOverlay(() => { open(false); input.focus(); }); // 入栈逐层关闭 else { releaseEsc?.(); releaseEsc = null; } }); - effect(() => { if (open()) place(flip(box, pop, 'bottom')); }); + let releaseShift: (() => void) | null = null; // 展开期间的位移跟随释放函数(滚动 / 缩放重算翻转) +effect(() => { + if (!open()) { // 收起即摘监听:弹层不在,重算方位没有意义 + releaseShift?.(); + releaseShift = null; + return; + } + place(flip(box, pop, 'bottom')); // 打开先算一次:空间不足自动翻转 + releaseShift ??= trackShift(() => place(flip(box, pop, 'bottom'))); // 展开期间跟着滚动重算:视口空间会变 + }); return root; } diff --git a/packages/skin/ui/src/timePicker.ts b/packages/skin/ui/src/timePicker.ts index a99eeca..eb718ba 100644 --- a/packages/skin/ui/src/timePicker.ts +++ b/packages/skin/ui/src/timePicker.ts @@ -1,7 +1,7 @@ import { effect, h, signal } from '@ui-arch/frame'; import type { Signal } from '@ui-arch/frame'; import type { TimePickerProps } from './types.js'; -import { flip } from './position.js'; +import { flip, trackShift } from './position.js'; import { bindOutside, pushOverlay } from './overlay.js'; /*** 两位补零:时钟显示固定宽度,避免 9:5 这类不对齐的写法 ***/ @@ -156,6 +156,15 @@ export function timePicker(props: TimePickerProps): HTMLElement { releaseEsc = null; } }); - effect(() => { if (open()) place(flip(trigger, pop, 'bottom')); }); // 空间不足自动翻到上方 + let releaseShift: (() => void) | null = null; // 展开期间的位移跟随释放函数(滚动 / 缩放重算翻转) +effect(() => { + if (!open()) { // 收起即摘监听:弹层不在,重算方位没有意义 + releaseShift?.(); + releaseShift = null; + return; + } + place(flip(trigger, pop, 'bottom')); // 打开先算一次:空间不足自动翻转 + releaseShift ??= trackShift(() => place(flip(trigger, pop, 'bottom'))); // 展开期间跟着滚动重算:视口空间会变 + }); // 空间不足自动翻到上方 return root; }