Accordion

概述

`accordion` 标签路由到 `rml_ui::Accordion`,是 **StatelessWithItems** 闭包式 builder 组件,用于渲染可折叠的内容面板列表。每个面板由 `accordion` 容器 + 多个 `item` 子项组成。

RML **推荐使用小写语法** `<accordion>` / `<item>`,简洁干净且与 HTML 风格一致。PascalCase 与 kebab-case 写法完全兼容,五种写法在 codegen 与属性校验上等价。

标签别名表

`canonical_tag()` 函数将所有写法统一映射到 PascalCase 标准名,供 `props_registry` 属性查询使用。`component_lookup()` 同时注册 `accordion` 和 `Accordion`,`is_item_builder_tag()` 识别 `item` 和 `AccordionItem`。

| 写法 | 规范化结果 | 推荐度 | 说明 | |------|-----------|--------|------| | `<accordion>` | `Accordion` | ✅ 推荐 | 小写,HTML 风格,简洁干净 | | `<Accordion>` | `Accordion` | 兼容 | PascalCase,向后兼容 | | `<item>` | `AccordionItem` | ✅ 推荐 | 短标签,仅 `<accordion>` 内上下文敏感 | | `<AccordionItem>` | `AccordionItem` | 兼容 | PascalCase,向后兼容 | | `<accordion-item>` | `AccordionItem` | 兼容 | kebab-case,由 `normalize_component_tag` 处理 |

> ⚠️ `<item>` 短标签仅在 `<accordion>` / `<Accordion>` 父容器内被识别为 `AccordionItem`(由 `is_item_builder_tag` 判断)。在顶层或其他容器内使用 `<item>` 会报 "unknown tag" 错误。

基本用法

最小示例 —— 单选模式(默认),带边框:

<accordion bordered="">
    <item title="Section 1" open="">
        <p>第一段内容</p>
    </item>
    <item title="Section 2">
        <p>第二段内容</p>
    </item>
</accordion>

- `bordered=""` 启用边框 - `open=""` 设置初始展开 - `title="..."` 设置面板标题(静态字符串)

容器属性

`<accordion>` 容器支持的属性:

| 属性 | 类型 | 绑定 | 说明 | |------|------|------|------| | `multiple` | 布尔标志 | `{expr}` | 多选模式(同时展开多个面板) | | `bordered` | 布尔标志 | `{expr}` | 显示边框 | | `on_toggle_click` | 事件 | `{fn}` | 面板切换事件 | | `small` / `xsmall` / `large` | 布尔标志 | — | Sizable 通用尺寸 | | `disabled` | 布尔 | `{expr}` | 禁用(Styled 通用) |

布尔标志写法:`multiple=""` / `multiple="true"` 启用,`multiple="false"` 显式关闭。

子项属性

`<item>` 子项支持的属性:

| 属性 | 类型 | 绑定 | 说明 | |------|------|------|------| | `title` | 字符串 / 元素 | `{expr}` | 面板标题(接受 `impl IntoElement`,可绑定 i18n 调用) | | `open` | 布尔标志 | `{expr}` | 初始展开状态 | | `icon` | `IconName` 枚举名 | `{expr}` | 面板图标(如 `Settings` / `Bell`) | | `small` / `xsmall` / `large` | 布尔标志 | — | Sizable 通用尺寸 | | `disabled` | 布尔 | `{expr}` | 禁用该面板 |

`icon` 值必须是 `rml_ui::IconName` 枚举的合法变体名(如 `Settings`、`Bell`、`User`),codegen 生成 `.icon(rml_ui::IconName::Settings)`。非法枚举名会导致 Rust 编译失败。

事件

`on_toggle_click`

面板展开/收起时触发。事件绑定到 `<accordion>` 容器,**不能**绑定到 `<item>`。

<accordion bordered="" on_toggle_click={on_toggle}>
    <item title="Section 1" />
</accordion>

**用户方法签名**(在 code-behind 中定义):

impl AccordionCase {
    #[command]
    pub fn on_toggle(&mut self, open_ixs: &[usize], cx: &mut Context<Self>) {
        self.last_open = format!("{:?}", open_ixs);
        cx.notify();
    }
}

- `open_ixs: &[usize]` —— 当前展开项的索引列表(按声明顺序,0-based) - 单选模式下长度最多为 1,多选模式下可包含多个索引

多选与单选

**默认单选**:展开一项自动收起其他项。

<accordion bordered="">
    <item title="A" open="">...</item>
    <item title="B">...</item>
</accordion>

**多选模式**:`multiple=""` 启用,可同时展开多个面板。

<accordion multiple="" bordered="">
    <item title="A" open="">...</item>
    <item title="B" open="">...</item>
</accordion>

尺寸

通过 Sizable 通用属性控制:

<accordion small="" bordered="">
    <item title="Small">小尺寸</item>
</accordion>

<accordion large="" bordered="">
    <item title="Large">大尺寸</item>
</accordion>

图标

`<item>` 的 `icon` 属性在标题前显示图标:

<accordion bordered="" on_toggle_click={on_toggle}>
    <item title="设置" icon="Settings">
        <p>设置内容</p>
    </item>
    <item title="通知" icon="Bell" disabled="true">
        <p>通知内容(已禁用)</p>
    </item>
</accordion>

`icon` 值必须是 `IconName` 枚举名,完整列表见 gpui-component `IconName` 文档。

嵌套

`<item>` 内可嵌套 `<accordion>`,实现多级折叠:

<accordion bordered="">
    <item title="父级面板">
        <accordion bordered="" multiple="">
            <item title="子面板 1">
                <p>子内容 1</p>
            </item>
            <item title="子面板 2">
                <p>子内容 2</p>
            </item>
        </accordion>
    </item>
</accordion>

Codegen 说明

RML 编译器将 `<accordion>` + `<item>` 转译为闭包式 builder 调用。每个 `<item>` 生成一个 `.item(|__rml_item: rml_ui::AccordionItem| ...)` 闭包。

**输入**:

<accordion bordered="" multiple="">
    <item title="Section 1" open="">
        <p>Content</p>
    </item>
</accordion>

**生成代码**(简化示意):

rml_ui::Accordion::new(("rml_el", 0usize))
    .bordered(true)
    .multiple(true)
    .item(|__rml_item: rml_ui::AccordionItem| {
        __rml_item.title("Section 1").open(true).child("Content")
    })

**`on_toggle_click` 事件生成**:

rml_ui::Accordion::new(("rml_el", 1usize))
    .on_toggle_click(cx.listener(move |this, open_ixs: &[usize], _window, cx| {
        this.on_toggle(open_ixs, cx);
    }))

注意 `cx.listener` 闭包接收 4 参数 `(this, open_ixs, _window, cx)`,但用户定义的 `on_toggle` 方法仅接收 3 参数 `(self, open_ixs, cx)`,`_window` 被丢弃。

完整示例

以下示例来自 `demo/src/cases/accordion_case.rml`,覆盖 basic / multiple / sizes / icon / nested 五种场景,全部使用小写语法:

Basic(基础单选)

<accordion bordered="">
    <item title={t("case.accordion.section1")} open="">
        <p>{t("case.accordion.content1")}</p>
    </item>
    <item title={t("case.accordion.section2")}>
        <p>{t("case.accordion.content2")}</p>
    </item>
    <item title={t("case.accordion.section3")}>
        <p>{t("case.accordion.content3")}</p>
    </item>
</accordion>

Multiple(多选模式)

<accordion multiple="" bordered="">
    <item title={t("case.accordion.section1")} open="">
        <p>{t("case.accordion.content1")}</p>
    </item>
    <item title={t("case.accordion.section2")} open="">
        <p>{t("case.accordion.content2")}</p>
    </item>
</accordion>

Sizes(尺寸 + 事件)

<accordion small="" bordered="" on_toggle_click={on_toggle}>
    <item title={t("case.accordion.small")}>
        <p>{t("case.accordion.content1")}</p>
    </item>
</accordion>
<accordion large="" bordered="" on_toggle_click={on_toggle}>
    <item title={t("case.accordion.large")}>
        <p>{t("case.accordion.content1")}</p>
    </item>
</accordion>

With Icon(图标 + 禁用)

<accordion bordered="" on_toggle_click={on_toggle}>
    <item title={t("case.accordion.settings")} icon="Settings">
        <p>{t("case.accordion.content1")}</p>
    </item>
    <item title={t("case.accordion.disabled")} icon="Bell" disabled="true">
        <p>{t("case.accordion.content2")}</p>
    </item>
</accordion>

Nested(嵌套)

<accordion bordered="">
    <item title={t("case.accordion.parent")}>
        <accordion bordered="" multiple="">
            <item title={t("case.accordion.child1")}>
                <p>{t("case.accordion.content1")}</p>
            </item>
            <item title={t("case.accordion.child2")}>
                <p>{t("case.accordion.content2")}</p>
            </item>
        </accordion>
    </item>
</accordion>

Code-behind(Rust 侧)

use gpui::SharedString;
use rml::prelude::*;
use rml_core::i18n::t_static;

#[contribute(
    host_id = "demo.activity",
    id = "components.accordion",
    kind = "case",
    group = "components",
    order = 10,
)]
#[component]
#[derive(Default)]
pub struct AccordionCase {
    pub last_open: String,
}

impl IContribution for AccordionCase {
    fn id(&self) -> &str { Self::CONTRIBUTION_ID }
    fn name(&self) -> SharedString { t_static("case.accordion.title").into() }
}

impl ILifecycle for AccordionCase {}

impl AccordionCase {
    #[computed]
    pub fn status_text(&self) -> String {
        if self.last_open.is_empty() {
            "尚未切换任何项".to_string()
        } else {
            format!("上次展开项索引:{}", self.last_open)
        }
    }

    #[command]
    pub fn on_toggle(&mut self, open_ixs: &[usize], cx: &mut Context<Self>) {
        self.last_open = format!("{:?}", open_ixs);
        cx.notify();
    }
}

常见错误

1. **`<item>` 在 `<accordion>` 外使用** —— 顶层使用 `<item>` 报 "unknown tag" 错误。`<item>` 短标签仅在 `<accordion>` / `<Accordion>` 父容器内被 `is_item_builder_tag` 识别。

2. **`<accordion>` 包含非 `<item>` 子节点** —— `<accordion><div /></accordion>` 报错:`<accordion> 仅支持 <item> 或 <AccordionItem> 子节点,得到 <div>`。

3. **`<accordion>` 包含文本子节点** —— 文本子节点会输出 `[rml warning] <Accordion> 不支持文本子节点` 并被忽略。文本应放在 `<item>` 内部。

4. **`icon` 值非 `IconName` 枚举名** —— `icon="foo"` 生成 `.icon(rml_ui::IconName::foo)`,若 `foo` 不是合法枚举变体则 Rust 编译失败。

5. **`on_toggle_click` 绑定到 `<item>`** —— `on_toggle_click` 是容器事件,仅在 `<accordion>` 上有效。绑定到 `<item>` 不会生成任何代码。

6. **混合写法不一致** —— `<accordion><AccordionItem /></accordion>` 可正常工作(别名等价),但建议同一文件内保持写法一致(推荐全小写)。

相关组件

- [组件参考目录](./INDEX.md) —— 所有已注册组件 - [属性映射参考](./props-mapping.md) —— 组件属性 ↔ builder 方法对照表 - [标签映射 §2.2.9](../../02-syntax/tags-mapping.md) —— kebab-case 与小写别名规范

RML 未覆盖的 API

以下 gpui-component Accordion API 需在 Rust code-behind 中手写:

- `Accordion::default_open(ixs)` —— 通过编程方式设置默认展开项(RML 仅支持 `<item open="">` 静态声明) - 动态控制展开状态(运行时增删 item、修改 open 状态)—— 需通过 `ref` 获取 `Accordion` 实例后操作 - 自定义面板渲染(替换默认标题/内容布局)—— 需扩展 `AccordionItem` 或手写 builder - `on_toggle_click` 之外的细粒度事件(单项展开/收起回调)—— gpui-component 当前仅提供 `on_toggle_click`