iter(g212): 页签溢出时自动把激活页签拉回可视区

This commit is contained in:
HIsheR
2026-10-26 19:50:33 +08:00
parent 7fa60fbf57
commit 6ccc341998
5 changed files with 30 additions and 5 deletions
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "ui-arch-registry-index/0.1", "$schema": "ui-arch-registry-index/0.1",
"generatedAt": "2026-10-26T11:20:15.456Z", "generatedAt": "2026-10-26T11:50:16.380Z",
"repoRoot": ".", "repoRoot": ".",
"packages": [ "packages": [
{ {
@@ -1472,6 +1472,7 @@
"创建信号 tab = signal('a'),初值为默认页签", "创建信号 tab = signal('a'),初值为默认页签",
"tabs 只渲染页签头;页签内容用响应式函数按信号切换,如 () => tab() === 'a' ? panelA : panelB", "tabs 只渲染页签头;页签内容用响应式函数按信号切换,如 () => tab() === 'a' ? panelA : panelB",
"点击或方向键(←→ 环形)、Home/End(直达首/末页签)切换,滑动指示条自动跟随;disabled: true 的页签置灰不可点", "点击或方向键(←→ 环形)、Home/End(直达首/末页签)切换,滑动指示条自动跟随;disabled: true 的页签置灰不可点",
"页签多到一屏放不下(页签条横向溢出)时,切到被裁掉的页签会自动滚回可视区:只滚页签条本身、不带动页面,系统开启「减弱动效」时直接跳位;已完整可见则不动,不打断用户手动滚动",
"需要可关闭页签时给该项加 closable: true,× 或聚焦后按 Delete 触发 onClose(value);组件不改数据源,调用方从数组里摘掉该项再重绘", "需要可关闭页签时给该项加 closable: true,× 或聚焦后按 Delete 触发 onClose(value);组件不改数据源,调用方从数组里摘掉该项再重绘",
"键盘可访问:Tab 聚焦激活页签,←→ 在启用页签间环形移动,Home/End 跳首个/末个启用页签", "键盘可访问:Tab 聚焦激活页签,←→ 在启用页签间环形移动,Home/End 跳首个/末个启用页签",
"页签栏右侧要放「新建 / 设置」这类操作就传 extra(按钮或链接):它与页签同一行共用一条分隔线,不传时 DOM 结构保持不变" "页签栏右侧要放「新建 / 设置」这类操作就传 extra(按钮或链接):它与页签同一行共用一条分隔线,不传时 DOM 结构保持不变"
+7
View File
@@ -181,6 +181,8 @@ const closableTabs = signal([
{ value: 'n3', label: '订单 #1026', closable: true }, { value: 'n3', label: '订单 #1026', closable: true },
]); ]);
const closableActive = signal('n1'); const closableActive = signal('n1');
// —— 溢出页签:容器装不下时,切到被裁掉的页签应自动拉回可视区 ——
const wideTab = signal('t0');
const modalOpen = signal(false); const modalOpen = signal(false);
const modalPick = signal('a'); // 弹窗内下拉的取值,用于演示 Esc 逐层关闭 const modalPick = signal('a'); // 弹窗内下拉的取值,用于演示 Esc 逐层关闭
@@ -583,6 +585,11 @@ const content = h(
}, },
})]; })];
}), }),
h('div', { style: 'max-width:320px;width:100%' }, tabs({
tabs: Array.from({ length: 8 }, (_, i) => ({ value: `t${i}`, label: `选项 ${i + 1}` })), // 八个页签必然溢出,用来验证自动回拉
active: wideTab,
})),
h('span', { class: 'out' }, () => `溢出演示当前页签:${wideTab()}(点最右侧的页签,标题条会自动滚过去)`),
button({ label: '打开弹窗', onClick: () => modalOpen(true) }), button({ label: '打开弹窗', onClick: () => modalOpen(true) }),
button({ label: '打开防误关弹窗', variant: 'ghost', onClick: () => stickyOpen(true) }), button({ label: '打开防误关弹窗', variant: 'ghost', onClick: () => stickyOpen(true) }),
modal( modal(
+5 -4
View File
@@ -1,6 +1,6 @@
# @ui-arch 组件库 Agent 文档(无图形版) # @ui-arch 组件库 Agent 文档(无图形版)
> 自动生成于 2026-10-26T11:20:15.456Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 自动生成于 2026-10-26T11:50:16.380Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
## 快速上手 ## 快速上手
@@ -724,9 +724,10 @@ const tab = signal('a'); tabs({ tabs: [{ value: 'a', label: '概览', closable:
1. 创建信号 tab = signal('a'),初值为默认页签 1. 创建信号 tab = signal('a'),初值为默认页签
2. tabs 只渲染页签头;页签内容用响应式函数按信号切换,如 () => tab() === 'a' ? panelA : panelB 2. tabs 只渲染页签头;页签内容用响应式函数按信号切换,如 () => tab() === 'a' ? panelA : panelB
3. 点击或方向键(←→ 环形)、Home/End(直达首/末页签)切换,滑动指示条自动跟随;disabled: true 的页签置灰不可点 3. 点击或方向键(←→ 环形)、Home/End(直达首/末页签)切换,滑动指示条自动跟随;disabled: true 的页签置灰不可点
4. 需要可关闭页签时给该项加 closable: true,× 或聚焦后按 Delete 触发 onClose(value);组件不改数据源,调用方从数组里摘掉该项再重绘 4. 页签多到一屏放不下(页签条横向溢出)时,切到被裁掉的页签会自动滚回可视区:只滚页签条本身、不带动页面,系统开启「减弱动效」时直接跳位;已完整可见则不动,不打断用户手动滚动
5. 键盘可访问:Tab 聚焦激活页签,←→ 在启用页签间环形移动,Home/End 跳首个/末个启用页签 5. 需要可关闭页签时给该项加 closable: true,× 或聚焦后按 Delete 触发 onClose(value);组件不改数据源,调用方从数组里摘掉该项再重绘
6. 页签栏右侧要放「新建 / 设置」这类操作就传 extra(按钮或链接):它与页签同一行共用一条分隔线,不传时 DOM 结构保持不变 6. 键盘可访问:Tab 聚焦激活页签,←→ 在启用页签间环形移动,Home/End 跳首个/末个启用页签
7. 页签栏右侧要放「新建 / 设置」这类操作就传 extra(按钮或链接):它与页签同一行共用一条分隔线,不传时 DOM 结构保持不变
现场演示:apps/docs/demo.html#sec-tabs-modal 现场演示:apps/docs/demo.html#sec-tabs-modal
+15
View File
@@ -9,6 +9,20 @@ export function tabs(props: TabsProps): HTMLElement {
const indicator = h('span', { class: 'ua-tabs-indicator' }); const indicator = h('span', { class: 'ua-tabs-indicator' });
const head = h('div', { class: 'ua-tabs-head', role: 'tablist', 'aria-label': '标签页' }, indicator); const head = h('div', { class: 'ua-tabs-head', role: 'tablist', 'aria-label': '标签页' }, indicator);
/*** 把激活页签滚进可视区:页签条溢出时只滚它自己这一层,不动页面与其它祖先容器 ***/
const revealActive = (wrap: HTMLElement) => {
const pad = 8; // 两侧留白:贴边时激活页签的外框不至于被裁一半
const max = head.scrollWidth - head.clientWidth;
if (max <= 0) return; // 卫语句:没有溢出(页签条放得下),不需要滚动
const left = wrap.offsetLeft;
const right = left + wrap.offsetWidth;
const viewLeft = head.scrollLeft;
if (left >= viewLeft && right <= viewLeft + head.clientWidth) return; // 卫语句:已完整可见,不打断用户的手动滚动
const target = left < viewLeft ? left - pad : right - head.clientWidth + pad; // 就近对齐:只补被裁掉的那一段,不做居中
const smooth = !window.matchMedia('(prefers-reduced-motion: reduce)').matches; // 尊重减弱动效偏好:直接跳位
head.scrollTo({ left: Math.max(0, Math.min(max, target)), behavior: smooth ? 'smooth' : 'auto' });
};
/*** 同步激活态与指示条几何位置:量外壳而非按钮,可关闭页签的指示条才会盖住关闭按钮 ***/ /*** 同步激活态与指示条几何位置:量外壳而非按钮,可关闭页签的指示条才会盖住关闭按钮 ***/
const sync = () => { const sync = () => {
const value = props.active?.(); const value = props.active?.();
@@ -21,6 +35,7 @@ export function tabs(props: TabsProps): HTMLElement {
const wrap = btn.parentElement ?? btn; const wrap = btn.parentElement ?? btn;
indicator.style.left = `${wrap.offsetLeft}px`; indicator.style.left = `${wrap.offsetLeft}px`;
indicator.style.width = `${wrap.offsetWidth}px`; indicator.style.width = `${wrap.offsetWidth}px`;
revealActive(wrap); // 激活项被滚出视口时拉回可见区(页签多到溢出才会真的动)
} }
}; };
+1
View File
@@ -1343,6 +1343,7 @@
"创建信号 tab = signal('a'),初值为默认页签", "创建信号 tab = signal('a'),初值为默认页签",
"tabs 只渲染页签头;页签内容用响应式函数按信号切换,如 () => tab() === 'a' ? panelA : panelB", "tabs 只渲染页签头;页签内容用响应式函数按信号切换,如 () => tab() === 'a' ? panelA : panelB",
"点击或方向键(←→ 环形)、Home/End(直达首/末页签)切换,滑动指示条自动跟随;disabled: true 的页签置灰不可点", "点击或方向键(←→ 环形)、Home/End(直达首/末页签)切换,滑动指示条自动跟随;disabled: true 的页签置灰不可点",
"页签多到一屏放不下(页签条横向溢出)时,切到被裁掉的页签会自动滚回可视区:只滚页签条本身、不带动页面,系统开启「减弱动效」时直接跳位;已完整可见则不动,不打断用户手动滚动",
"需要可关闭页签时给该项加 closable: true,× 或聚焦后按 Delete 触发 onClose(value);组件不改数据源,调用方从数组里摘掉该项再重绘", "需要可关闭页签时给该项加 closable: true,× 或聚焦后按 Delete 触发 onClose(value);组件不改数据源,调用方从数组里摘掉该项再重绘",
"键盘可访问:Tab 聚焦激活页签,←→ 在启用页签间环形移动,Home/End 跳首个/末个启用页签", "键盘可访问:Tab 聚焦激活页签,←→ 在启用页签间环形移动,Home/End 跳首个/末个启用页签",
"页签栏右侧要放「新建 / 设置」这类操作就传 extra(按钮或链接):它与页签同一行共用一条分隔线,不传时 DOM 结构保持不变" "页签栏右侧要放「新建 / 设置」这类操作就传 extra(按钮或链接):它与页签同一行共用一条分隔线,不传时 DOM 结构保持不变"