iter(g111): 面包屑支持超长路径折叠

This commit is contained in:
HIsheR
2026-10-26 14:04:40 +08:00
parent f90033707c
commit f40ef6d3ea
8 changed files with 91 additions and 18 deletions
+34 -13
View File
@@ -1,20 +1,41 @@
import { h } from '@ui-arch/frame';
import { effect, h, signal } from '@ui-arch/frame';
import type { BreadcrumbProps } from './types.js';
/*** 面包屑:nav + ol 语义结构,末项为当前页(不可点、aria-current);有 href 渲染 <a>,无 href 渲染 button 交 onNavigate ***/
/*** 面包屑:nav + ol 语义结构,末项为当前页(不可点、aria-current);条目超过 maxItems 时把中段折叠成「…」按钮 ***/
export function breadcrumb(props: BreadcrumbProps): HTMLElement {
// —— 状态集中声明 ——
const items = props.items;
const sep = props.separator ?? '/';
const sep = props.separator ?? '/'; // 分隔符
const max = props.maxItems ?? Number.POSITIVE_INFINITY; // 展示条数上限:超出折叠中段
const expanded = signal(false); // 折叠段是否已展开(展开后不再折叠)
const list = h('ol', { class: 'ua-crumb-list' });
const entries = items.map((item, i) => {
const last = i === items.length - 1; // 末项即当前页
const node = last
? h('span', { class: 'ua-crumb-current', 'aria-current': 'page' }, item.label)
: item.href
? h('a', { class: 'ua-crumb-link', href: item.href }, item.label)
: h('button', { type: 'button', class: 'ua-crumb-link', onClick: () => props.onNavigate?.(item, i) }, item.label);
return h('li', { class: 'ua-crumb-item' }, node, last ? null : h('span', { class: 'ua-crumb-sep', 'aria-hidden': 'true' }, sep));
});
/*** 逐项渲染:有 href 用 <a>,无 href 用 button 交 onNavigate;折叠按钮紧跟首项插入一次 ***/
const render = (): void => {
const fold = !expanded() && items.length > max; // 是否需要折叠
const tail = Math.max(2, max - 1); // 尾部保留条数:至少当前页 + 上一级
const kept = fold ? [0, ...items.slice(-tail).map((_, k) => items.length - tail + k)] : items.map((_, i) => i);
const nodes: HTMLElement[] = [];
kept.forEach((idx, pos) => {
const item = items[idx];
const last = idx === items.length - 1; // 末项即当前页
const node = last
? h('span', { class: 'ua-crumb-current', 'aria-current': 'page' }, item.label)
: item.href
? h('a', { class: 'ua-crumb-link', href: item.href }, item.label)
: h('button', { type: 'button', class: 'ua-crumb-link', onClick: () => props.onNavigate?.(item, idx) }, item.label);
nodes.push(h('li', { class: 'ua-crumb-item' }, node, last ? null : h('span', { class: 'ua-crumb-sep', 'aria-hidden': 'true' }, sep)));
if (!fold || pos > 0) return; // 卫语句:折叠按钮只插一次
const hidden = items.length - kept.length; // 被折叠的条数
nodes.push(h('li', { class: 'ua-crumb-item' }, h(
'button',
{ type: 'button', class: 'ua-crumb-fold', 'aria-label': `展开折叠的 ${hidden} 项`, onClick: () => expanded(true) },
'…',
), h('span', { class: 'ua-crumb-sep', 'aria-hidden': 'true' }, sep)));
});
list.replaceChildren(...nodes);
};
return h('nav', { class: 'ua-breadcrumb', 'aria-label': '面包屑导航' }, h('ol', { class: 'ua-crumb-list' }, ...entries));
effect(render); // expanded 变化即重绘
return h('nav', { class: 'ua-breadcrumb', 'aria-label': '面包屑导航' }, list);
}
+1
View File
@@ -149,6 +149,7 @@ export interface BreadcrumbItem {
export interface BreadcrumbProps {
items: BreadcrumbItem[]; // 路径条目,末项视为当前页(不可点)
separator?: string; // 分隔符;缺省 '/'
maxItems?: number; // 最多展示几项;超出时中段折叠成「…」按钮,点开即完整展开;缺省不折叠
onNavigate?: (item: BreadcrumbItem, index: number) => void; // 无 href 条目的点击回调
}
+7 -1
View File
@@ -706,6 +706,11 @@
"t": "(item, index) => void",
"k": 1,
"d": "无 href 条目的点击回调"
},
{
"n": "maxItems",
"t": "number",
"d": "最多展示几项:超出时首项之后折叠成「…」按钮(aria-label 报出折叠条数),点开即完整展开;缺省不折叠"
}
],
"example": "breadcrumb({ items: [{ label: '首页', href: '/' }, { label: '设置' }, { label: '账户' }], onNavigate: (it) => console.log(it.label) })",
@@ -713,7 +718,8 @@
"按层级顺序准备 items,最后一项即当前页,自动渲染为不可点的灰色文字并带 aria-current='page'",
"需要真实跳转就给条目加 href(渲染 <a>);单页应用想自行接管时不给 href,改用 onNavigate 回调",
"改分隔符传 separator,如 '›';分隔符对读屏隐藏",
"语义结构为 nav + ol + li,可直接放进页头或卡片标题栏"
"语义结构为 nav + ol + li,可直接放进页头或卡片标题栏",
"路径过深时传 maxItems(如 4):首项 + 「…」+ 末几项,点「…」原地展开全部,末项始终是当前页"
]
},
{
+20
View File
@@ -3784,3 +3784,23 @@ button.ua-step-dot:hover {
.ua-select-clear[hidden] {
display: none;
}
/* —— 面包屑折叠按钮 —— */
.ua-crumb-fold {
padding: 0 2px;
border: none;
background: none;
color: var(--ua-color-text-muted);
font-size: inherit;
cursor: pointer;
border-radius: var(--ua-radius-sm);
transition: color 0.15s, background-color 0.15s;
}
.ua-crumb-fold:hover {
color: var(--ua-color-accent);
background: color-mix(in srgb, var(--ua-color-accent) 8%, transparent);
}
.ua-crumb-fold:focus-visible {
outline: 2px solid var(--ua-color-accent);
outline-offset: 1px;
}