diff --git a/.ua/backlog.json b/.ua/backlog.json index 55120e1..18761bc 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -1292,6 +1292,14 @@ "acceptance": "DrawerProps 新增 modal(缺省 true):false 时遮罩透明且 pointer-events: none、面板自身可交互并带阴影、不锁背景滚动、不启用 Tab 焦点陷阱、aria-modal=false、点外部不算关闭(× 与 Esc 仍可用);文档参数表 / 手把手与演示(非模态检查器)同步;npm run check 全绿", "notes": "自补:组件可用性;沿用现有遮罩节点加一个 is-plain 分支,不新增 DOM 结构", "status": "done" + }, + { + "id": "g117", + "title": "指示条与滑块跟随尺寸重算", + "motivation": "tabs 的滑动指示条与 segmented 的滑块都只在「激活项变化」和挂载后一帧各量一次:窗口缩放、侧栏展开收起、字体调大之后,指示条宽度就停在旧值上,明显错位;这类「布局变了但没重算」的问题用户只会觉得组件坏了一半", + "acceptance": "watch.ts 新增 trackSize(root, handler):用 ResizeObserver 观察根节点尺寸,变化即回调,节点脱离文档时自查并断开(无 ResizeObserver 的环境回落成 window resize);tabs / segmented 接入,指示条与滑块在容器变宽变窄后保持对齐;npm run check 全绿", + "notes": "自补:正确性;与 watchViewport 同一处收口,后续布局敏感组件照抄", + "status": "open" } ] } diff --git a/packages/skin/ui/src/segmented.ts b/packages/skin/ui/src/segmented.ts index bcf85b6..75443e5 100644 --- a/packages/skin/ui/src/segmented.ts +++ b/packages/skin/ui/src/segmented.ts @@ -1,5 +1,6 @@ import { effect, h } from '@ui-arch/frame'; import type { SegmentedProps } from './types.js'; +import { trackSize } from './watch.js'; /*** 分段控制器:胶囊分段条 + 滑动滑块,value 信号双向绑定,←→/Home/End 环形导航,整组或单项可禁用 ***/ export function segmented(props: SegmentedProps): HTMLElement { @@ -59,5 +60,6 @@ export function segmented(props: SegmentedProps): HTMLElement { effect(sync); requestAnimationFrame(sync); // 挂载后再量一次布局,否则滑块宽度为 0 + trackSize(root, sync); // 尺寸变化后重算滑块位置与宽度 return root; } diff --git a/packages/skin/ui/src/tabs.ts b/packages/skin/ui/src/tabs.ts index 1f52b11..446ac76 100644 --- a/packages/skin/ui/src/tabs.ts +++ b/packages/skin/ui/src/tabs.ts @@ -1,5 +1,6 @@ import { effect, h } from '@ui-arch/frame'; import type { TabItem, TabsProps } from './types.js'; +import { trackSize } from './watch.js'; /*** 标签页:active 信号双向绑定 + 滑动指示条 + 方向键环形导航;closable 页签带关闭按钮(聚焦后 Delete 亦可关) ***/ export function tabs(props: TabsProps): HTMLElement { @@ -63,7 +64,9 @@ export function tabs(props: TabsProps): HTMLElement { next?.click(); }); + const root = h('div', { class: 'ua-tabs' }, head); effect(sync); requestAnimationFrame(sync); // 挂载后再量一次布局,否则指示条宽度为 0 - return h('div', { class: 'ua-tabs' }, head); + trackSize(root, sync); // 容器变宽 / 窗口缩放后重算,指示条不会停在旧宽度 + return root; } diff --git a/packages/skin/ui/src/watch.ts b/packages/skin/ui/src/watch.ts index 7cbff24..58ece5a 100644 --- a/packages/skin/ui/src/watch.ts +++ b/packages/skin/ui/src/watch.ts @@ -13,3 +13,13 @@ export function watchViewport(root: HTMLElement, host: HTMLElement | Window, han window.addEventListener('resize', run); // 视口变化会改判定线与可滚距离 handler(); // 首帧同步一次:刷新时停在页面中段也能得到正确状态 } + +/*** 尺寸监听:根节点尺寸变化时重算(视口缩放、侧栏展开、字体变大),脱离文档即自断,不用手动销毁 ***/ +export function trackSize(root: HTMLElement, handler: () => void): void { + if (typeof ResizeObserver === 'undefined') { window.addEventListener('resize', handler); return; } // 老环境回落:至少跟上视口变化 + const ro = new ResizeObserver(() => { + if (!root.isConnected) { ro.disconnect(); return; } // 已卸载:断开观察,避免闭包被观察器拽住 + handler(); + }); + ro.observe(root); +}