Просмотр исходного кода

Merge branch 'feat/a11y-aria' of https://github.com/DouyinFE/semi-design into feat/a11y-aria

走鹃 3 лет назад
Родитель
Сommit
81ee3ea8cd

+ 9 - 0
content/input/upload/index-en-US.md

@@ -1081,6 +1081,15 @@ import { IconUpload } from '@douyinfe/semi-icons';
 };
 ```
 
+## Accessibility
+
+The Upload component is an interactive control that can trigger file selection when clicking or dragging. After the file is selected, the status will be displayed in the file list.
+
+### ARIA
+
+- Add `role="button"` to clickable elements
+- Add `role="list"` to the file list and describe it with `aria-label`
+
 ## API Reference
 
 ---

+ 9 - 0
content/input/upload/index.md

@@ -1068,6 +1068,15 @@ import { IconUpload } from '@douyinfe/semi-icons';
 };
 ```
 
+## Accessibility
+
+Upload组件是一个可交互的控件,在点击或拖拽时触发文件选择,文件选中后会在文件列表内展示状态。
+
+### ARIA
+
+- 为可点击元素添加 `role="button"`
+- 文件列表添加 `role="list"`,并用 `aria-label` 描述
+
 ## API 参考
 
 ---

+ 7 - 0
content/navigation/pagination/index-en-US.md

@@ -201,6 +201,13 @@ import { Pagination } from '@douyinfe/semi-ui';
 | onPageChange       | A callback function for page number changes                                                                 | function(currentPage: number)                   |                     |              |
 | onPageSize Change  | Callback function when capacity changes per page                                                            | function(pageSize: number)                      |                     |              |
 
+## Accessibility
+
+### ARIA
+
+- `aria-label`: Labels the element such as previous, next, pages in the pagination.
+- `aria-current`: Indicates the current page.
+
 ## Design Tokens
 <DesignToken/>
 

+ 7 - 0
content/navigation/pagination/index.md

@@ -194,6 +194,13 @@ import { Pagination } from '@douyinfe/semi-ui';
 | onPageChange       | 页码变化的回调函数                                                                | function(currentPage: number)                   |                     |
 | onPageSizeChange   | 每页容量变化时的回调函数                                                          | function(pageSize: number)                      |                     |
 
+## Accessibility
+
+### ARIA
+
+- `aria-label`: 描述组件内页码、前一页、后一页等元素的标签
+- `aria-current`: 指向当前页的页码元素
+
 ## 设计变量
 <DesignToken/>
 

+ 15 - 0
content/navigation/tabs/index-en-US.md

@@ -560,6 +560,21 @@ style | style object | CSSProperties | None |
 tab | Tab page bar display text | ReactNode | None |
 closable | whether user can close the tab **>=2.1.0** | boolean | false |
 
+
+## Accessibility
+
+### ARIA
+- About role
+  - TabBar has a role of `tablist`
+  - Tab in TabBar has a role of `tab`
+  - TabPane has a role of `tabpanel`
+
+- aria-orientation: Indicates TabBar's orientation, can be `vertical` or `horizontal`. When tabPosition is `left`, aria-orientation will be `vertical`, when tabPosition is `top`, aria-orientation will be `horizontal`.
+- aria-disabled: When TabPane is disabled, the related Tab's aria-disabled will be set to true.
+- aria-selected: Indicates whether the Tab is selected.
+- aria-controls: Indicates the TabPane controlled by the Tab
+- aria-labelledby: Indicates the element labels the TabPane
+
 ## Design Token
 
 <DesignToken/>

+ 14 - 0
content/navigation/tabs/index.md

@@ -585,6 +585,20 @@ style     | 样式对象         | CSSProperties             | 无     |
 tab       | 标签页栏显示文字 | ReactNode | 无     |
 closable  | 允许关闭tab **>=2.1.0**| boolean | false |
 
+## Accessibility
+
+### ARIA
+- 关于 role
+  - TabBar 对应的 role 为 `tablist`
+  - TabBar 中的 Tab 对应的 role 为 `tab`
+  - TabPane 对应的 role 为 `tabpanel`
+
+- aria-orientation: 表明 TabBar 的方向,有 `vertical` 和 `horizontal` 两种。当传入 tabPosition 为 left 时,aria-orientation 会被设置为 `vertical`,tabPosition 为 top 时,设置为 `horizontal`
+- aria-disabled: 当 TabPane 设置为 disabled 时,对应 Tab 的 aria-disabled 会被设置为 true
+- aria-selected: 表明 Tab 是否被选中
+- aria-controls: 指向 Tab 标签所控制的 TabPane
+- aria-labelledby: 指向设置 TabPane 标签的元素
+
 ## 设计变量
 
 <DesignToken/>

+ 24 - 0
content/show/collapsible/index-en-US.md

@@ -187,6 +187,30 @@ import { Collapsible, Button } from '@douyinfe/semi-ui';
 | motion | Toggle whether to turn on animation | Motion | `true` | - |
 | reCalcKey | When reCalcKey changes, the height of children will be reset. Used for optimize dynamic content rendering. | number \| string | - | 1.5.0 |
 | style | Style object | CSSProperties | - | 0.34.0 |
+| aria-controls | [aria-controls](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-controls) | string |-| 2.3.0|
+
+## Accessibility
+
+### Aria
+
+- Collapsible has `id` props, the value passed in will be set as the id of the wrapper element, which can be used with other components' `aria-controls` to indicate the control relationship, see the usage example below.
+
+```jsx
+import Collapsible from './index';
+
+
+()=>{
+    const collapseId = 'myCollapsible';
+    const [visible,setVisible]=useState(false);
+    return <>
+        <Button onClick={()=>setVisible(!visible)} aria-controls={`${collapseId}`}>{visible?'hide':'show'}</Button>
+        <Collapsible isOpen={visible} id={collapseId}>
+            <div>hide content</div>
+        </Collapsible>
+    </>
+}
+
+```
 
 ## FAQ
 

+ 23 - 0
content/show/collapsible/index.md

@@ -218,6 +218,29 @@ import { Collapsible, Button } from '@douyinfe/semi-ui';
 | motion | 是否开启动画 | Motion | `true` | - |
 | reCalcKey | 当 reCalcKey 改变时,将重新计算子节点的高度,用于优化动态渲染时的计算 | number \| string | - | 1.5.0 |
 | style | 样式 | CSSProperties | - | 0.34.0 |
+| id | id | html id string type | - | 2.3.0 |
+## Accessibility
+
+### Aria
+
+-   Collapsible 具有 `id` props,传入的值会被设置为 wrapper 元素的id, 可以配合其他组件的 `aria-controls` 指明控制关系, 见下方使用示例。
+
+```jsx
+import Collapsible from './index';
+
+
+()=>{
+    const collapseId = 'myCollapsible';
+    const [visible,setVisible]=useState(false);
+    return <>
+        <Button onClick={()=>setVisible(!visible)} aria-controls={`${collapseId}`}>{visible?'hide':'show'}</Button>    
+        <Collapsible isOpen={visible} id={collapseId}>
+            <div>hide content</div>
+        </Collapsible>
+    </>
+}
+
+```
 
 ## FAQ
 

+ 13 - 0
content/show/timeline/index-en-US.md

@@ -203,5 +203,18 @@ import { IconAlertTriangle } from '@douyinfe/semi-icons';
 | onClick    | Click event                                              | (e: MouseEvent) => void                             | -         | 2.2.0     |
 
 
+
+## Accessibility
+
+### ARIA
+- The element of dot and line between dots in TimeLine have a `aria-hidden`, indicates that they do not support Accessibility API.
+- Supporting API `aria-label` to specify TimeLine's label.
+```js
+<Timeline aria-label="Accident timeline">
+    <Timeline.Item time="2015-09-01">Accident started</Timeline.Item>
+    <Timeline.Item time="2015-09-01">Process</Timeline.Item>
+</Timeline>
+```
+
 ## Design Tokens
 <DesignToken/>

+ 13 - 1
content/show/timeline/index.md

@@ -244,7 +244,19 @@ import { IconAlertTriangle } from '@douyinfe/semi-icons';
 | type | 当前圆圈的模式 | `default`\|`ongoing`\|`success`\|`warning`\|`error` | `default` | - |
 | onClick | 鼠标点击事件的回调 | (e: MouseEvent) => void | - | 2.2.0 |
 
-
+## Accessibility
+
+### ARIA
+- 组件中时间点的连线以及时间点本身被设置了 `aria-hidden`,不会响应 Accessibility API
+- 可以通过传入 `aria-label` 设置 TimeLine 组件的标签
+```js
+<Timeline aria-label="xx事故处理过程时间线">
+    <Timeline.Item time="2015-09-01">创建服务现场</Timeline.Item>
+    <Timeline.Item time="2015-09-02">初步排除网络异常</Timeline.Item>
+    <Timeline.Item time="2015-09-03">技术测试异常</Timeline.Item>
+    <Timeline.Item time="2015-09-05">网络异常正在修复</Timeline.Item>
+</Timeline>
+```
 ## 设计变量
 
 <DesignToken/>

+ 4 - 2
packages/semi-ui/collapsible/index.tsx

@@ -19,6 +19,7 @@ export interface CollapsibleProps {
     style?: React.CSSProperties;
     collapseHeight?: number;
     reCalcKey?: number | string;
+    id?:string,
 }
 
 
@@ -32,7 +33,8 @@ const Collapsible = (props: CollapsibleProps) => {
         collapseHeight,
         style,
         className,
-        reCalcKey
+        reCalcKey,
+        id
     } = props;
 
     const ref = useRef(null);
@@ -90,7 +92,7 @@ const Collapsible = (props: CollapsibleProps) => {
         const wrapperCls = cls(`${cssClasses.PREFIX}-wrapper`, className);
         return (
             <div style={wrapperstyle} className={wrapperCls} ref={ref}>
-                <div ref={setHeight} style={{overflow:'hidden'}}>{children}</div>
+                <div ref={setHeight} style={{ overflow: 'hidden' }} id={id}>{children}</div>
             </div>
         );
     };

+ 9 - 5
packages/semi-ui/pagination/index.tsx

@@ -1,4 +1,5 @@
 /* eslint-disable max-len */
+/* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role */
 import React from 'react';
 import classNames from 'classnames';
 import PropTypes from 'prop-types';
@@ -213,7 +214,7 @@ export default class Pagination extends BaseComponent<PaginationProps, Paginatio
             [`${prefixCls}-item-disabled`]: prevDisabled,
         });
         return (
-            <li onClick={e => !prevDisabled && this.foundation.goPrev(e)} className={preClassName} tab-index={0}>
+            <li role="button" aria-disabled={prevDisabled ? true : false} aria-label="Previous" onClick={e => !prevDisabled && this.foundation.goPrev(e)} className={preClassName}>
                 {prevText || <IconChevronLeft size="large" />}
             </li>
         );
@@ -228,7 +229,7 @@ export default class Pagination extends BaseComponent<PaginationProps, Paginatio
             [`${prefixCls}-next`]: true,
         });
         return (
-            <li onClick={e => !nextDisabled && this.foundation.goNext(e)} className={nextClassName} tab-index={0}>
+            <li role="button" aria-disabled={nextDisabled ? true : false} aria-label="Next" onClick={e => !nextDisabled && this.foundation.goNext(e)} className={nextClassName}>
                 {nextText || <IconChevronRight size="large" />}
             </li>
         );
@@ -257,6 +258,7 @@ export default class Pagination extends BaseComponent<PaginationProps, Paginatio
         return (
             <div className={switchCls}>
                 <Select
+                    aria-label="Page size selector"
                     onChange={newPageSize => this.foundation.changePageSize(newPageSize)}
                     value={pageSize}
                     key={pageSizeText}
@@ -319,8 +321,8 @@ export default class Pagination extends BaseComponent<PaginationProps, Paginatio
                     key={`${page}${i}`}
                     onClick={() => this.foundation.goPage(page, i)}
                     className={pageListClassName}
-                    tab-index={0}
-
+                    aria-label={page === '...' ? 'More' : `Page ${page}`}
+                    aria-current={currentPage === page ? "page" : false}
                 >
                     {page}
                 </li>
@@ -355,10 +357,12 @@ export default class Pagination extends BaseComponent<PaginationProps, Paginatio
             const page = restList[index];
             return (
                 <div
+                    role="listitem"
                     key={`${page}${index}`}
                     className={className}
                     onClick={() => this.foundation.goPage(page, index)}
                     style={style}
+                    aria-label={`${page}`}
                 >
                     {page}
                 </div>
@@ -417,7 +421,7 @@ export default class Pagination extends BaseComponent<PaginationProps, Paginatio
         const { total, pageSize } = this.state;
         const { showTotal, className, style, hideOnSinglePage, showSizeChanger } = this.props;
         const paginationCls = classNames(className, `${prefixCls}`);
-        const showTotalCls = `${prefixCls }-total`;
+        const showTotalCls = `${prefixCls}-total`;
         const totalPageNum = Math.ceil(total / pageSize);
         if (totalPageNum < 2 && hideOnSinglePage && !showSizeChanger) {
             return null;

+ 8 - 5
packages/semi-ui/tabs/TabBar.tsx

@@ -91,7 +91,7 @@ class TabBar extends React.Component<TabBarProps, TabBarState> {
     renderTabItem = (panel: PlainTab): ReactNode => {
         const { size, type, deleteTabItem } = this.props;
         const panelIcon = panel.icon ? this.renderIcon(panel.icon) : null;
-        const closableIcon = (type === 'card' && panel.closable) ? <IconClose className={`${cssClasses.TABS_TAB}-icon-close`} onClick={(e: React.MouseEvent<HTMLSpanElement>) => deleteTabItem(panel.itemKey, e)} /> : null;
+        const closableIcon = (type === 'card' && panel.closable) ? <IconClose aria-label="Close" role="button" className={`${cssClasses.TABS_TAB}-icon-close`} onClick={(e: React.MouseEvent<HTMLSpanElement>) => deleteTabItem(panel.itemKey, e)} /> : null;
         let events = {};
         const key = panel.itemKey;
         if (!panel.disabled) {
@@ -99,8 +99,9 @@ class TabBar extends React.Component<TabBarProps, TabBarState> {
                 onClick: (e: MouseEvent<HTMLDivElement>): void => this.handleItemClick(key, e),
             };
         }
+        const isSelected = this._isActive(key);
         const className = cls(cssClasses.TABS_TAB, {
-            [cssClasses.TABS_TAB_ACTIVE]: this._isActive(key),
+            [cssClasses.TABS_TAB_ACTIVE]: isSelected,
             [cssClasses.TABS_TAB_DISABLED]: panel.disabled,
             [`${cssClasses.TABS_TAB}-small`]: size === 'small',
             [`${cssClasses.TABS_TAB}-medium`]: size === 'medium',
@@ -108,8 +109,10 @@ class TabBar extends React.Component<TabBarProps, TabBarState> {
         return (
             <div
                 role="tab"
+                id={`semiTab${key}`}
+                aria-controls={`semiTabPanel${key}`}
                 aria-disabled={panel.disabled ? 'true' : 'false'}
-                aria-selected={this._isActive(key) ? 'true' : 'false'}
+                aria-selected={isSelected ? 'true' : 'false'}
                 {...events}
                 className={className}
                 key={this._getItemKey(key)}
@@ -182,7 +185,7 @@ class TabBar extends React.Component<TabBarProps, TabBarState> {
                 style={dropdownStyle}
                 trigger={'hover'}
             >
-                <div className={arrowCls} onClick={(e): void => this.handleArrowClick(items, pos)}>
+                <div role="presentation" className={arrowCls} onClick={(e): void => this.handleArrowClick(items, pos)}>
                     <Button
                         disabled={disabled}
                         icon={icon}
@@ -233,7 +236,7 @@ class TabBar extends React.Component<TabBarProps, TabBarState> {
         const contents = collapsible ? this.renderCollapsedTab() : this.renderTabComponents(list);
 
         return (
-            <div role="tab-list" className={classNames} style={style} {...getDataAttr(restProps)} data-uuid={this.uuid}>
+            <div role="tablist" aria-orientation={tabPosition === "left" ? "vertical" : "horizontal"} className={classNames} style={style} {...getDataAttr(restProps)} data-uuid={this.uuid}>
                 {contents}
                 {extra}
             </div>

+ 3 - 1
packages/semi-ui/tabs/TabPane.tsx

@@ -86,7 +86,9 @@ class TabPane extends PureComponent<TabPaneProps> {
         return (
             <div
                 ref={this.ref}
-                role="tab-panel"
+                role="tabpanel"
+                id={`semiTabPanel${itemKey}`}
+                aria-labelledby={`semiTab${itemKey}`}
                 className={classNames}
                 style={style}
                 aria-hidden={active ? 'false' : 'true'}

+ 0 - 1
packages/semi-ui/tabs/index.tsx

@@ -285,7 +285,6 @@ class Tabs extends BaseComponent<TabsProps, TabsState> {
                 >
                     <div
                         ref={this.setContentRef}
-                        role="tab-content"
                         className={tabContentCls}
                         style={{ ...contentStyle }}
                     >

+ 1 - 1
packages/semi-ui/timeline/_story/timeline.stories.js

@@ -10,7 +10,7 @@ export default {
 
 export const DefaultTimeline = () => (
   <div>
-    <Timeline>
+    <Timeline aria-label="xx事故处理过程时间线">
       <Timeline.Item time="2015-09-01">创建服务现场</Timeline.Item>
       <Timeline.Item time="2015-09-01">初步排除网络异常</Timeline.Item>
       <Timeline.Item time="2015-09-01">技术测试异常</Timeline.Item>

+ 2 - 2
packages/semi-ui/timeline/index.tsx

@@ -12,7 +12,7 @@ export interface Data extends TimelineItemProps {
     content: React.ReactNode;
 }
 
-export interface TimelineProps {
+export interface TimelineProps extends Pick<React.AriaAttributes, 'aria-label'> {
     mode?: 'left' | 'right' | 'center' | 'alternate';
     className?: string;
     style?: React.CSSProperties;
@@ -86,7 +86,7 @@ class Timeline extends PureComponent<TimelineProps> {
         const items = childrenList || this.addClassName(children);
 
         return (
-            <ul style={style} className={classString}>
+            <ul aria-label={this.props['aria-label']} style={style} className={classString}>
                 {items}
             </ul>
         );

+ 2 - 1
packages/semi-ui/timeline/item.tsx

@@ -63,9 +63,10 @@ export default class Item extends PureComponent<TimelineItemProps> {
         const dotStyle = color ? { style: { backgroundColor: color } } : null;
         return (
             <li className={itemCls} style={style} onClick={onClick}>
-                <div className={`${prefixCls}-tail`} />
+                <div className={`${prefixCls}-tail`} aria-hidden />
                 <div
                     className={dotCls}
+                    aria-hidden
                     {...dotStyle}
                 >
                     {dot}

+ 13 - 15
packages/semi-ui/upload/fileCard.tsx

@@ -17,7 +17,7 @@ import { RenderFileItemProps } from './interface';
 const prefixCls = cssClasses.PREFIX;
 
 const ErrorSvg: FC<SVGProps<SVGSVGElement>> = (props = {}) => (
-    <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
+    <svg focusable={false} aria-hidden width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
         <circle cx="7.99992" cy="7.99992" r="6.66667" fill="white" />
         <path
             fillRule="evenodd"
@@ -28,7 +28,7 @@ const ErrorSvg: FC<SVGProps<SVGSVGElement>> = (props = {}) => (
 );
 
 const ReplaceSvg: FC<SVGProps<SVGSVGElement>> = (props = {}) => (
-    <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
+    <svg focusable={false} aria-hidden width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
         <circle cx="14" cy="14" r="14" fill="#16161A" fillOpacity="0.6" />
         <path d="M9 10.25V18.25L10.25 13.25H17.875V11.75C17.875 11.4739 17.6511 11.25 17.375 11.25H14L12.75 9.75H9.5C9.22386 9.75 9 9.97386 9 10.25Z" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
         <path d="M18 18.25L19 13.25H10.2031L9 18.25H18Z" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
@@ -36,7 +36,7 @@ const ReplaceSvg: FC<SVGProps<SVGSVGElement>> = (props = {}) => (
 );
 
 const DirectorySvg: FC<SVGProps<SVGSVGElement>> = (props = {}) => (
-    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
+    <svg focusable={false} aria-hidden width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
         <path d="M6 17V7.58824C6 7.26336 6.26863 7 6.6 7H10.5L12 8.76471H16.05C16.3814 8.76471 16.65 9.02806 16.65 9.35294V11.1176H7.5L6 17ZM6 17L7.44375 11.1176H18L16.8 17L6 17Z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
     </svg>
 
@@ -136,15 +136,13 @@ class FileCard extends PureComponent<FileCardProps> {
         });
         const closeCls = `${prefixCls}-picture-file-card-close`;
         const retry = (
-            <div
-                className={`${prefixCls}-picture-file-card-retry`} onClick={e => this.onRetry(e)}>
+            <div role="button" tabIndex={0} className={`${prefixCls}-picture-file-card-retry`} onClick={e => this.onRetry(e)}>
                 <IconRefresh className={`${prefixCls}-picture-file-card-icon-retry`} />
             </div>
         );
         const replace = (
             <Tooltip trigger="hover" position="top" content={locale.replace} showArrow={false} spacing={4}>
-                <div
-                    className={`${prefixCls}-picture-file-card-replace`} onClick={(e): void => this.onReplace(e)}>
+                <div role="button" tabIndex={0} className={`${prefixCls}-picture-file-card-replace`} onClick={(e): void => this.onReplace(e)}>
                     <ReplaceSvg className={`${prefixCls}-picture-file-card-icon-replace`} />
                 </div>
             </Tooltip>
@@ -155,18 +153,18 @@ class FileCard extends PureComponent<FileCardProps> {
             <div className={`${prefixCls }-picture-file-card-pic-info`}>{index + 1}</div>
         );
 
-        const thumbnail = typeof renderThumbnail === 'function' ? renderThumbnail(this.props) : <img src={url} alt={`picture of ${name}`} />;
+        const thumbnail = typeof renderThumbnail === 'function' ? renderThumbnail(this.props) : <img src={url} alt={name} />;
 
         return (
-            <div className={filePicCardCls} style={style} onClick={onPreviewClick}>
+            <div role="listitem" className={filePicCardCls} style={style} onClick={onPreviewClick}>
                 {thumbnail}
-                {showProgress ? <Progress percent={percent} type="circle" size="small" orbitStroke={'#FFF'} /> : null}
+                {showProgress ? <Progress percent={percent} type="circle" size="small" orbitStroke={'#FFF'} aria-label="uploading file progress" /> : null}
                 {showRetry ? retry : null}
                 {showReplace && replace}
                 {showPicInfo && picInfo}
                 {!disabled && (
                     <div className={closeCls}>
-                        <IconClose size="extra-small" onClick={e => this.onRemove(e)} />
+                        <IconClose tabIndex={0} role="button" size="extra-small" onClick={e => this.onRemove(e)} />
                     </div>
                 )}
                 {this.renderPicValidateMsg()}
@@ -193,12 +191,12 @@ class FileCard extends PureComponent<FileCardProps> {
         const showRetry = status === strings.FILE_STATUS_UPLOAD_FAIL && propsShowRetry;
         const showReplace = status === strings.FILE_STATUS_SUCCESS && propsShowReplace;
         const fileSize = this.transSize(size);
-        let previewContent: ReactNode = preview ? (<img src={url} />) : (<IconFile size="large" />);
+        let previewContent: ReactNode = preview ? (<img src={url} alt={name} />) : (<IconFile size="large" />);
         if (previewFile) {
             previewContent = previewFile(this.props);
         }
         return (
-            <div className={fileCardCls} style={style} onClick={onPreviewClick}>
+            <div role="listitem" className={fileCardCls} style={style} onClick={onPreviewClick}>
                 <div className={previewCls}>
                     {previewContent}
                 </div>
@@ -225,12 +223,12 @@ class FileCard extends PureComponent<FileCardProps> {
                         </span>
 
                     </div>
-                    {showProgress ? (<Progress percent={percent} style={{ width: '100%' }} />) : null}
+                    {showProgress ? (<Progress percent={percent} style={{ width: '100%' }} aria-label="uploading file progress" />) : null}
                     <div className={`${infoCls}-main-control`}>
                         <span className={`${infoCls}-validate-message`}>
                             {this.renderValidateMessage()}
                         </span>
-                        {showRetry ? <span className={`${infoCls}-retry`} onClick={e => this.onRetry(e)}>{locale.retry}</span> : null}
+                        {showRetry ? <span role="button" tabIndex={0} className={`${infoCls}-retry`} onClick={e => this.onRetry(e)}>{locale.retry}</span> : null}
                     </div>
                 </div>
                 <IconButton

+ 10 - 6
packages/semi-ui/upload/index.tsx

@@ -400,6 +400,7 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
         });
         const mainCls = `${prefixCls}-file-list-main`;
         const addContentProps = {
+            role: 'button',
             className: uploadAddCls,
             onClick: this.onClick,
         };
@@ -432,7 +433,7 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
             <LocaleConsumer componentName="Upload">
                 {(locale: Locale['Upload']) => (
                     <div {...containerProps}>
-                        <div className={mainCls}>
+                        <div className={mainCls} role="list" aria-label="picture list">
                             {fileList.map((file, index) => this.renderFile(file, index, locale))}
                             {showAddTriggerInList ? addContent : null}
                         </div>
@@ -467,14 +468,14 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
                             <div className={titleCls}>
                                 <span className={`${titleCls}-choosen`}>{locale.selectedFiles}</span>
                                 {showClear ? (
-                                    <span onClick={this.clear} className={`${titleCls}-clear`}>
+                                    <span role="button" tabIndex={0} onClick={this.clear} className={`${titleCls}-clear`}>
                                         {locale.clear}
                                     </span>
                                 ) : null}
                             </div>
                         ) : null}
 
-                        <div className={mainCls}>
+                        <div className={mainCls} role="list" aria-label="file list">
                             {fileList.map((file, index) => this.renderFile(file, index, locale))}
                         </div>
                     </div>
@@ -501,7 +502,7 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
     };
 
     renderAddContent = () => {
-        const { draggable, children, listType } = this.props;
+        const { draggable, children, listType, disabled } = this.props;
         const uploadAddCls = cls(`${prefixCls}-add`);
         if (listType === strings.FILE_LIST_PIC) {
             return null;
@@ -510,7 +511,7 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
             return this.renderDragArea();
         }
         return (
-            <div className={uploadAddCls} onClick={this.onClick}>
+            <div role="button" tabIndex={0} aria-disabled={disabled} className={uploadAddCls} onClick={this.onClick}>
                 {children}
             </div>
         );
@@ -518,7 +519,7 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
 
     renderDragArea = (): ReactNode => {
         const { dragAreaStatus } = this.state;
-        const { children, dragIcon, dragMainText, dragSubText } = this.props;
+        const { children, dragIcon, dragMainText, dragSubText, disabled } = this.props;
         const dragAreaBaseCls = `${prefixCls}-drag-area`;
         const dragAreaCls = cls(dragAreaBaseCls, {
             [`${dragAreaBaseCls}-legal`]: dragAreaStatus === strings.DRAG_AREA_LEGAL,
@@ -530,6 +531,9 @@ class Upload extends BaseComponent<UploadProps, UploadState> {
             <LocaleConsumer componentName="Upload">
                 {(locale: Locale['Upload']): ReactNode => (
                     <div
+                        role="button"
+                        tabIndex={0}
+                        aria-disabled={disabled}
                         className={dragAreaCls}
                         onDrop={this.onDrop}
                         onDragOver={this.onDragOver}