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
37 changes: 17 additions & 20 deletions packages/components/steps/Steps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,8 @@ const Steps = forwardRefWithStatics(
return item.status;
}
// value 不存在时,使用 index 进行区分每一个步骤
if (item.value === undefined) {
if (sequence === 'positive' && typeof current === 'number' && index < current) {
return 'finish';
}
if (sequence === 'reverse' && typeof current === 'number' && index > current) {
return 'finish';
}
if (item.value === undefined && typeof current === 'number' && index < current) {
return 'finish';
}

// value 存在,找匹配位置
Expand All @@ -70,10 +65,7 @@ const Steps = forwardRefWithStatics(
console.warn('TDesign Steps Warn: The current `value` is not exist.');
return 'default';
}
if (sequence === 'positive' && index < matchIndex) {
return 'finish';
}
if (sequence === 'reverse' && index > matchIndex) {
if (index < matchIndex) {
return 'finish';
}
}
Expand All @@ -83,27 +75,32 @@ const Steps = forwardRefWithStatics(
}
return 'default';
},
[current, sequence, indexMap],
[current, indexMap],
);

const stepItemList = useMemo<React.ReactNode[]>(() => {
if (options) {
const optionsDisplayList = sequence === 'reverse' ? options.reverse() : options;
return options.map<React.ReactNode>((item, index) => {
const stepIndex = sequence === 'reverse' ? optionsDisplayList.length - index - 1 : index;
return <StepItem key={index} {...item} index={stepIndex} status={handleStatus(item, index)} />;
const optionsDisplayList = sequence === 'reverse' ? [...options].reverse() : options;
const optionsDisplayListLength = optionsDisplayList.length;

return optionsDisplayList.map<React.ReactNode>((item, index) => {
const stepIndex = sequence === 'reverse' ? optionsDisplayListLength - index - 1 : index;
return (
<StepItem key={item.value ?? index} {...item} index={stepIndex} status={handleStatus(item, stepIndex)} />
);
});
}

const childrenList = React.Children.toArray(children);
const childrenDisplayList = sequence === 'reverse' ? childrenList.reverse() : childrenList;
const childrenDisplayList = sequence === 'reverse' ? [...childrenList].reverse() : childrenList;
const childrenDisplayListLength = childrenDisplayList.length;

return childrenList.map((child: React.ReactElement<StepItemProps>, index: number) => {
const stepIndex = sequence === 'reverse' ? childrenDisplayList.length - index - 1 : index;
return childrenDisplayList.map((child: React.ReactElement<StepItemProps>, index: number) => {
const stepIndex = sequence === 'reverse' ? childrenDisplayListLength - index - 1 : index;
return React.cloneElement(child, {
...child.props,
index: stepIndex,
status: handleStatus(child.props, index),
status: handleStatus(child.props, stepIndex),
});
});
}, [options, children, sequence, handleStatus]);
Expand Down
59 changes: 59 additions & 0 deletions packages/components/steps/__tests__/steps.branches.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import { render, waitFor } from '@test/utils';
import { vi } from 'vitest';
import Steps from '../Steps';
import StepItem from '../StepItem';

const TEST_ROOT_ID = 'step-test-branches';

describe('Steps branches', () => {
test('options reverse order and index mapping', async () => {
const opts = [
{ title: 'first', content: 'c1', value: 0 },
{ title: 'second', content: 'c2', value: 1 },
{ title: 'third', content: 'c3', value: 2 },
];
const { getByTestId } = render(
<div data-testid={TEST_ROOT_ID}>
<Steps options={opts} sequence="reverse" current={1} />
</div>,
);
const root = await waitFor(() => getByTestId(TEST_ROOT_ID));
const titles = Array.from(root.querySelectorAll('.t-steps-item__title')).map((n) => n.textContent);
// when reverse, display order should be same length but titles still present
expect(titles.length).toBe(3);
expect(titles).toContain('first');
expect(titles).toContain('second');
expect(titles).toContain('third');
});

test('children cloneElement branch and FINISH current', async () => {
const onChange = vi.fn?.() ?? (() => undefined);
const { getByTestId } = render(
<div data-testid={TEST_ROOT_ID}>
<Steps current={'FINISH'} onChange={onChange}>
<StepItem title="A" content="a" value={0} />
<StepItem title="B" content="b" value={1} />
</Steps>
</div>,
);
const root = await waitFor(() => getByTestId(TEST_ROOT_ID));
// all items should be finish
const finish = root.querySelectorAll('.t-steps-item--finish');
expect(finish.length).toBe(2);
});

test('StepItem default icon number rendered when icon=true and theme=default', async () => {
const { getByTestId } = render(
<div data-testid={TEST_ROOT_ID}>
<Steps current={0}>
<StepItem title="X" content="x" index={0} icon={true as any} status={'default'} />
</Steps>
</div>,
);
const root = await waitFor(() => getByTestId(TEST_ROOT_ID));
const number = root.querySelector('.t-steps-item__icon--number');
expect(number).not.toBeNull();
expect(number).toHaveTextContent('1');
});
});
90 changes: 90 additions & 0 deletions packages/components/steps/__tests__/steps.extra.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';
import { render, waitFor, fireEvent, vi } from '@test/utils';
import Steps from '../Steps';
import StepItem from '../StepItem';
import StepsContext from '../StepsContext';

const TEST_ROOT_ID = 'step-test-root-extra';

const defaultOptions = [
{ title: '0', content: '这里', value: 0 },
{ title: '1', content: '这里', value: 1 },
{ title: '2', content: '这里', value: 2 },
];

describe('Steps extra branches', () => {
test("current='FINISH' -> all items finish (options)", async () => {
const { getByTestId } = render(
<div data-testid={TEST_ROOT_ID}>
<Steps current={'FINISH'} options={defaultOptions} />
</div>,
);
const root = await waitFor(() => getByTestId(TEST_ROOT_ID));
const finishes = root.querySelectorAll('.t-steps-item--finish');
expect(finishes.length).toBe(defaultOptions.length);
});

test('item.status override (error) renders error class', async () => {
const opts = [
{ title: 'a', content: 'a', value: 0, status: 'error' as any },
{ title: 'b', content: 'b', value: 1 },
];
const { getByTestId } = render(
<div data-testid={TEST_ROOT_ID}>
<Steps current={0} options={opts} />
</div>,
);
const root = await waitFor(() => getByTestId(TEST_ROOT_ID));
const errorItem = root.querySelector('.t-steps-item--error');
expect(errorItem).not.toBeNull();
});

test('current value not exist triggers console.warn and defaults to wait', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
const opts = [
{ title: 'a', content: 'a', value: 'a' },
{ title: 'b', content: 'b', value: 'b' },
];
const { getByTestId } = render(
<div data-testid={TEST_ROOT_ID}>
<Steps current={'not-exist'} options={opts as any} />
</div>,
);
const root = await waitFor(() => getByTestId(TEST_ROOT_ID));
expect(warnSpy).toHaveBeenCalledWith('TDesign Steps Warn: The current `value` is not exist.');
const waitItems = root.querySelectorAll('.t-steps-item--wait');
// both should be in default/wait state
expect(waitItems.length).toBe(opts.length);
warnSpy.mockRestore();
});

test('status=process prevents click triggering onChange', async () => {
const onChange = vi.fn();
const { getByTestId } = render(
<div data-testid={TEST_ROOT_ID}>
<StepsContext.Provider value={{ current: 0, theme: 'default', readonly: false, onChange }}>
<StepItem title="t" index={0} status={'process'} />
</StepsContext.Provider>
</div>,
);
const root = await waitFor(() => getByTestId(TEST_ROOT_ID));
const inner = root.querySelector('.t-steps-item__inner');
fireEvent.click(inner);
expect(onChange).not.toHaveBeenCalled();
});

test('icon=false results in no icon content', async () => {
const { getByTestId } = render(
<div data-testid={TEST_ROOT_ID}>
<StepsContext.Provider value={{ current: 0, theme: 'default', readonly: false, onChange: null }}>
<StepItem title="t" index={0} status={'default'} icon={false as any} />
</StepsContext.Provider>
</div>,
);
const root = await waitFor(() => getByTestId(TEST_ROOT_ID));
const icon = root.querySelector('.t-steps-item__icon');
// icon container exists but should have no child nodes when icon=false
expect(icon).not.toBeNull();
expect(icon.childNodes.length).toBe(0);
});
});
Loading
Loading