iter: d03 新增折叠面板 accordion 组件

This commit is contained in:
ui-arch-bot
2026-10-23 16:47:56 +08:00
parent 6b9fa95d1e
commit df2fb34e63
11 changed files with 281 additions and 8 deletions
+55
View File
@@ -0,0 +1,55 @@
import { h, signal } from '@ui-arch/frame';
import type { AccordionItem, AccordionProps } from './types.js';
/*** 折叠面板:标题按钮 + grid 行高过渡;单开(手风琴)或多开,展开态由 active 信号驱动或内部持有 ***/
export function accordion(props: AccordionProps): HTMLElement {
// —— 状态集中声明 ——
const internal = signal<string[]>([]); // 内部展开集合(未传 active 时使用)
const opened = (v: string): boolean => (props.active ? props.active() === v : internal().includes(v));
const toggle = (item: AccordionItem): void => {
if (props.active) {
const next = props.active() === item.value ? '' : item.value; // 单开:再点收起
props.active(next);
props.onChange?.(next);
return;
}
const cur = internal();
const next = cur.includes(item.value)
? cur.filter((v) => v !== item.value)
: props.multiple
? [...cur, item.value]
: [item.value];
internal(next);
props.onChange?.(next[0] ?? '');
};
// —— 面板:标题为原生 button(键盘可达),内容包一层 overflow 容器供过渡裁剪 ——
const rows = props.items.map((item, i) =>
h(
'div',
{ class: () => `ua-acc-item${opened(item.value) ? ' ua-open' : ''}` },
h(
'button',
{
type: 'button',
class: 'ua-acc-head',
id: `ua-acc-h-${i}`,
'aria-expanded': () => String(opened(item.value)),
'aria-controls': `ua-acc-b-${i}`,
disabled: item.disabled ?? false, // 布尔透传即得禁用态
onClick: () => toggle(item),
},
h('span', { class: 'ua-acc-title' }, item.title),
h('span', { class: 'ua-acc-caret' }, '▸'),
),
h(
'div',
{ class: 'ua-acc-body', id: `ua-acc-b-${i}`, role: 'region', 'aria-labelledby': `ua-acc-h-${i}` },
h('div', { class: 'ua-acc-inner' }, item.content),
),
),
);
return h('div', { class: 'ua-accordion' }, ...rows);
}
+1
View File
@@ -7,6 +7,7 @@ export { switchButton } from './switch.js';
export { select } from './select.js';
export { menu } from './menu.js';
export { radioGroup } from './radioGroup.js';
export { accordion } from './accordion.js';
export { card } from './card.js';
export { badge } from './badge.js';
export { alert } from './alert.js';
+16
View File
@@ -97,6 +97,22 @@ export interface MenuProps {
onSelect?: (value: string) => void; // 选中回调(禁用项不触发)
}
/*** 折叠面板条目 ***/
export interface AccordionItem {
value: string; // 面板标识(回调与展开集合用)
title: string; // 标题文案(同时作为触发器)
content: unknown; // 面板内容:字符串 / DOM 节点 / 响应式函数
disabled?: boolean; // 禁用该面板:标题置灰且不可展开
}
/*** 折叠面板入参 ***/
export interface AccordionProps {
items: AccordionItem[]; // 面板列表
active?: Signal<string>; // 展开面板信号(单开模式);缺省由组件内部持有状态
multiple?: boolean; // 是否允许多个面板同时展开;缺省 false(手风琴)
onChange?: (active: string) => void; // 展开变化回调:收起时传空串
}
/*** 多行输入入参 ***/
export interface TextareaProps {
placeholder?: string; // 占位文案
+43 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@ui-arch/ui",
"type": "skin-dom",
"description": "DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / menu / radioGroup / card / badge / alert / toast / tooltip / skeleton / spinner / progress / tabs / pagination / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。",
"description": "DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / menu / radioGroup / accordion / card / badge / alert / toast / tooltip / skeleton / spinner / progress / tabs / pagination / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。",
"tags": [
"components",
"dom",
@@ -21,6 +21,7 @@
"select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画,disabled 支持布尔或信号禁用",
"menu(props):下拉菜单,触发器 + 自绘弹层,条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部 / Esc 关闭",
"radioGroup(props):单选组,options + value 信号双向绑定,disabled 整组禁用",
"accordion(props):折叠面板,标题按钮 + grid 行高过渡,单开(手风琴)或多开,支持禁用面板与 active 信号受控",
"card(props, ...children):卡片容器",
"badge(props):徽标,variant = default | success | warning | danger",
"alert(props):提示条,variant = info | success | warning | danger",
@@ -46,6 +47,8 @@
"RadioGroupProps",
"MenuItem",
"MenuProps",
"AccordionItem",
"AccordionProps",
"OptionItem",
"CardProps",
"BadgeProps",
@@ -72,7 +75,7 @@
"语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色"
],
"docs": {
"intro": "21 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。",
"intro": "22 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。",
"steps": [
"安装依赖:pnpm add @ui-arch/ui @ui-arch/frame(workspace 内自动链接)",
"引入样式:在应用入口 import '@ui-arch/ui/ui.css',暗色主题在根元素设 data-theme='dark'",
@@ -381,6 +384,44 @@
"需要整组禁用时传 disabled:布尔值静态禁用,也可传信号(如 locked)实现动态锁定"
]
},
{
"id": "accordion",
"title": "折叠面板 Accordion",
"demo": "sec-accordion",
"props": [
{
"n": "items",
"t": "AccordionItem[]",
"r": 1,
"d": "面板列表:{ value, title, content, disabled? }"
},
{
"n": "active",
"t": "Signal<string>",
"d": "受控展开面板信号(单开模式);缺省由组件内部持有状态"
},
{
"n": "multiple",
"t": "boolean",
"d": "允许多个面板同时展开;缺省 false(手风琴,单开)",
"df": "false"
},
{
"n": "onChange",
"t": "(active: string) => void",
"k": 1,
"d": "展开变化回调:收起时传空串"
}
],
"example": "accordion({ items: [{ value: 'a', title: '账户设置', content: '内容…' }, { value: 'b', title: '通知', content: h('p', null, '…') }] })",
"steps": [
"准备 items:value 作标识、title 作标题、content 可为字符串 / DOM 节点 / 响应式函数",
"默认手风琴模式:展开一个自动收起其他;需要同时展开多个时传 multiple: true",
"需要外部控制或读取展开项时传 active 信号(受控单开),否则组件内部自持状态",
"标题是原生 button:Tab 聚焦、Enter / Space 切换,展开态带 aria-expanded + region 关联",
"内容区用 grid 行高 0fr → 1fr 过渡,无需测量高度;reduced-motion 用户自动静态切换"
]
},
{
"id": "card",
"title": "卡片 Card",
+68
View File
@@ -384,6 +384,71 @@
background: var(--ua-color-border);
}
/* —— 折叠面板 —— */
.ua-accordion {
display: flex;
flex-direction: column;
border: 1px solid var(--ua-color-border);
border-radius: var(--ua-radius-md);
overflow: hidden;
background: var(--ua-color-bg);
}
.ua-acc-item + .ua-acc-item {
border-top: 1px solid var(--ua-color-border);
}
.ua-acc-head {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 10px 12px;
border: none;
background: none;
color: var(--ua-color-text);
font-size: var(--ua-font-size-lg);
text-align: left;
cursor: pointer;
transition: background 0.15s;
}
.ua-acc-head:hover:not(:disabled) {
background: color-mix(in srgb, var(--ua-color-accent) 6%, transparent);
}
.ua-acc-head:focus-visible {
outline: none;
box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--ua-color-accent) 45%, transparent);
}
.ua-acc-head:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.ua-acc-caret {
font-size: 10px;
color: var(--ua-color-text-muted);
transition: transform 0.22s ease;
}
.ua-acc-item.ua-open .ua-acc-caret {
transform: rotate(90deg);
}
/* 内容区用 grid 行高过渡:0fr → 1fr,无需测量高度 */
.ua-acc-body {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.24s ease;
}
.ua-acc-item.ua-open .ua-acc-body {
grid-template-rows: 1fr;
}
.ua-acc-inner {
overflow: hidden;
padding: 0 12px;
color: var(--ua-color-text-muted);
font-size: var(--ua-font-size-md);
}
.ua-acc-item.ua-open .ua-acc-inner {
padding-bottom: 12px;
}
/* —— 多行输入 —— */
.ua-textarea {
width: 100%;
@@ -885,6 +950,9 @@
.ua-menu-pop,
.ua-menu-caret,
.ua-menu-item,
.ua-acc-head,
.ua-acc-caret,
.ua-acc-body,
.ua-switch-track,
.ua-switch-thumb,
.ua-tab,