浏览代码

feat: [Steps] support a11y (#426)

* feat: [Steps] support a11y

* docs: [Steps] add a11y

* docs: alter spell

* feat: steps support aria-role and replace keyCode with key
Neptune 3 年之前
父节点
当前提交
c584cbdf39

+ 10 - 0
content/navigation/steps/index-en-US.md

@@ -311,6 +311,13 @@ class App extends React.Component {
 }
 ```
 
+## Accessibility
+
+### ARIA
+
+- Steps and Step components support passing in the `aria-label` attribute to represent the description of Steps and Steps
+- Step component has an `aria-current` `step` attribute, indicating that this is a step in the step bar
+
 ## API reference
 
 ### Steps
@@ -334,12 +341,15 @@ Step in the step bar.
 
 | Parameters | Instructions | type | Default | Version |
 | --- | --- | --- | --- | --- |
+| aria-label | Container aria-label   | React.AriaAttributes["aria-label"] |  |   |
 | description | Detailed description of steps, optional | ReactNode |  | - |  |
 | icon | Type of step icon, optional | ReactNode |  | - |  |
+| role      | Container role  | React.AriaRole | -  |    |
 | status | Specify the state. When this property is not configured, the `current`of Steps is used to automatically specify the state. Optional: `wait`,`process`,`finish`,`error`,`warning` | string | wait |  |
 | style     | CSS Style                                                                          | CSSProperties |            |    |
 | title | Title | ReactNode |  | - |  |
 | onClick | Callback of click | function | - |  |
+| onKeyDown     | Callback ok keyDown  | function | -   |    |
 
 ## Design Tokens
 

+ 29 - 19
content/navigation/steps/index.md

@@ -313,38 +313,48 @@ class App extends React.Component {
 }
 ```
 
+## Accessibility
+
+### ARIA
+
+- Steps、Step组件支持传入`aria-label`属性,来表示Steps和Step的描述
+- Step组件具有 `aria-current` `step` 属性,表示这是步骤条内的一步
+
 ## API 参考
 
 ### Steps
 
 整体步骤条。
 
-| 参数      | 说明                                                                          | 类型   | 默认值     | 版本    |
-| --------- | ----------------------------------------------------------------------------- | ------ | ---------- | ------ |
-| className | 类名                                                                          | string |            |    |
-| current   | 指定当前步骤,从 0 开始记数。在子 Step 元素中,可以通过 `status` 属性覆盖状态 | number | 0          |    |
+| 参数      | 说明   | 类型   | 默认值     | 版本    |
+| --------- | ------- | ------ | ----- | ----- |
+| className | 类名  | string |    |   |
+| current   | 指定当前步骤,从 0 开始记数。在子 Step 元素中,可以通过 `status` 属性覆盖状态 | number | 0  |    |
 | direction | 指定步骤条方向。目前支持水平(`horizontal`)和竖直(`vertical`)两种方向      | string | horizontal |    |
-| hasLine   | 步骤条类型为basic时,可控制是否显示连接线                                             | boolean | true       | 1.18.0    |
-| initial   | 起始序号,从 0 开始记数                                                       | number | 0          |    |
-| status    | 指定当前步骤的状态,可选 `wait`、`process`、`finish`、`error`、`warning`           | string | process    |    |
+| hasLine   | 步骤条类型为basic时,可控制是否显示连接线  | boolean | true       | 1.18.0    |
+| initial   | 起始序号,从 0 开始记数   | number | 0   |    |
+| status    | 指定当前步骤的状态,可选 `wait`、`process`、`finish`、`error`、`warning`  | string | process    |    |
 | size      | 对于简单步骤条和导航步骤条,可选尺寸尺寸,值为`small`、`default`   | string  | `default` | 1.18.0  |
-| style     | 样式                                                                          | CSSProperties |            |    |
-| type     | 步骤条类型,可选 `fill`、`basic`、`nav`                                             | string | fill       | 1.18.0    |
-| onChange  | 改变步骤条的回调                                              | (index: number) => void | -       | 1.29.0    |
+| style     | 样式   | CSSProperties |   |    |
+| type     | 步骤条类型,可选 `fill`、`basic`、`nav`  | string | fill | 1.18.0    |
+| onChange  | 改变步骤条的回调 | (index: number) => void | -  | 1.29.0    |
 
 ### Steps.Step
 
 步骤条内的每一个步骤。
 
-| 参数        | 说明                                                                                                          | 类型              | 默认值 | 版本   |
-| ----------- | ------------------------------------------------------------------------------------------------------------- | ----------------- | ------ | ---- |
-| className | 类名                                                                          | string |            |    |
-| description | 步骤的详情描述,可选                                                                                          | ReactNode | -      |    |
-| icon        | 步骤图标的类型,可选                                                                                          | ReactNode | -      |    |
-| status      | 指定状态。当不配置该属性时,会使用 Steps 的 `current` 来自动指定状态。可选:`wait`、`process`、`finish`、`error`、`warning` | string            | wait   |    |
-| style     | 样式                                                                          | CSSProperties |            |    |
-| title       | 标题                                                                                                        | ReactNode | -      |    |
-| onClick     | 点击回调                                                                                                          | function | -      |    |
+| 参数  | 说明  | 类型  | 默认值 | 版本  |
+| ---- | -------| ----- | ----- | ---- |
+| aria-label | 容器aria-label   | React.AriaAttributes["aria-label"] |  |   |
+| className | 类名   | string |  |   |
+| description | 步骤的详情描述,可选  | ReactNode | -  |    |
+| icon      | 步骤图标的类型,可选  | ReactNode | -  |    |
+| role      | 容器role  | React.AriaRole | -  |    |
+| status | 指定状态。当不配置该属性时,会使用 Steps 的 `current` 来自动指定状态。可选:`wait`、`process`、`finish`、`error`、`warning` | string | wait  |  |
+| style  | 样式 | CSSProperties |  |   |
+| title   | 标题  | ReactNode | -  |   |
+| onClick     | 点击回调  | function | -   |    |
+| onKeyDown     | 回车事件回调  | function | -   |    |
 
 ## 设计变量
 <DesignToken/>

+ 15 - 4
packages/semi-ui/steps/basicStep.tsx

@@ -21,6 +21,9 @@ export interface BasicStepProps {
     done?: boolean;
     onChange?: () => void;
     onClick?: React.MouseEventHandler<HTMLDivElement>;
+    onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
+    "role"?: React.AriaRole;
+    "aria-label"?: React.AriaAttributes["aria-label"];
 }
 export enum stepSizeMapIconSize {
     small = 'large',
@@ -42,7 +45,7 @@ const BasicStep = (props: BasicStepProps) => {
         stepNumber,
         onClick,
         onChange,
-        ...restProps
+        onKeyDown,
     } = props;
     const renderIcon = () => {
         let inner, progress;
@@ -82,18 +85,26 @@ const BasicStep = (props: BasicStepProps) => {
 
         return inner ? <span className={cls}>{inner}</span> : null;
     };
-    const classString = classnames(prefixCls, className, `${prefixCls}-${status}`, {
+    const classString = classnames(prefixCls, `${prefixCls}-${status}`, {
         [`${prefixCls}-active`]: active,
         [`${prefixCls}-done`]: done
-    });
+    }, className);
     const handleClick = (e: React.MouseEvent) => {
         if (isFunction(onClick)) {
             onClick(e);
         }
         onChange();
     };
+    const handleKeyDown = (e) => {
+        if (e.key === 'Enter') {
+            if (isFunction(onKeyDown)) {
+                onKeyDown(e);
+            }
+            onChange();
+        }
+    };
     return (
-        <div {...restProps} className={classString} style={style} onClick={e => handleClick(e)}>
+        <div role={props["role"]} aria-label={props["aria-label"]} tabIndex={0} aria-current="step" className={classString} style={style} onClick={e => handleClick(e)} onKeyDown={handleKeyDown}>
             <div className={`${prefixCls}-container`}>
                 <div className={`${prefixCls}-left`}>{renderIcon()}</div>
                 <div className={`${prefixCls}-content`}>

+ 3 - 2
packages/semi-ui/steps/basicSteps.tsx

@@ -18,6 +18,7 @@ export interface BasicStepsProps {
     hasLine?: boolean;
     children?: React.ReactNode;
     onChange?: (current: number) => void;
+    "aria-label"?: string;
 }
 
 const Steps = (props: BasicStepsProps) => {
@@ -70,7 +71,7 @@ const Steps = (props: BasicStepsProps) => {
             return cloneElement(child, { ...childProps });
         });
         return content;
-    }, [children, initial, prefixCls, direction, status, current, size]);
+    }, [children, initial, prefixCls, direction, status, current, size, onChange]);
 
     const wrapperCls = cls(className, {
         [`${prefixCls}-basic`]: true,
@@ -80,7 +81,7 @@ const Steps = (props: BasicStepsProps) => {
     });
 
     return (
-        <div className={wrapperCls} style={style}>
+        <div aria-label={props["aria-label"]} className={wrapperCls} style={style}>
             {inner}
         </div>
     );

+ 27 - 12
packages/semi-ui/steps/fillStep.tsx

@@ -18,10 +18,13 @@ export interface FillStepProps {
     stepNumber?: string;
     onChange?: () => void;
     onClick?: React.MouseEventHandler<HTMLDivElement>;
+    onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
+    "role"?: React.AriaRole;
+    "aria-label"?: React.AriaAttributes["aria-label"];
 }
 
 const FillStep = (props: FillStepProps) => {
-    const { prefixCls, className, title, description, status, style, onClick, icon, onChange, stepNumber } = props;
+    const { prefixCls, className, title, description, status, style, onClick, icon, onChange, stepNumber, onKeyDown } = props;
     const renderIcon = () => {
         let inner, progress;
 
@@ -51,10 +54,10 @@ const FillStep = (props: FillStepProps) => {
             }
         }
         const cls = classnames({
-            [`${prefixCls }-left`]: true,
-            [`${prefixCls }-icon`]: 'icon' in props,
-            [`${prefixCls }-plain`]: !('icon' in props),
-            [`${prefixCls }-icon-process`]: progress,
+            [`${prefixCls}-left`]: true,
+            [`${prefixCls}-icon`]: 'icon' in props,
+            [`${prefixCls}-plain`]: !('icon' in props),
+            [`${prefixCls}-icon-process`]: progress,
         });
 
         return inner ? <div className={cls}>{inner}</div> : null;
@@ -65,26 +68,38 @@ const FillStep = (props: FillStepProps) => {
         }
         onChange();
     };
+    const handleKeyDown = (e) => {
+        if (e.key === 'Enter') {
+            if (isFunction(onKeyDown)) {
+                onKeyDown(e);
+            }
+            onChange();
+        }
+    };
     return (
         <div
+            role={props["role"]}
+            aria-label={props["aria-label"]}
+            aria-current="step"
+            tabIndex={0} 
             className={classnames({
-                [className]: Boolean(className),
                 [prefixCls]: true,
                 [`${prefixCls}-${status}`]: Boolean(status),
-                [`${prefixCls }-clickable`]: onClick,
-            })}
+                [`${prefixCls}-clickable`]: onClick,
+            }, className)}
             style={style}
             onClick={e => {
                 handleClick(e);
             }}
+            onKeyDown={handleKeyDown}
         >
             {renderIcon()}
-            <div className={`${prefixCls }-content`}>
-                <div className={`${prefixCls }-title`} title={typeof title === 'string' ? title : null}>
-                    <span className={`${prefixCls }-title-text`}>{title}</span>
+            <div className={`${prefixCls}-content`}>
+                <div className={`${prefixCls}-title`} title={typeof title === 'string' ? title : null}>
+                    <span className={`${prefixCls}-title-text`}>{title}</span>
                 </div>
                 <div
-                    className={`${prefixCls }-description`}
+                    className={`${prefixCls}-description`}
                     title={typeof description === 'string' ? description : null}
                 >
                     {description}

+ 2 - 0
packages/semi-ui/steps/fillSteps.tsx

@@ -16,6 +16,7 @@ export interface FillStepsProps {
     style?: React.CSSProperties;
     children?: React.ReactNode;
     onChange?: (current: number) => void;
+    "aria-label"?: string;
 }
 
 const Steps = (props: FillStepsProps) => {
@@ -66,6 +67,7 @@ const Steps = (props: FillStepsProps) => {
         <div
             className={wrapperCls}
             style={style}
+            aria-label={props["aria-label"]}
         >
             <Row type="flex" justify="start">
                 {inner}

+ 15 - 4
packages/semi-ui/steps/navStep.tsx

@@ -15,21 +15,32 @@ export interface NavStepProps {
     prefixCls?: string;
     onChange?: () => void;
     onClick?: React.MouseEventHandler<HTMLDivElement>;
+    onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
+    "role"?: React.AriaRole;
+    "aria-label"?: React.AriaAttributes["aria-label"];
 }
 
 const NavStep = (props: NavStepProps) => {
-    const { prefixCls, className, title, style, active, index, total, onClick, onChange, ...restProps } = props;
-    const classString = classnames(prefixCls, className, {
+    const { prefixCls, className, title, style, active, index, total, onClick, onKeyDown, onChange } = props;
+    const classString = classnames(prefixCls, {
         [`${prefixCls}-active`]: active
-    });
+    }, className);
     const handleClick = (e: React.MouseEvent) => {
         if (isFunction(onClick)) {
             onClick(e);
         }
         onChange();
     };
+    const handleKeyDown = (e) => {
+        if (e.key === 'Enter') {
+            if (isFunction(onKeyDown)) {
+                onKeyDown(e);
+            }
+            onChange();
+        }
+    };
     return (
-        <div {...restProps} className={classString} style={style} onClick={e => handleClick(e)}>
+        <div role={props["role"]} aria-label={props["aria-label"]} aria-current="step" tabIndex={0} className={classString} style={style} onClick={e => handleClick(e)} onKeyDown={handleKeyDown}>
             <div className={`${prefixCls}-container`}>
                 <div className={`${prefixCls}-content`}>
                     <div className={`${prefixCls}-title`}>

+ 3 - 2
packages/semi-ui/steps/navSteps.tsx

@@ -13,6 +13,7 @@ export interface NavStepsProps {
     size?: Size;
     children?: React.ReactNode;
     onChange?: (current: number) => void;
+    "aria-label"?: string;
 }
 
 const Steps = (props: NavStepsProps) => {
@@ -38,7 +39,7 @@ const Steps = (props: NavStepsProps) => {
             return cloneElement(child, { ...childProps });
         });
         return content;
-    }, [children, prefixCls, current, size]);
+    }, [children, prefixCls, current, size, initial, onChange]);
 
     const wrapperCls = cls(className, {
         [`${prefixCls}-nav`]: true,
@@ -46,7 +47,7 @@ const Steps = (props: NavStepsProps) => {
     });
 
     return (
-        <div className={wrapperCls} style={style}>
+        <div aria-label={props["aria-label"]} className={wrapperCls} style={style}>
             {inner}
         </div>
     );