From 48e1d1d342032bc78a0c7cbf595c8c0533fc0fe8 Mon Sep 17 00:00:00 2001 From: carl chen Date: Tue, 4 Aug 2026 08:57:51 +0800 Subject: [PATCH 1/2] feat(x): support destroyOnHidden on Think and ThoughtChain Expose destroyOnHidden on Think and on ThoughtChain items, defaulting to true so collapsed content keeps being destroyed. Setting it to false keeps the content node mounted and only hides it, preserving streaming output and inner component state across a collapse. Sync ant-design/x#1985. close #145 --- .../think/demo/destroy-on-hidden.vue | 36 ++++++++ .../src/pages/components/think/index.en-US.md | 26 +++--- .../src/pages/components/think/index.zh-CN.md | 26 +++--- .../thought-chain/demo/destroy-on-hidden.vue | 55 +++++++++++++ .../components/thought-chain/index.en-US.md | 24 +++--- .../components/thought-chain/index.zh-CN.md | 24 +++--- packages/x/components/think/Think.tsx | 53 ++++++++---- .../components/think/__tests__/index.test.tsx | 63 ++++++++++++++ packages/x/components/think/interface.ts | 7 ++ packages/x/components/thought-chain/Node.tsx | 40 ++++++--- .../thought-chain/__tests__/index.test.tsx | 82 +++++++++++++++++++ .../x/components/thought-chain/interface.ts | 7 ++ 12 files changed, 369 insertions(+), 74 deletions(-) create mode 100644 packages/docs/src/pages/components/think/demo/destroy-on-hidden.vue create mode 100644 packages/docs/src/pages/components/thought-chain/demo/destroy-on-hidden.vue diff --git a/packages/docs/src/pages/components/think/demo/destroy-on-hidden.vue b/packages/docs/src/pages/components/think/demo/destroy-on-hidden.vue new file mode 100644 index 00000000..1899c50d --- /dev/null +++ b/packages/docs/src/pages/components/think/demo/destroy-on-hidden.vue @@ -0,0 +1,36 @@ + + + + + +`destroyOnHidden` 默认为 `true`,折叠时销毁内容节点。设为 `false` 时内容节点保留在 DOM 中,仅隐藏,可以保留流式输出与内部组件状态(例如上面输入框中已输入的内容)。 + + + +`destroyOnHidden` defaults to `true`, which destroys the content node on collapse. Set it to `false` to keep the node mounted and merely hidden, preserving streaming output and inner component state (such as the text typed into the input above). + diff --git a/packages/docs/src/pages/components/think/index.en-US.md b/packages/docs/src/pages/components/think/index.en-US.md index 283f2f5a..71e0b07b 100644 --- a/packages/docs/src/pages/components/think/index.en-US.md +++ b/packages/docs/src/pages/components/think/index.en-US.md @@ -13,23 +13,25 @@ Used to show deep thinking process. Status Expand Slots +Destroy On Hidden ## API ### ThinkProps -| Property | Description | Type | Default | Version | -| ------------------ | ---------------------------- | ----------------------------------------------------- | ------- | ------- | -| classes | Semantic DOM class | [Record\](#semantic-dom) | - | - | -| styles | Semantic DOM style | [Record\](#semantic-dom) | - | - | -| default | Content (default slot) | `VNodeChild` | - | - | -| title | Text of status | `VNodeChild` | - | - | -| icon | Show icon | `VNodeChild` | - | - | -| loading | Loading | `boolean \| VNodeChild` | `false` | - | -| defaultExpanded | Default Expand state | `boolean` | `true` | - | -| expanded (v-model) | Expand state | `boolean` | - | - | -| onExpand | Callback when expand changes | `(expand: boolean) => void` | - | - | -| blink | Blink mode | `boolean` | - | - | +| Property | Description | Type | Default | Version | +| ------------------ | -------------------------------------------------- | ----------------------------------------------------- | ------- | ------- | +| classes | Semantic DOM class | [Record\](#semantic-dom) | - | - | +| styles | Semantic DOM style | [Record\](#semantic-dom) | - | - | +| default | Content (default slot) | `VNodeChild` | - | - | +| title | Text of status | `VNodeChild` | - | - | +| icon | Show icon | `VNodeChild` | - | - | +| loading | Loading | `boolean \| VNodeChild` | `false` | - | +| defaultExpanded | Default Expand state | `boolean` | `true` | - | +| expanded (v-model) | Expand state | `boolean` | - | - | +| onExpand | Callback when expand changes | `(expand: boolean) => void` | - | - | +| blink | Blink mode | `boolean` | - | - | +| destroyOnHidden | Whether to destroy the content node when collapsed | `boolean` | `true` | - | ### Think Slots diff --git a/packages/docs/src/pages/components/think/index.zh-CN.md b/packages/docs/src/pages/components/think/index.zh-CN.md index 6e72d65a..8be36002 100644 --- a/packages/docs/src/pages/components/think/index.zh-CN.md +++ b/packages/docs/src/pages/components/think/index.zh-CN.md @@ -14,23 +14,25 @@ description: 展示大模型深度思考过程。 设置状态 是否展开 插槽用法 +折叠时销毁内容 ## API ### ThinkProps -| 属性 | 说明 | 类型 | 默认值 | 版本 | -| ------------------ | ---------------- | --------------------------------------------------- | ------- | ---- | -| classes | 语义化结构类名 | [Record\](#语义化-dom) | - | - | -| styles | 语义化结构样式 | [Record\](#语义化-dom) | - | - | -| default | 内容(默认插槽) | `VNodeChild` | - | - | -| title | 状态文本 | `VNodeChild` | - | - | -| icon | 状态图标 | `VNodeChild` | - | - | -| loading | 加载中 | `boolean \| VNodeChild` | `false` | - | -| defaultExpanded | 默认是否展开 | `boolean` | `true` | - | -| expanded (v-model) | 是否展开 | `boolean` | - | - | -| onExpand | 展开事件 | `(expand: boolean) => void` | - | - | -| blink | 闪动模式 | `boolean` | - | - | +| 属性 | 说明 | 类型 | 默认值 | 版本 | +| ------------------ | ---------------------- | --------------------------------------------------- | ------- | ---- | +| classes | 语义化结构类名 | [Record\](#语义化-dom) | - | - | +| styles | 语义化结构样式 | [Record\](#语义化-dom) | - | - | +| default | 内容(默认插槽) | `VNodeChild` | - | - | +| title | 状态文本 | `VNodeChild` | - | - | +| icon | 状态图标 | `VNodeChild` | - | - | +| loading | 加载中 | `boolean \| VNodeChild` | `false` | - | +| defaultExpanded | 默认是否展开 | `boolean` | `true` | - | +| expanded (v-model) | 是否展开 | `boolean` | - | - | +| onExpand | 展开事件 | `(expand: boolean) => void` | - | - | +| blink | 闪动模式 | `boolean` | - | - | +| destroyOnHidden | 折叠时是否销毁内容节点 | `boolean` | `true` | - | ### Think 插槽 diff --git a/packages/docs/src/pages/components/thought-chain/demo/destroy-on-hidden.vue b/packages/docs/src/pages/components/thought-chain/demo/destroy-on-hidden.vue new file mode 100644 index 00000000..b194b013 --- /dev/null +++ b/packages/docs/src/pages/components/thought-chain/demo/destroy-on-hidden.vue @@ -0,0 +1,55 @@ + + + + + +节点的 `destroyOnHidden` 默认为 `true`,折叠时销毁内容节点。设为 `false` 时内容节点保留在 DOM 中,仅隐藏,可以保留流式输出与内部组件状态(例如上面输入框中已输入的内容)。 + + + +The node level `destroyOnHidden` defaults to `true`, which destroys the content node on collapse. Set it to `false` to keep the node mounted and merely hidden, preserving streaming output and inner component state (such as the text typed into the input above). + diff --git a/packages/docs/src/pages/components/thought-chain/index.en-US.md b/packages/docs/src/pages/components/thought-chain/index.en-US.md index 90053bb7..423b371d 100644 --- a/packages/docs/src/pages/components/thought-chain/index.en-US.md +++ b/packages/docs/src/pages/components/thought-chain/index.en-US.md @@ -19,6 +19,7 @@ description: The ThoughtChain component is used to visualize and track the call Slots Nested Usage Single Row +Destroy On Hidden ## API @@ -50,17 +51,18 @@ description: The ThoughtChain component is used to visualize and track the call ### ThoughtChainItemType -| Property | Description | Type | Default | Version | -| ----------- | ---------------------------------------------------- | ---------------------------------------------- | ----------- | ------- | -| content | Content of the thought node | `VNodeChild` | - | - | -| description | Description of the thought node | `VNodeChild` | - | - | -| footer | Footer of the thought node | `VNodeChild` | - | - | -| icon | Icon of the thought node, not displayed when `false` | `false \| VNodeChild` | DefaultIcon | - | -| key | Unique identifier for the thought node | `string` | - | - | -| status | Status of the thought node | `'loading' \| 'success' \| 'error' \| 'abort'` | - | - | -| title | Title of the thought node | `VNodeChild` | - | - | -| collapsible | Whether the thought node is collapsible | `boolean` | `false` | - | -| blink | Blink mode | `boolean` | - | - | +| Property | Description | Type | Default | Version | +| --------------- | ---------------------------------------------------- | ---------------------------------------------- | ----------- | ------- | +| content | Content of the thought node | `VNodeChild` | - | - | +| description | Description of the thought node | `VNodeChild` | - | - | +| footer | Footer of the thought node | `VNodeChild` | - | - | +| icon | Icon of the thought node, not displayed when `false` | `false \| VNodeChild` | DefaultIcon | - | +| key | Unique identifier for the thought node | `string` | - | - | +| status | Status of the thought node | `'loading' \| 'success' \| 'error' \| 'abort'` | - | - | +| title | Title of the thought node | `VNodeChild` | - | - | +| collapsible | Whether the thought node is collapsible | `boolean` | `false` | - | +| blink | Blink mode | `boolean` | - | - | +| destroyOnHidden | Whether to destroy the content node when collapsed | `boolean` | `true` | - | ### ThoughtChain.Item diff --git a/packages/docs/src/pages/components/thought-chain/index.zh-CN.md b/packages/docs/src/pages/components/thought-chain/index.zh-CN.md index 53f337a9..a915b0ec 100644 --- a/packages/docs/src/pages/components/thought-chain/index.zh-CN.md +++ b/packages/docs/src/pages/components/thought-chain/index.zh-CN.md @@ -20,6 +20,7 @@ description: 思维链组件用于可视化和追踪 Agent 对 Actions 和 Tools 插槽自定义 嵌套使用 单行折叠 +折叠时销毁内容 ## API @@ -51,17 +52,18 @@ description: 思维链组件用于可视化和追踪 Agent 对 Actions 和 Tools ### ThoughtChainItemType -| 属性 | 说明 | 类型 | 默认值 | 版本 | -| ----------- | --------------------------------- | ---------------------------------------------- | ----------- | ---- | -| content | 思维节点内容 | `VNodeChild` | - | - | -| description | 思维节点描述 | `VNodeChild` | - | - | -| footer | 思维节点脚注 | `VNodeChild` | - | - | -| icon | 思维节点图标,为 `false` 时不展示 | `false \| VNodeChild` | DefaultIcon | - | -| key | 思维节点唯一标识符 | `string` | - | - | -| status | 思维节点状态 | `'loading' \| 'success' \| 'error' \| 'abort'` | - | - | -| title | 思维节点标题 | `VNodeChild` | - | - | -| collapsible | 思维节点是否可折叠 | `boolean` | `false` | - | -| blink | 闪动效果 | `boolean` | - | - | +| 属性 | 说明 | 类型 | 默认值 | 版本 | +| --------------- | --------------------------------- | ---------------------------------------------- | ----------- | ---- | +| content | 思维节点内容 | `VNodeChild` | - | - | +| description | 思维节点描述 | `VNodeChild` | - | - | +| footer | 思维节点脚注 | `VNodeChild` | - | - | +| icon | 思维节点图标,为 `false` 时不展示 | `false \| VNodeChild` | DefaultIcon | - | +| key | 思维节点唯一标识符 | `string` | - | - | +| status | 思维节点状态 | `'loading' \| 'success' \| 'error' \| 'abort'` | - | - | +| title | 思维节点标题 | `VNodeChild` | - | - | +| collapsible | 思维节点是否可折叠 | `boolean` | `false` | - | +| blink | 闪动效果 | `boolean` | - | - | +| destroyOnHidden | 折叠时是否销毁内容节点 | `boolean` | `true` | - | ### ThoughtChain.Item diff --git a/packages/x/components/think/Think.tsx b/packages/x/components/think/Think.tsx index 51da8731..ac105483 100644 --- a/packages/x/components/think/Think.tsx +++ b/packages/x/components/think/Think.tsx @@ -8,7 +8,9 @@ import { defineComponent, ref, useAttrs, + vShow, watch, + withDirectives, } from "vue"; import type { SemanticType, ThinkProps, ThinkRef } from "./interface"; @@ -69,6 +71,10 @@ export const XThink = defineComponent({ type: Boolean, default: false, }, + destroyOnHidden: { + type: Boolean, + default: true, + }, }, emits: ["update:expanded", "expand"], setup(props, { slots, emit, expose }) { @@ -163,6 +169,33 @@ export const XThink = defineComponent({ const prefixCls = computed(() => props.prefixCls); + // Content is kept mounted and only toggled with `v-show` when + // `destroyOnHidden` is false, so streaming output and inner component + // state survive a collapse. + function renderContent() { + const node = ( +
+
+ {slots.default?.()} +
+
+ ); + + return props.destroyOnHidden + ? node + : withDirectives(node, [[vShow, mergedExpanded.value]]); + } + expose({ get nativeElement() { return rootRef.value as HTMLDivElement; @@ -237,23 +270,9 @@ export const XThink = defineComponent({ onLeave={onLeave} onAfterLeave={onAfterLeave} > - {mergedExpanded.value ? ( -
-
- {slots.default?.()} -
-
- ) : null} + {props.destroyOnHidden && !mergedExpanded.value + ? null + : renderContent()} ); diff --git a/packages/x/components/think/__tests__/index.test.tsx b/packages/x/components/think/__tests__/index.test.tsx index 5ff28fd9..7c3e2f29 100644 --- a/packages/x/components/think/__tests__/index.test.tsx +++ b/packages/x/components/think/__tests__/index.test.tsx @@ -206,6 +206,69 @@ describe("Think", () => { expect(chevron.attributes("style")).toContain("rotate(0deg)"); }); + it("destroys the content node on collapse by default", async () => { + const wrapper = mount(Think, { + slots: { default: () => "Toggle me" }, + }); + + expect(wrapper.find(".antd-think-content").exists()).toBe(true); + + await wrapper.find(".antd-think-status-wrapper").trigger("click"); + await nextTick(); + + expect(wrapper.find(".antd-think-content").exists()).toBe(false); + }); + + it("keeps the content node mounted when destroyOnHidden is false", () => { + const wrapper = mount(Think, { + props: { destroyOnHidden: false, defaultExpanded: false }, + slots: { default: () => "Hidden but mounted" }, + }); + + const content = wrapper.find(".antd-think-content"); + expect(content.exists()).toBe(true); + expect(wrapper.text()).toContain("Hidden but mounted"); + expect(content.element.parentElement?.getAttribute("style")).toContain( + "display: none", + ); + }); + + it("preserves inner state across collapse when destroyOnHidden is false", async () => { + const wrapper = mount(Think, { + props: { destroyOnHidden: false }, + slots: { default: () => h("input", { class: "inner-input" }) }, + }); + + const input = wrapper.find(".inner-input"); + expect(input.exists()).toBe(true); + const inputEl = input.element; + inputEl.value = "streaming"; + + await wrapper.find(".antd-think-status-wrapper").trigger("click"); + await nextTick(); + + const afterCollapse = wrapper.find(".inner-input"); + expect(afterCollapse.exists()).toBe(true); + // Same DOM node, so the value typed before collapsing survives. + expect(afterCollapse.element).toBe(inputEl); + expect(afterCollapse.element.value).toBe("streaming"); + }); + + it("keeps the content node mounted for controlled expanded when destroyOnHidden is false", async () => { + const wrapper = mount(Think, { + props: { destroyOnHidden: false, expanded: true }, + slots: { default: () => "Controlled content" }, + }); + + expect(wrapper.find(".antd-think-content").exists()).toBe(true); + + await wrapper.setProps({ expanded: false }); + await nextTick(); + + expect(wrapper.find(".antd-think-content").exists()).toBe(true); + expect(wrapper.text()).toContain("Controlled content"); + }); + it("exposes nativeElement ref", () => { const wrapper = mount(Think, { slots: { default: () => "Content" }, diff --git a/packages/x/components/think/interface.ts b/packages/x/components/think/interface.ts index df326e2a..b2a6f6ff 100644 --- a/packages/x/components/think/interface.ts +++ b/packages/x/components/think/interface.ts @@ -79,6 +79,13 @@ export interface ThinkProps extends Omit { * @descZH 标题文字闪烁动画 */ blink?: boolean; + + /** + * @desc Whether to destroy the content node when collapsed + * @descZH 折叠时是否销毁内容节点 + * @default true + */ + destroyOnHidden?: boolean; } export interface ThinkRef { diff --git a/packages/x/components/thought-chain/Node.tsx b/packages/x/components/thought-chain/Node.tsx index d6e762b5..b11387dd 100644 --- a/packages/x/components/thought-chain/Node.tsx +++ b/packages/x/components/thought-chain/Node.tsx @@ -2,7 +2,13 @@ import type { CSSProperties, PropType, VNodeChild } from "vue"; import { LeftOutlined, RightOutlined } from "@antdv-next/icons"; import { useConfig } from "antdv-next/config-provider/context"; -import { Transition, computed, defineComponent } from "vue"; +import { + Transition, + computed, + defineComponent, + vShow, + withDirectives, +} from "vue"; import type { SemanticType, @@ -261,6 +267,27 @@ export default defineComponent({ } as ThoughtChainFooterSlotInfo) : item.footer; const showContent = hasContent && (!collapsible || props.expanded); + const destroyOnHidden = item.destroyOnHidden ?? true; + + // Keep the content mounted and only toggle it with `v-show` when + // `destroyOnHidden` is false, so streaming output and inner component + // state survive a collapse. + const renderContent = () => { + const node = ( +
+
+
{contentNode}
+
+
+ ); + + return destroyOnHidden + ? node + : withDirectives(node, [[vShow, showContent]]); + }; return (
@@ -278,16 +305,7 @@ export default defineComponent({ onLeave={onLeave} onAfterLeave={onAfterLeave} > - {showContent ? ( -
-
-
{contentNode}
-
-
- ) : null} + {destroyOnHidden && !showContent ? null : renderContent()} )} diff --git a/packages/x/components/thought-chain/__tests__/index.test.tsx b/packages/x/components/thought-chain/__tests__/index.test.tsx index d9a9ce7b..7e6983c5 100644 --- a/packages/x/components/thought-chain/__tests__/index.test.tsx +++ b/packages/x/components/thought-chain/__tests__/index.test.tsx @@ -346,6 +346,88 @@ describe("ThoughtChain", () => { expect(wrapper.findAll(".antd-thought-chain-node").length).toBe(2); }); + it("keeps the item content mounted when destroyOnHidden is false", () => { + const wrapper = mount(ThoughtChain, { + props: { + items: [ + { + key: "1", + title: "Step 1", + content: h("span", "Hidden but mounted"), + collapsible: true, + destroyOnHidden: false, + }, + ], + defaultExpandedKeys: [], + }, + }); + + const content = wrapper.find(".antd-thought-chain-node-content"); + expect(content.exists()).toBe(true); + expect(wrapper.text()).toContain("Hidden but mounted"); + expect(content.element.parentElement?.getAttribute("style")).toContain( + "display: none", + ); + }); + + it("preserves item content across collapse when destroyOnHidden is false", async () => { + const wrapper = mount(ThoughtChain, { + props: { + items: [ + { + key: "1", + title: "Step 1", + content: h("input", { class: "inner-input" }), + collapsible: true, + destroyOnHidden: false, + }, + ], + defaultExpandedKeys: ["1"], + }, + }); + + const inputEl = wrapper.find(".inner-input").element; + inputEl.value = "streaming"; + + await wrapper.find(".antd-thought-chain-node-collapsible").trigger("click"); + await nextTick(); + + const afterCollapse = wrapper.find(".inner-input"); + expect(afterCollapse.exists()).toBe(true); + // Same DOM node, so the value typed before collapsing survives. + expect(afterCollapse.element).toBe(inputEl); + expect(afterCollapse.element.value).toBe("streaming"); + }); + + it("keeps the item content mounted for controlled expandedKeys when destroyOnHidden is false", async () => { + const wrapper = mount(ThoughtChain, { + props: { + items: [ + { + key: "1", + title: "Step 1", + content: h("span", "Controlled content"), + collapsible: true, + destroyOnHidden: false, + }, + ], + expandedKeys: ["1"], + }, + }); + + expect(wrapper.find(".antd-thought-chain-node-content").exists()).toBe( + true, + ); + + await wrapper.setProps({ expandedKeys: [] }); + await nextTick(); + + expect(wrapper.find(".antd-thought-chain-node-content").exists()).toBe( + true, + ); + expect(wrapper.text()).toContain("Controlled content"); + }); + it("exposes nativeElement ref", () => { const wrapper = mount(ThoughtChain, { props: { items: basicItems }, diff --git a/packages/x/components/thought-chain/interface.ts b/packages/x/components/thought-chain/interface.ts index 7cdd7545..63ac09f4 100644 --- a/packages/x/components/thought-chain/interface.ts +++ b/packages/x/components/thought-chain/interface.ts @@ -100,6 +100,13 @@ export interface ThoughtChainItemType { * @descZH 标题闪烁动画 */ blink?: boolean; + + /** + * @desc Whether to destroy the content node when collapsed + * @descZH 折叠时是否销毁内容节点 + * @default true + */ + destroyOnHidden?: boolean; } export interface ThoughtChainProps extends Omit { From 9c4f66b30726347e46c2215c38b8e4378b7fdb7c Mon Sep 17 00:00:00 2001 From: carl chen Date: Wed, 5 Aug 2026 00:37:59 +0800 Subject: [PATCH 2/2] fix(x): keep retained collapse content hidden --- .../think/demo/destroy-on-hidden.vue | 48 +++++++------- packages/x/components/think/Think.tsx | 8 +-- .../components/think/__tests__/index.test.tsx | 48 +++++++++++++- packages/x/components/thought-chain/Node.tsx | 8 +-- .../thought-chain/__tests__/index.test.tsx | 63 ++++++++++++++++++- 5 files changed, 142 insertions(+), 33 deletions(-) diff --git a/packages/docs/src/pages/components/think/demo/destroy-on-hidden.vue b/packages/docs/src/pages/components/think/demo/destroy-on-hidden.vue index 1899c50d..b268967f 100644 --- a/packages/docs/src/pages/components/think/demo/destroy-on-hidden.vue +++ b/packages/docs/src/pages/components/think/demo/destroy-on-hidden.vue @@ -1,36 +1,38 @@ -`destroyOnHidden` 默认为 `true`,折叠时销毁内容节点。设为 `false` 时内容节点保留在 DOM 中,仅隐藏,可以保留流式输出与内部组件状态(例如上面输入框中已输入的内容)。 +通过外部按钮控制展开状态,并切换 `destroyOnHidden` 对折叠内容的处理方式。 -`destroyOnHidden` defaults to `true`, which destroys the content node on collapse. Set it to `false` to keep the node mounted and merely hidden, preserving streaming output and inner component state (such as the text typed into the input above). +Control the expanded state externally and switch how collapsed content is handled with `destroyOnHidden`. diff --git a/packages/x/components/think/Think.tsx b/packages/x/components/think/Think.tsx index ac105483..7284f519 100644 --- a/packages/x/components/think/Think.tsx +++ b/packages/x/components/think/Think.tsx @@ -171,7 +171,8 @@ export const XThink = defineComponent({ // Content is kept mounted and only toggled with `v-show` when // `destroyOnHidden` is false, so streaming output and inner component - // state survive a collapse. + // state survive a collapse. Keep `vShow` attached in both modes so changing + // `destroyOnHidden` at runtime does not reset the directive lifecycle. function renderContent() { const node = (
@@ -191,9 +192,7 @@ export const XThink = defineComponent({
); - return props.destroyOnHidden - ? node - : withDirectives(node, [[vShow, mergedExpanded.value]]); + return withDirectives(node, [[vShow, mergedExpanded.value]]); } expose({ @@ -263,6 +262,7 @@ export const XThink = defineComponent({ {/* Collapsible content */} { expect(wrapper.find(".antd-think-content").exists()).toBe(false); }); - it("keeps the content node mounted when destroyOnHidden is false", () => { + it("keeps the content node mounted and toggles its visibility when destroyOnHidden is false", async () => { const wrapper = mount(Think, { props: { destroyOnHidden: false, defaultExpanded: false }, slots: { default: () => "Hidden but mounted" }, @@ -231,6 +231,20 @@ describe("Think", () => { expect(content.element.parentElement?.getAttribute("style")).toContain( "display: none", ); + + await wrapper.find(".antd-think-status-wrapper").trigger("click"); + await nextTick(); + + expect( + content.element.parentElement?.getAttribute("style") ?? "", + ).not.toContain("display: none"); + + await wrapper.find(".antd-think-status-wrapper").trigger("click"); + await nextTick(); + + expect(content.element.parentElement?.getAttribute("style")).toContain( + "display: none", + ); }); it("preserves inner state across collapse when destroyOnHidden is false", async () => { @@ -254,6 +268,38 @@ describe("Think", () => { expect(afterCollapse.element.value).toBe("streaming"); }); + it("keeps toggling after destroyOnHidden changes at runtime", async () => { + const wrapper = mount(Think, { + props: { destroyOnHidden: false }, + slots: { default: () => "Dynamic content" }, + }); + + await wrapper.setProps({ destroyOnHidden: true }); + await wrapper.find(".antd-think-status-wrapper").trigger("click"); + await nextTick(); + expect(wrapper.find(".antd-think-content").exists()).toBe(false); + + await wrapper.find(".antd-think-status-wrapper").trigger("click"); + await nextTick(); + expect(wrapper.find(".antd-think-content").exists()).toBe(true); + + await wrapper.setProps({ destroyOnHidden: false }); + const content = wrapper.find(".antd-think-content"); + + await wrapper.find(".antd-think-status-wrapper").trigger("click"); + await nextTick(); + expect(content.exists()).toBe(true); + expect(content.element.parentElement?.getAttribute("style")).toContain( + "display: none", + ); + + await wrapper.find(".antd-think-status-wrapper").trigger("click"); + await nextTick(); + expect( + content.element.parentElement?.getAttribute("style") ?? "", + ).not.toContain("display: none"); + }); + it("keeps the content node mounted for controlled expanded when destroyOnHidden is false", async () => { const wrapper = mount(Think, { props: { destroyOnHidden: false, expanded: true }, diff --git a/packages/x/components/thought-chain/Node.tsx b/packages/x/components/thought-chain/Node.tsx index b11387dd..173397f3 100644 --- a/packages/x/components/thought-chain/Node.tsx +++ b/packages/x/components/thought-chain/Node.tsx @@ -271,7 +271,8 @@ export default defineComponent({ // Keep the content mounted and only toggle it with `v-show` when // `destroyOnHidden` is false, so streaming output and inner component - // state survive a collapse. + // state survive a collapse. Keep `vShow` attached in both modes so + // changing `destroyOnHidden` at runtime does not reset its lifecycle. const renderContent = () => { const node = (
@@ -284,9 +285,7 @@ export default defineComponent({
); - return destroyOnHidden - ? node - : withDirectives(node, [[vShow, showContent]]); + return withDirectives(node, [[vShow, showContent]]); }; return ( @@ -298,6 +297,7 @@ export default defineComponent({ {hasContent && ( { expect(wrapper.findAll(".antd-thought-chain-node").length).toBe(2); }); - it("keeps the item content mounted when destroyOnHidden is false", () => { + it("keeps the item content mounted and toggles its visibility when destroyOnHidden is false", async () => { const wrapper = mount(ThoughtChain, { props: { items: [ @@ -368,6 +368,20 @@ describe("ThoughtChain", () => { expect(content.element.parentElement?.getAttribute("style")).toContain( "display: none", ); + + await wrapper.find(".antd-thought-chain-node-collapsible").trigger("click"); + await nextTick(); + + expect( + content.element.parentElement?.getAttribute("style") ?? "", + ).not.toContain("display: none"); + + await wrapper.find(".antd-thought-chain-node-collapsible").trigger("click"); + await nextTick(); + + expect(content.element.parentElement?.getAttribute("style")).toContain( + "display: none", + ); }); it("preserves item content across collapse when destroyOnHidden is false", async () => { @@ -399,6 +413,53 @@ describe("ThoughtChain", () => { expect(afterCollapse.element.value).toBe("streaming"); }); + it("keeps toggling after destroyOnHidden changes at runtime", async () => { + const createItems = (destroyOnHidden: boolean) => [ + { + key: "1", + title: "Step 1", + content: h("span", "Dynamic content"), + collapsible: true, + destroyOnHidden, + }, + ]; + const wrapper = mount(ThoughtChain, { + props: { + items: createItems(false), + defaultExpandedKeys: ["1"], + }, + }); + + await wrapper.setProps({ items: createItems(true) }); + await wrapper.find(".antd-thought-chain-node-collapsible").trigger("click"); + await nextTick(); + expect(wrapper.find(".antd-thought-chain-node-content").exists()).toBe( + false, + ); + + await wrapper.find(".antd-thought-chain-node-collapsible").trigger("click"); + await nextTick(); + expect(wrapper.find(".antd-thought-chain-node-content").exists()).toBe( + true, + ); + + await wrapper.setProps({ items: createItems(false) }); + const content = wrapper.find(".antd-thought-chain-node-content"); + + await wrapper.find(".antd-thought-chain-node-collapsible").trigger("click"); + await nextTick(); + expect(content.exists()).toBe(true); + expect(content.element.parentElement?.getAttribute("style")).toContain( + "display: none", + ); + + await wrapper.find(".antd-thought-chain-node-collapsible").trigger("click"); + await nextTick(); + expect( + content.element.parentElement?.getAttribute("style") ?? "", + ).not.toContain("display: none"); + }); + it("keeps the item content mounted for controlled expandedKeys when destroyOnHidden is false", async () => { const wrapper = mount(ThoughtChain, { props: {