Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/common
Submodule common updated 53 files
+7 −7 .github/workflows/component-ci-call.yml
+0 −23 docs/mobile/api/attachments.md
+0 −15 docs/mobile/api/chat-actionbar.md
+0 −13 docs/mobile/api/chat-content.md
+0 −33 docs/mobile/api/chat-list.md
+0 −17 docs/mobile/api/chat-loading.md
+0 −33 docs/mobile/api/chat-markdown.md
+0 −33 docs/mobile/api/chat-message.md
+0 −29 docs/mobile/api/chat-sender.md
+0 −27 docs/mobile/api/chat-thinking.md
+29 −0 docs/mobile/api/comment.en-US.md
+29 −0 docs/mobile/api/comment.md
+1 −1 docs/mobile/api/config-provider.md
+2 −2 docs/mobile/api/empty.en-US.md
+2 −2 docs/mobile/api/empty.md
+3 −3 docs/mobile/api/notice-bar.en-US.md
+3 −3 docs/mobile/api/notice-bar.md
+0 −0 docs/mobile/api/segmented.en-US.md
+4 −4 docs/mobile/api/watermark.en-US.md
+4 −4 docs/mobile/api/watermark.md
+7 −0 docs/mobile/overview.en-US.md
+7 −0 docs/mobile/overview.md
+21 −0 docs/plugins/generate-llms/README.md
+13 −7 docs/plugins/generate-llms/index.ts
+6 −0 docs/plugins/generate-llms/types.ts
+1 −1 docs/web/api/config-provider.md
+6 −6 js/tree-v1/tree-node.ts
+4 −2 js/tree-v1/tree-store.ts
+11 −6 js/tree/tree-node.ts
+4 −2 js/tree/tree-store.ts
+15 −4 style/mobile/_variables.less
+100 −0 style/mobile/components/comment/_index.less
+18 −0 style/mobile/components/comment/_var.less
+1 −1 style/mobile/components/dropdown-item/_index.less
+3 −4 style/mobile/components/guide/_var.less
+5 −5 style/mobile/components/image-viewer/_index.less
+4 −0 style/mobile/components/input/_index.less
+1 −1 style/mobile/components/loading/_var.less
+1 −1 style/mobile/components/message/_var.less
+1 −0 style/mobile/components/overlay/_index.less
+1 −1 style/mobile/components/overlay/_var.less
+1 −1 style/mobile/components/popover/_index.less
+7 −1 style/mobile/components/popup/_index.less
+1 −0 style/mobile/components/rate/_index.less
+4 −0 style/mobile/components/tag/_index.less
+3 −3 style/mobile/components/tag/_var.less
+2 −2 style/mobile/components/textarea/_var.less
+1 −1 style/mobile/components/toast/_index.less
+1 −1 style/mobile/theme/_dark.less
+4 −4 style/mobile/theme/_light.less
+59 −0 test/unit/tree-v1/disabled.test.ts
+107 −0 test/unit/tree/disabled-checking.test.ts
+22 −22 test/unit/tree/disabled.test.js
215 changes: 215 additions & 0 deletions packages/components/tree/__tests__/disabled-checking.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import React from 'react';
import { fireEvent, render, vi, waitFor } from '@test/utils';

import Tree from '../Tree';

import type { TreeProps } from '../Tree';

describe('Tree checking with disabled children', () => {
const data = [
{
value: 'parent',
label: 'Parent',
children: [
{ value: 'a', label: 'A' },
{ value: 'b', label: 'B' },
{ value: 'disabled', label: 'Disabled', disabled: true },
],
},
];

async function setup(props: Partial<TreeProps> = {}) {
const onChange = vi.fn();
const result = render(<Tree data={data} checkable expandAll transition={false} onChange={onChange} {...props} />);
await waitFor(() => expect(result.getAllByRole('checkbox')).toHaveLength(4));
return {
...result,
onChange,
inputs: result.getAllByRole('checkbox') as HTMLInputElement[],
};
}

const modes = [
['onlyLeaf', ['a', 'b']],
['all', ['parent', 'a', 'b']],
['parentFirst', ['parent']],
] as const;

modes.forEach(([valueMode, expected]) => {
it(`round-trips controlled values in ${valueMode} mode without checking disabled children`, async () => {
const onChange = vi.fn();
function ControlledTree() {
const [value, setValue] = React.useState([]);
return (
<Tree
data={data}
checkable
expandAll
transition={false}
valueMode={valueMode}
value={value}
onChange={(nextValue, context) => {
onChange(nextValue, context);
setValue(nextValue);
}}
/>
);
}
const { getAllByRole } = render(<ControlledTree />);
await waitFor(() => expect(getAllByRole('checkbox')).toHaveLength(4));
const inputs = getAllByRole('checkbox');
fireEvent.click(inputs[0]);
expect(onChange.mock.lastCall[0]).toEqual(expected);
await waitFor(() => expect(inputs[0]).toBeChecked());
expect(inputs[3]).not.toBeChecked();
fireEvent.click(inputs[0]);
expect(onChange.mock.lastCall[0]).toEqual([]);
await waitFor(() => expect(inputs[0]).not.toBeChecked());
});

it(`checks and unchecks the parent in ${valueMode} mode`, async () => {
const { inputs, onChange } = await setup({ valueMode });
fireEvent.click(inputs[0]);
expect(onChange.mock.lastCall[0]).toEqual(expected);
expect(onChange.mock.lastCall[1].node.value).toBe('parent');
await waitFor(() => expect(inputs[0]).toBeChecked());
expect(inputs[0].closest('label')).not.toHaveClass('t-is-indeterminate');
expect(inputs[1]).toBeChecked();
expect(inputs[2]).toBeChecked();
expect(inputs[3]).not.toBeChecked();
expect(inputs[3]).toBeDisabled();

fireEvent.click(inputs[0]);
expect(onChange.mock.lastCall[0]).toEqual([]);
await waitFor(() => expect(inputs[0]).not.toBeChecked());
expect(inputs[0].closest('label')).not.toHaveClass('t-is-indeterminate');
expect(inputs[1]).not.toBeChecked();
expect(inputs[2]).not.toBeChecked();
});

it(`derives full and partial selection from enabled children in ${valueMode} mode`, async () => {
const { inputs, onChange } = await setup({ valueMode });
fireEvent.click(inputs[1]);
expect(onChange.mock.lastCall[0]).toEqual(['a']);
await waitFor(() => expect(inputs[0].closest('label')).toHaveClass('t-is-indeterminate'));

fireEvent.click(inputs[2]);
expect(onChange.mock.lastCall[0]).toEqual(expected);
await waitFor(() => expect(inputs[0]).toBeChecked());
expect(inputs[0].closest('label')).not.toHaveClass('t-is-indeterminate');

fireEvent.click(inputs[1]);
expect(onChange.mock.lastCall[0]).toEqual(['b']);
await waitFor(() => expect(inputs[0].closest('label')).toHaveClass('t-is-indeterminate'));
expect(inputs[0]).not.toBeChecked();
});

it(`does not automatically check a parent with only disabled children in ${valueMode} mode`, async () => {
const { inputs, onChange } = await setup({
valueMode,
data: [
{
...data[0],
children: data[0].children.map((child) => ({
...child,
disabled: true,
})),
},
],
});
expect(inputs[0]).not.toBeChecked();
expect(inputs[0].closest('label')).not.toHaveClass('t-is-indeterminate');
fireEvent.click(inputs[0]);
expect(onChange.mock.lastCall[0]).toEqual([]);
expect(inputs[0]).not.toBeChecked();
});
});

it.each(['all', 'parentFirst'] as const)('preserves a selected disabled child in %s mode', async (valueMode) => {
const { inputs, onChange } = await setup({
valueMode,
defaultValue: ['disabled'],
});
expect(inputs[0]).not.toBeChecked();
expect(inputs[0].closest('label')).not.toHaveClass('t-is-indeterminate');
expect(inputs[3]).toBeChecked();
fireEvent.click(inputs[0]);
expect(onChange.mock.lastCall[0]).toEqual(
valueMode === 'all' ? ['parent', 'a', 'b', 'disabled'] : ['parent', 'disabled'],
);
await waitFor(() => expect(inputs[0]).toBeChecked());
fireEvent.click(inputs[0]);
expect(onChange.mock.lastCall[0]).toEqual(['disabled']);
await waitFor(() => expect(inputs[0]).not.toBeChecked());
expect(inputs[0].closest('label')).not.toHaveClass('t-is-indeterminate');
expect(inputs[3]).toBeChecked();
});

it('excludes disabled branches and their descendants from parent selection', async () => {
const onChange = vi.fn();
const { getAllByRole } = render(
<Tree
checkable
expandAll
transition={false}
valueMode="all"
data={[
{
value: 'root',
children: [
{ value: 'enabled' },
{
value: 'branch',
disabled: true,
children: [{ value: 'leaf' }],
},
],
},
]}
onChange={onChange}
/>,
);
await waitFor(() => expect(getAllByRole('checkbox')).toHaveLength(4));
const inputs = getAllByRole('checkbox');
fireEvent.click(inputs[0]);
expect(onChange.mock.lastCall[0]).toEqual(['root', 'enabled']);
await waitFor(() => expect(inputs[0]).toBeChecked());
expect(inputs[2]).not.toBeChecked();
expect(inputs[3]).not.toBeChecked();
expect(inputs[2]).toBeDisabled();
expect(inputs[3]).toBeDisabled();
});

it('propagates full selection through nested enabled branches', async () => {
const onChange = vi.fn();
const { getAllByRole } = render(
<Tree
checkable
expandAll
transition={false}
valueMode="all"
data={[{ value: 'root', label: 'Root', children: data }]}
onChange={onChange}
/>,
);
await waitFor(() => expect(getAllByRole('checkbox')).toHaveLength(5));
const inputs = getAllByRole('checkbox') as HTMLInputElement[];
fireEvent.click(inputs[0]);
expect(onChange.mock.lastCall[0]).toEqual(['root', 'parent', 'a', 'b']);
await waitFor(() => expect(inputs[0]).toBeChecked());
expect(inputs[1]).toBeChecked();
expect(inputs[0].closest('label')).not.toHaveClass('t-is-indeterminate');
expect(inputs[1].closest('label')).not.toHaveClass('t-is-indeterminate');
expect(inputs[4]).not.toBeChecked();
});

it('keeps strict checking independent of child selection', async () => {
const { inputs, onChange } = await setup({ checkStrictly: true });
fireEvent.click(inputs[0]);
expect(onChange.mock.lastCall[0]).toEqual(['parent']);
await waitFor(() => expect(inputs[0]).toBeChecked());
expect(inputs[1]).not.toBeChecked();
expect(inputs[2]).not.toBeChecked();
expect(inputs[3]).not.toBeChecked();
});
});
Loading