Skip to content

Commit

Permalink
fix(steps): fix some steps bugs and add test cases (#2066)
Browse files Browse the repository at this point in the history
  • Loading branch information
gavin-hao committed Jun 9, 2022
1 parent 1273841 commit 610eb51
Show file tree
Hide file tree
Showing 9 changed files with 426 additions and 209 deletions.
43 changes: 43 additions & 0 deletions src/steps/Step.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { CheckOutlined } from '@gio-design/icons';
import { usePrefixCls } from '@gio-design/utils';
import classnames from 'classnames';
import React, { useMemo } from 'react';
import { StepProps } from './interface';

const Step = (props: StepProps & { stepIndex?: number; active?: number }) => {
const { title, status, prefix, onClick, disabled = false, className, stepIndex = 1, active } = props;
const prefixCls = usePrefixCls('steps-item');
const cls = classnames(`${prefixCls}`, className, `${prefixCls}-${status}`, {
[`${prefixCls}-active`]: active,
[`${prefixCls}-disabled`]: disabled,
});

const stepIcon = useMemo(() => {
if (!React.isValidElement(prefix) && status === 'finish') {
return <CheckOutlined className={`${prefixCls}-finish-icon`} />;
}
return prefix;
}, [prefix, status, prefixCls]);
return (
<button
className={cls}
disabled={disabled}
type="button"
data-testid={`step-${stepIndex}`}
value={stepIndex}
tabIndex={onClick && !disabled ? 0 : undefined}
onClick={() => onClick?.(stepIndex)}
>
<span
className={classnames(`${prefixCls}-prefix`, {
[`${prefixCls}-prefix-none`]: !stepIcon,
[`${prefixCls}-prefix-only`]: !title,
})}
>
{stepIcon}
</span>
{title}
</button>
);
};
export default Step;
122 changes: 47 additions & 75 deletions src/steps/Steps.tsx
Original file line number Diff line number Diff line change
@@ -1,112 +1,84 @@
import React, { DOMAttributes, useEffect } from 'react';
import React, { DOMAttributes } from 'react';
import classnames from 'classnames';
import { isNil } from 'lodash';
import { CheckOutlined } from '@gio-design/icons';
import { usePrefixCls } from '@gio-design/utils';
import { StepsProps } from './interface';
import { TabProps } from '../tabs/interface';
import { StepProps, StepsProps } from './interface';
import useControlledState from '../utils/hooks/useControlledState';
import Tab from '../tabs/Tab';
import TabButton from '../tabs/TabButton';
import Step from './Step';
import { WithCommonProps } from '../utils/interfaces';
import WithRef from '../utils/withRef';
import StepsContext from '../tabs/context';

export const Steps = WithRef<
HTMLDivElement,
WithCommonProps<StepsProps> & Omit<DOMAttributes<HTMLDivElement>, 'onChange'>
>(
(
{
current = 1,
value,
current,
defaultCurrent = 0,
onChange,
className: classname,
children,
size = 'normal',
className: customClassName,
...restProps
}: WithCommonProps<StepsProps> & Omit<DOMAttributes<HTMLDivElement>, 'onChange'>,
ref?
) => {
const [activeValue, setActiveValue] = useControlledState<React.Key>(value, current - 1);
const prefixCls = usePrefixCls('tabs');
const tabClasses = classnames(classname, prefixCls);

const elementList = React.Children.toArray(children).filter(
(node) => React.isValidElement(node) && node.type === Tab
);
const validCurrent = !isNil(current) && current < 0 ? defaultCurrent : current;
const [mergedCurrent, setCurrent] = useControlledState<number>(validCurrent, defaultCurrent);
const prefixCls = usePrefixCls('steps');
const stepCls = classnames(prefixCls, classname, customClassName, {
[`${prefixCls}-${size}`]: size,
});

useEffect(() => {
let currentVal: number;
if (current > elementList.length) {
currentVal = elementList?.length + 1;
} else if (current <= 1) {
currentVal = 1;
} else {
currentVal = current;
}
setActiveValue(currentVal - 1);
}, [current, elementList.length, setActiveValue]);
const childrenToArray = () =>
React.Children.toArray(children).filter(
(node) => React.isValidElement(node) && node.type === Step
) as React.ReactElement<StepProps & { stepIndex?: number }>[];

const onClick = (v: React.Key) => {
if (v <= current - 1) {
setActiveValue(v);
onChange?.(v);
const onStepClick = (next: number) => {
if (next < mergedCurrent) {
setCurrent(next);
onChange?.(next);
}
};

const tabs = elementList.map((tab: React.ReactElement<WithCommonProps<TabProps>>, index) => {
let prefix = null;
let className = '';
if (index < current - 1) {
prefix = <CheckOutlined />;
className = 'complete';
} else if (index === current - 1) {
className = 'process';
} else if (index >= current) {
className = 'uncomplete';
const steps = childrenToArray().map((child: React.ReactElement<StepProps>, index) => {
const { status, onClick: propOnClick, ...restStepProps } = child.props;
const stepIndex = index + 1;
const childProps: StepProps & { stepIndex?: number; active?: boolean } = {
stepIndex,
status,
onClick: () => {
onStepClick(stepIndex);
propOnClick?.(stepIndex);
},
...restStepProps,
};
if (!child.props.status) {
if (stepIndex === mergedCurrent) {
childProps.status = 'process';
} else if (stepIndex < mergedCurrent) {
childProps.status = 'finish';
} else {
childProps.status = 'pending';
}
}
return (
<span className={`${prefixCls}-tablist-stepbar stepbar-${className}`} key={tab.props.value}>
<TabButton
value={tab.props.value || index}
size={size}
onClick={onClick}
prefix={prefix}
active={activeValue === index}
disabled={tab.props.disabled}
>
{tab.props.label}
</TabButton>
</span>
);
});

const tabPanels = elementList.map((tab: React.ReactElement<WithCommonProps<TabProps>>, index) => {
if (isNil(tab.props.value)) {
return React.cloneElement(<Tab />, { ...tab.props, value: index, key: tab.props.value });
}
return React.cloneElement(<Tab />, { ...tab.props, key: tab.props.value });
childProps.active = stepIndex === mergedCurrent;

return React.cloneElement(child, { ...childProps, key: stepIndex });
});

return (
<StepsContext.Provider value={{ activeValue }}>
<div className={tabClasses} data-testid="steps" ref={ref} {...restProps}>
<div data-testid="tablist" className={`${prefixCls}-tablist steps-container`}>
{tabs}
</div>
<div data-testid="tabpanels" className={`${prefixCls}-tabpanels`}>
{tabPanels}
</div>
<div className={stepCls} data-testid="steps" ref={ref} {...restProps}>
<div data-testid="step-bars" className={`${prefixCls}-container`}>
{steps}
</div>
</StepsContext.Provider>
</div>
);
}
);
Steps.defaultProps = {
current: 1,
size: 'normal',
};

Steps.displayName = 'Steps';

Expand Down
76 changes: 76 additions & 0 deletions src/steps/__tests__/Steps.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import Steps from '..';

describe('Test Steps', () => {
it('should render correctly', () => {
const { container } = render(
<Steps>
<Steps.Step title="Step 1" />
<Steps.Step title="Step 2" />
</Steps>
);
expect(container.querySelector('.gio-steps')).toBeInTheDocument();
});
it('should render correctly with current below zaro ', () => {
const { container } = render(
<Steps current={-1}>
<Steps.Step title="Step 1" />
<Steps.Step title="Step 2" />
</Steps>
);
expect(container.querySelector('.gio-steps-item-pending')).toHaveTextContent('Step 1');
});
it('renders with prop size', () => {
const { container } = render(
<Steps size="small">
<Steps.Step title="Step 1" />
<Steps.Step title="Step 2" />
</Steps>
);
expect(container.querySelector('.gio-steps-small')).toBeInTheDocument();
});
it('can click step', () => {
const stepClick = jest.fn();
const { container } = render(
<Steps defaultCurrent={2}>
<Steps.Step title="Step 1" onClick={stepClick} />
<Steps.Step title="Step 2" />
</Steps>
);
fireEvent.click(screen.getByText('Step 1'));
expect(container.querySelector('.gio-steps-item-active')).toHaveTextContent('Step 1');
expect(stepClick).toHaveBeenCalledTimes(1);
});
it('should change current', () => {
const change = jest.fn();
const { container } = render(
<Steps defaultCurrent={2} onChange={change}>
<Steps.Step title="Step 1" />
<Steps.Step title="Step 2" />
</Steps>
);
expect(container.querySelector('.gio-steps-item-active')).toHaveTextContent('Step 2');
fireEvent.click(screen.getByText('Step 1'));
expect(change).toHaveBeenCalledWith(1);
expect(change).toHaveBeenCalledTimes(1);
expect(container.querySelector('.gio-steps-item-active')).toHaveTextContent('Step 1');
});

it('render disabled Step', () => {
const { container } = render(
<Steps>
<Steps.Step title="Step 1" disabled />
<Steps.Step title="Step 2" />
</Steps>
);
expect(container.querySelector('.gio-steps-item-disabled')).toBeInTheDocument();
expect(container.querySelector('.gio-steps-item-disabled')).not.toHaveAttribute('tabIndex');
});

it('render Step with icon', () => {
const { container } = render(<Steps.Step title="Step 1" prefix={<span>1</span>} />);
expect(container.querySelector('.gio-steps-item-prefix').firstChild).toHaveTextContent('1');
fireEvent.click(screen.getByText('Step 1'));
});
});

1 comment on commit 610eb51

@vercel
Copy link

@vercel vercel bot commented on 610eb51 Jun 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

gio-design – ./

gio-design-growingio.vercel.app
gio-design-git-master-growingio.vercel.app
gio-design.vercel.app

Please sign in to comment.