Skip to content

Commit 1a066a3

Browse files
authored
feat: add keyboard support (#332)
* feat: support key switch * test: add test case
1 parent 9988062 commit 1a066a3

File tree

5 files changed

+101
-62
lines changed

5 files changed

+101
-62
lines changed

.husky/pre-commit

Lines changed: 0 additions & 4 deletions
This file was deleted.

docs/examples/inline.jsx

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,41 @@ export default () => {
66
const [current, setCurrent] = useState(0);
77

88
return (
9-
<Steps
10-
type="inline"
11-
current={current}
12-
onChange={setCurrent}
13-
items={[
14-
{
15-
title: '开发',
16-
description: '开发阶段:开发中',
17-
},
18-
{
19-
title: '测试',
20-
description: '测试阶段:测试中',
21-
},
22-
{
23-
title: '预发',
24-
description: '预发阶段:预发中',
25-
},
26-
{
27-
title: '发布',
28-
description: '发布阶段:发布中',
29-
}
30-
]}
31-
itemRender={(item, stepItem) => (
32-
React.cloneElement(stepItem, { title: item.description })
33-
)}
34-
/>
35-
)
9+
<>
10+
<button
11+
onClick={() => {
12+
setCurrent(0);
13+
}}
14+
>
15+
Current: {current}
16+
</button>
17+
18+
<br />
19+
20+
<Steps
21+
type="inline"
22+
current={current}
23+
onChange={setCurrent}
24+
items={[
25+
{
26+
title: '开发',
27+
description: '开发阶段:开发中',
28+
},
29+
{
30+
title: '测试',
31+
description: '测试阶段:测试中',
32+
},
33+
{
34+
title: '预发',
35+
description: '预发阶段:预发中',
36+
},
37+
{
38+
title: '发布',
39+
description: '发布阶段:发布中',
40+
},
41+
]}
42+
itemRender={(item, stepItem) => React.cloneElement(stepItem, { title: item.description })}
43+
/>
44+
</>
45+
);
3646
};

src/Step.tsx

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint react/prop-types: 0 */
22
import * as React from 'react';
33
import classNames from 'classnames';
4+
import KeyCode from 'rc-util/lib/KeyCode';
45
import type { Status, Icons } from './interface';
56
import type { StepIconRender, ProgressDotRender } from './Steps';
67

@@ -29,7 +30,7 @@ export interface StepProps {
2930
onStepClick?: (index: number) => void;
3031
progressDot?: ProgressDotRender | boolean;
3132
stepIcon?: StepIconRender;
32-
render?: (stepItem: React.ReactNode) => React.ReactNode;
33+
render?: (stepItem: React.ReactElement) => React.ReactNode;
3334
}
3435

3536
function Step(props: StepProps) {
@@ -58,14 +59,31 @@ function Step(props: StepProps) {
5859
...restProps
5960
} = props;
6061

61-
const onInternalClick: React.MouseEventHandler<HTMLDivElement> = (...args) => {
62-
if (onClick) {
63-
onClick(...args);
64-
}
62+
// ========================= Click ==========================
63+
const clickable = !!onStepClick && !disabled;
6564

66-
onStepClick(stepIndex);
67-
};
65+
const accessibilityProps: {
66+
role?: string;
67+
tabIndex?: number;
68+
onClick?: React.MouseEventHandler<HTMLDivElement>;
69+
onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
70+
} = {};
71+
if (clickable) {
72+
accessibilityProps.role = 'button';
73+
accessibilityProps.tabIndex = 0;
74+
accessibilityProps.onClick = (e) => {
75+
onClick?.(e);
76+
onStepClick(stepIndex);
77+
};
78+
accessibilityProps.onKeyDown = (e) => {
79+
const { which } = e;
80+
if (which === KeyCode.ENTER || which === KeyCode.SPACE) {
81+
onStepClick(stepIndex);
82+
}
83+
};
84+
}
6885

86+
// ========================= Render =========================
6987
const renderIconNode = () => {
7088
let iconNode;
7189
const iconClassName = classNames(`${prefixCls}-icon`, `${iconPrefix}icon`, {
@@ -119,25 +137,19 @@ function Step(props: StepProps) {
119137

120138
const mergedStatus = status || 'wait';
121139

122-
const classString = classNames(`${prefixCls}-item`, `${prefixCls}-item-${mergedStatus}`, className, {
123-
[`${prefixCls}-item-custom`]: icon,
124-
[`${prefixCls}-item-active`]: active,
125-
[`${prefixCls}-item-disabled`]: disabled === true,
126-
});
140+
const classString = classNames(
141+
`${prefixCls}-item`,
142+
`${prefixCls}-item-${mergedStatus}`,
143+
className,
144+
{
145+
[`${prefixCls}-item-custom`]: icon,
146+
[`${prefixCls}-item-active`]: active,
147+
[`${prefixCls}-item-disabled`]: disabled === true,
148+
},
149+
);
127150
const stepItemStyle = { ...style };
128151

129-
const accessibilityProps: {
130-
role?: string;
131-
tabIndex?: number;
132-
onClick?: React.MouseEventHandler<HTMLDivElement>;
133-
} = {};
134-
if (onStepClick && !disabled) {
135-
accessibilityProps.role = 'button';
136-
accessibilityProps.tabIndex = 0;
137-
accessibilityProps.onClick = onInternalClick;
138-
}
139-
140-
let stepNode: React.ReactNode = (
152+
let stepNode: React.ReactElement = (
141153
<div {...restProps} className={classString} style={stepItemStyle}>
142154
<div onClick={onClick} {...accessibilityProps} className={`${prefixCls}-item-container`}>
143155
<div className={`${prefixCls}-item-tail`}>{tailContent}</div>
@@ -160,12 +172,11 @@ function Step(props: StepProps) {
160172
</div>
161173
);
162174

163-
164175
if (render) {
165-
stepNode = render(stepNode) || null;
176+
stepNode = (render(stepNode) || null) as React.ReactElement;
166177
}
167178

168-
return stepNode as React.ReactElement;
179+
return stepNode;
169180
}
170181

171182
export default Step;

src/Steps.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export interface StepsProps {
4040
initial?: number;
4141
icons?: Icons;
4242
items?: StepProps[];
43-
itemRender?: (item: StepProps, stepItem: React.ReactNode) => React.ReactNode;
43+
itemRender?: (item: StepProps, stepItem: React.ReactElement) => React.ReactNode;
4444
onChange?: (current: number) => void;
4545
}
4646

tests/index.test.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,7 @@ describe('Steps', () => {
257257
disabled: true,
258258
},
259259
]}
260-
itemRender={(item, stepItem) => (
261-
React.cloneElement(stepItem, { title: item.description })
262-
)}
260+
itemRender={(item, stepItem) => React.cloneElement(stepItem, { title: item.description })}
263261
/>,
264262
);
265263
expect(wrapper).toMatchSnapshot();
@@ -470,4 +468,28 @@ describe('Steps', () => {
470468
wrapper.find('.rc-steps-item-container').at(2).simulate('click');
471469
expect(onChange).not.toBeCalled();
472470
});
471+
472+
it('key board support', () => {
473+
const onChange = jest.fn();
474+
const wrapper = mount(
475+
<Steps
476+
current={0}
477+
onChange={onChange}
478+
items={[
479+
{
480+
title: 'Finished',
481+
description: 'This is a description',
482+
},
483+
{
484+
title: 'Waiting',
485+
description: 'This is a description',
486+
},
487+
]}
488+
/>,
489+
);
490+
491+
wrapper.find('[role="button"]').at(1).simulate('keydown', { which: 13 });
492+
493+
expect(onChange).toBeCalledWith(1);
494+
});
473495
});

0 commit comments

Comments
 (0)