Prechádzať zdrojové kódy

feat: a11y - improve form & field component aria, #205 (#479)

pointhalo 3 rokov pred
rodič
commit
c4bbb31e04
31 zmenil súbory, kde vykonal 407 pridanie a 70 odobranie
  1. 19 2
      content/input/form/index.md
  2. 1 1
      content/show/popover/index-en-US.md
  3. 1 1
      content/show/popover/index.md
  4. 2 0
      packages/semi-foundation/form/utils.ts
  5. 28 1
      packages/semi-ui/autoComplete/index.tsx
  6. 33 5
      packages/semi-ui/cascader/index.tsx
  7. 12 0
      packages/semi-ui/checkbox/checkbox.tsx
  8. 15 0
      packages/semi-ui/checkbox/checkboxGroup.tsx
  9. 14 1
      packages/semi-ui/checkbox/checkboxInner.tsx
  10. 27 0
      packages/semi-ui/configProvider/_story/configProvider.stories.tsx
  11. 19 0
      packages/semi-ui/datePicker/datePicker.tsx
  12. 3 2
      packages/semi-ui/form/_story/FormApi/formApiDemo.jsx
  13. 12 3
      packages/semi-ui/form/_story/demo.jsx
  14. 0 7
      packages/semi-ui/form/_story/form.stories.js
  15. 2 0
      packages/semi-ui/form/baseForm.tsx
  16. 13 2
      packages/semi-ui/form/errorMessage.tsx
  17. 37 8
      packages/semi-ui/form/hoc/withField.tsx
  18. 2 0
      packages/semi-ui/form/interface.ts
  19. 4 2
      packages/semi-ui/form/label.tsx
  20. 16 2
      packages/semi-ui/input/index.tsx
  21. 8 0
      packages/semi-ui/inputNumber/index.tsx
  22. 25 2
      packages/semi-ui/radio/radioGroup.tsx
  23. 16 4
      packages/semi-ui/rating/index.tsx
  24. 20 2
      packages/semi-ui/select/index.tsx
  25. 18 8
      packages/semi-ui/switch/index.tsx
  26. 14 1
      packages/semi-ui/timePicker/TimePicker.tsx
  27. 4 4
      packages/semi-ui/tooltip/index.tsx
  28. 1 1
      packages/semi-ui/tree/index.tsx
  29. 29 6
      packages/semi-ui/treeSelect/index.tsx
  30. 3 3
      src/components/Footer/index.jsx
  31. 9 2
      src/sitePages/newHome/components/banner/banner.jsx

+ 19 - 2
content/input/form/index.md

@@ -11,8 +11,7 @@ dir: column
 ## 表单(Form)
 
 -   **按需重绘**,避免了不必要的全量渲染, 性能更高
--   简单易用,**结构简洁**,避免了不必要的层级嵌套
-    **无需 Form.create()、无需 Form.item,无需 getFieldDecorator()**
+-   简单易用,**结构极简**,避免了不必要的层级嵌套
 -   在 Form 外部可方便地获取 formState / fieldState  
     提供在外部对表单内部进行操作的方法:formApi / fieldApi
 -   支持将自定义组件封装成表单控件,你可以通过 Form 提供的扩展机制(withField HOC)快捷接入自己团队的组件
@@ -2080,6 +2079,24 @@ const { ErrorMessage } = Form;
 | maintainCursor    | 是否需要保持光标,用于 Input 类组件                                                                                                                                                                                                           | false      |
 | shouldMemo        | 是否需要 memo(用于表单性能优化,避免 Form rerender 时 Field 也被 rerender),对于有内部状态且内部状态可能会更新并影响 UI 的自定义组件,此项应该置为 false <br/>**v0.27.0 后提供** | true       |
 
+
+## Accessibility
+
+### ARIA
+
+- [aria-labelledby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-labelledby)、for
+  - Field 组件,会自动添加 label DOM。label 的 `for` 属性与 `props.id` 或 `props.name` 或 `props.field` 相同 ;label 的id 属性由 `props.id` 或 `props.name` 或 `props.field` 决定,值格式为 `${props.field}-label`;
+  - 当 Form 或者 Field 的 props.labelPosition 设置为 inset时,此时不存在 label 标签,而是 div 标签。insetLabel 对应的 div 标签会被自动追加 id,值与上述 label 的 id 相同,对应 Field 组件的 `aria-labelledby`
+  - Field 组件会被自动追加 `aria-labelledby`,值与上述 label 的id 相同
+- [aria-required](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-required_attribute)
+  - 当 Field 配置了必填时(即 props.rules中包含 require: true 或 props.label配置了required: true),Field 组件会被自动追加  aria-required = true(Form.Switch、Form.CheckboxGroup 除外)
+- [aria-invalid](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-invalid_attribute) 、[aria-errormessage](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-errormessage) 
+  - 当 Field 校验未通过时,Field 组件会被自动添加 `aria-invalid` = true 属性,Form.CheckboxGroup 除外。
+  - 当 Field 校验未通过时,Field 组件会被自动追加 `aria-errormessage` 属性,值为 errorMessage 所对应DOM元素的 id (格式: `${props.field}-errormessage`),Form.CheckboxGroup 除外。
+- [aria-describedby](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-describedby_attribute)
+  - 当 Field 配置了 `helpText` 或 `extraText` 时,Field 组件会被自动添加 `aria-describedby` 属性,值为 helpText、extraText 所对应DOM元素的 id (格式:`${props.field}-helpText` 、`${props.field}-extraText`)
+
+
 ## 设计变量
 <DesignToken/>
 

+ 1 - 1
content/show/popover/index-en-US.md

@@ -488,7 +488,7 @@ Please refer to [Use with Tooltip/Popconfirm](/en-US/show/tooltip#%E6%90%AD%E9%8
 - Popover's children 
   - Will be automatically added [aria-expanded](https://www.w3.org/TR/wai-aria-1.1/#aria-expanded) attribute, when Popover is visible, the attribute value is `true`, when invisible Is `false`
   - Will be automatically added [aria-haspopup](https://www.w3.org/TR/wai-aria-1.1/#aria-haspopup) attribute, which is `dialog`
-  - Will be automatically added [aria-controls](https://www.w3.org/TR/wai-aria-1.1/#aria-controls) and `aria-describedby` attribute, which is the id of the content wrapper
+  - Will be automatically added [aria-controls](https://www.w3.org/TR/wai-aria-1.1/#aria-controls) attribute, which is the id of the content wrapper
 
 ## Design Tokens
 

+ 1 - 1
content/show/popover/index.md

@@ -488,7 +488,7 @@ function Demo() {
 - Popover 的 children 
   - 会被自动添加 [aria-expanded](https://www.w3.org/TR/wai-aria-1.1/#aria-expanded) 属性,当 Popover 可见时,属性值为 `true`,不可见时为 `false`
   - 会被自动添加 [aria-haspopup](https://www.w3.org/TR/wai-aria-1.1/#aria-haspopup) 属性,为 `dialog`
-  - 会被自动添加 [aria-controls](https://www.w3.org/TR/wai-aria-1.1/#aria-controls) 与 `aria-describedby` 属性,为 content 的 wrapper 的 id
+  - 会被自动添加 [aria-controls](https://www.w3.org/TR/wai-aria-1.1/#aria-controls) 属性,为 content 的 wrapper 的 id
 
 ## 设计变量
 <DesignToken/>

+ 2 - 0
packages/semi-foundation/form/utils.ts

@@ -133,6 +133,7 @@ export function mergeProps(props: any) {
         extraText,
         extraTextPosition,
         pure,
+        id,
         ...rest
     }: any = { ...defaultProps, ...props };
     // Form中的任何类型组件,初始值都统一通过initValue字段来传入,同时将可能会导致组件行为错误的props抽取出来,防止透传到组件中
@@ -183,6 +184,7 @@ export function mergeProps(props: any) {
         extraTextPosition,
         pure,
         rest,
+        id
     };
 }
 

+ 28 - 1
packages/semi-ui/autoComplete/index.tsx

@@ -39,6 +39,12 @@ export interface BaseDataItem extends DataItem {
 export type AutoCompleteItems = BaseDataItem | string | number;
 
 export interface AutoCompleteProps<T extends AutoCompleteItems> {
+    'aria-describedby'?: React.AriaAttributes['aria-describedby'];
+    'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
+    'aria-invalid'?: React.AriaAttributes['aria-invalid'];
+    'aria-label'?: React.AriaAttributes['aria-label'];
+    'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
+    'aria-required'?: React.AriaAttributes['aria-required'];
     autoAdjustOverflow?: boolean;
     autoFocus?: boolean;
     className?: string;
@@ -54,6 +60,8 @@ export interface AutoCompleteProps<T extends AutoCompleteItems> {
     emptyContent?: React.ReactNode | null;
     getPopupContainer?: () => HTMLElement;
     insetLabel?: React.ReactNode;
+    insetLabelId?: string;
+    id?: string;
     loading?: boolean;
     motion?: Motion;
     maxHeight?: string | number;
@@ -101,6 +109,12 @@ interface AutoCompleteState {
 
 class AutoComplete<T extends AutoCompleteItems> extends BaseComponent<AutoCompleteProps<T>, AutoCompleteState> {
     static propTypes = {
+        'aria-label': PropTypes.string,
+        'aria-labelledby': PropTypes.string,
+        'aria-invalid': PropTypes.bool,
+        'aria-errormessage': PropTypes.string,
+        'aria-describedby': PropTypes.string,
+        'aria-required': PropTypes.bool,
         autoFocus: PropTypes.bool,
         autoAdjustOverflow: PropTypes.bool,
         className: PropTypes.string,
@@ -114,6 +128,9 @@ class AutoComplete<T extends AutoCompleteItems> extends BaseComponent<AutoComple
         dropdownClassName: PropTypes.string,
         dropdownStyle: PropTypes.object,
         emptyContent: PropTypes.node,
+        id: PropTypes.string,
+        insetLabel: PropTypes.node,
+        insetLabelId: PropTypes.string,
         onSearch: PropTypes.func,
         onSelect: PropTypes.func,
         onClear: PropTypes.func,
@@ -313,6 +330,7 @@ class AutoComplete<T extends AutoCompleteItems> extends BaseComponent<AutoComple
             size,
             prefix,
             insetLabel,
+            insetLabelId,
             suffix,
             placeholder,
             style,
@@ -322,7 +340,8 @@ class AutoComplete<T extends AutoCompleteItems> extends BaseComponent<AutoComple
             triggerRender,
             validateStatus,
             autoFocus,
-            value
+            value,
+            id,
         } = this.props;
         const { inputValue, keyboardEventSet, selection } = this.state;
 
@@ -341,6 +360,7 @@ class AutoComplete<T extends AutoCompleteItems> extends BaseComponent<AutoComple
                 ),
             onClick: this.handleInputClick,
             ref: this.triggerRef,
+            id,
             ...keyboardEventSet,
         };
 
@@ -350,9 +370,16 @@ class AutoComplete<T extends AutoCompleteItems> extends BaseComponent<AutoComple
             autofocus: autoFocus,
             onChange: this.onSearch,
             onClear: this.onInputClear,
+            'aria-label': this.props['aria-label'],
+            'aria-labelledby': this.props['aria-labelledby'],
+            'aria-invalid': this.props['aria-invalid'],
+            'aria-errormessage': this.props['aria-errormessage'],
+            'aria-describedby': this.props['aria-describedby'],
+            'aria-required': this.props['aria-required'],
             // TODO: remove in next major version
             suffix,
             prefix: prefix || insetLabel,
+            insetLabelId,
             showClear,
             validateStatus,
             size,

+ 33 - 5
packages/semi-ui/cascader/index.tsx

@@ -49,6 +49,11 @@ export type SimpleValueType = string | number | CascaderData;
 export type Value = SimpleValueType | Array<SimpleValueType> | Array<Array<SimpleValueType>>;
 
 export interface CascaderProps extends BasicCascaderProps {
+    'aria-describedby'?: React.AriaAttributes['aria-describedby'];
+    'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
+    'aria-invalid'?: React.AriaAttributes['aria-invalid'];
+    'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
+    'aria-required'?: React.AriaAttributes['aria-required'];
     arrowIcon?: ReactNode;
     defaultValue?: Value;
     dropdownStyle?: CSSProperties;
@@ -60,7 +65,9 @@ export interface CascaderProps extends BasicCascaderProps {
     value?: Value;
     prefix?: ReactNode;
     suffix?: ReactNode;
+    id?: string;
     insetLabel?: ReactNode;
+    insetLabelId?: string;
     style?: CSSProperties;
     bottomSlot?: ReactNode;
     topSlot?: ReactNode;
@@ -88,6 +95,11 @@ const resetkey = 0;
 class Cascader extends BaseComponent<CascaderProps, CascaderState> {
     static contextType = ConfigContext;
     static propTypes = {
+        'aria-labelledby': PropTypes.string,
+        'aria-invalid': PropTypes.bool,
+        'aria-errormessage': PropTypes.string,
+        'aria-describedby': PropTypes.string,
+        'aria-required': PropTypes.bool,
         arrowIcon: PropTypes.node,
         changeOnSelect: PropTypes.bool,
         defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
@@ -114,6 +126,8 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
         suffix: PropTypes.node,
         prefix: PropTypes.node,
         insetLabel: PropTypes.node,
+        insetLabelId: PropTypes.string,
+        id: PropTypes.string,
         displayProp: PropTypes.string,
         displayRender: PropTypes.func,
         onChange: PropTypes.func,
@@ -610,7 +624,7 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
         const popoverCls = cls(dropdownClassName, `${prefixcls}-popover`);
         const renderData = this.foundation.getRenderData();
         const content = (
-            <div className={popoverCls} role="list-box" style={dropdownStyle}>
+            <div className={popoverCls} role="listbox" style={dropdownStyle}>
                 {topSlot}
                 <Item
                     activeKeys={activeKeys}
@@ -726,7 +740,7 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
     };
 
     renderPrefix = () => {
-        const { prefix, insetLabel } = this.props;
+        const { prefix, insetLabel, insetLabelId } = this.props;
         const labelNode: any = prefix || insetLabel;
 
         const prefixWrapperCls = cls({
@@ -737,7 +751,7 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
             [`${prefixcls}-prefix-icon`]: isSemiIcon(labelNode),
         });
 
-        return <div className={prefixWrapperCls}>{labelNode}</div>;
+        return <div className={prefixWrapperCls} id={insetLabelId}>{labelNode}</div>;
     };
 
     renderCustomTrigger = () => {
@@ -794,7 +808,7 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
         const allowClear = this.showClearBtn();
         if (allowClear) {
             return (
-                <div className={clearCls} onClick={this.handleClear}>
+                <div className={clearCls} onClick={this.handleClear} role='button' tabIndex={0}>
                     <IconClear />
                 </div>
             );
@@ -825,6 +839,7 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
             insetLabel,
             triggerRender,
             showClear,
+            id,
         } = this.props;
         const { isOpen, isFocus, isInput, checkedKeys } = this.state;
         const filterable = Boolean(filterTreeNode);
@@ -863,14 +878,27 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
                 <Fragment key={'suffix'}>{suffix ? this.renderSuffix() : null}</Fragment>,
                 <Fragment key={'arrow'}>{this.renderArrow()}</Fragment>,
             ];
-
+        /**
+         * Reasons for disabling the a11y eslint rule:
+         * The following attributes(aria-controls,aria-expanded) will be automatically added by Tooltip, no need to declare here
+         */
         return (
             <div
                 className={classNames}
                 style={style}
                 ref={this.triggerRef}
                 onClick={e => this.foundation.handleClick(e)}
+                aria-invalid={this.props['aria-invalid']}
+                aria-errormessage={this.props['aria-errormessage']}
+                aria-label={this.props['aria-label']}
+                aria-labelledby={this.props['aria-labelledby']}
+                aria-describedby={this.props["aria-describedby"]}
+                aria-required={this.props['aria-required']}
+                id={id}
                 {...mouseEvent}
+                // eslint-disable-next-line jsx-a11y/role-has-required-aria-props
+                role='combobox'
+                tabIndex={0}
             >
                 {inner}
             </div>

+ 12 - 0
packages/semi-ui/checkbox/checkbox.tsx

@@ -14,6 +14,11 @@ export type CheckboxEvent = BasicCheckboxEvent;
 export type TargetObject = BasicTargetObject;
 
 export interface CheckboxProps extends BaseCheckboxProps {
+    'aria-describedby'?: React.AriaAttributes['aria-describedby'];
+    'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
+    'aria-invalid'?: React.AriaAttributes['aria-invalid'];
+    'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
+    'aria-required'?: React.AriaAttributes['aria-required'];
     onChange?: (e: CheckboxEvent) => any;
     // TODO, docs
     style?: React.CSSProperties;
@@ -31,6 +36,11 @@ class Checkbox extends BaseComponent<CheckboxProps, CheckboxState> {
     static contextType = Context;
 
     static propTypes = {
+        'aria-describedby': PropTypes.string,
+        'aria-errormessage': PropTypes.string,
+        'aria-invalid': PropTypes.bool,
+        'aria-labelledby': PropTypes.string,
+        'aria-required': PropTypes.bool,
         // Specifies whether it is currently selected
         checked: PropTypes.bool,
         // Initial check
@@ -200,6 +210,8 @@ class Checkbox extends BaseComponent<CheckboxProps, CheckboxState> {
                 onMouseEnter={onMouseEnter}
                 onMouseLeave={onMouseLeave}
                 onClick={this.handleChange}
+                onKeyPress={this.handleEnterPress}
+                aria-labelledby={this.props['aria-labelledby']}
             >
                 <CheckboxInner
                     {...this.props}

+ 15 - 0
packages/semi-ui/checkbox/checkboxGroup.tsx

@@ -13,6 +13,11 @@ export type CheckboxDirection = 'horizontal' | 'vertical';
 export type CheckboxType = 'default' | 'card' | 'pureCard';
 
 export type CheckboxGroupProps = {
+    'aria-describedby'?: React.AriaAttributes['aria-describedby'];
+    'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
+    'aria-invalid'?: React.AriaAttributes['aria-invalid'];
+    'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
+    'aria-required'?: React.AriaAttributes['aria-required'];
     defaultValue?: any[];
     disabled?: boolean;
     name?: string;
@@ -35,6 +40,11 @@ export type CheckboxGroupState = {
 class CheckboxGroup extends BaseComponent<CheckboxGroupProps, CheckboxGroupState> {
 
     static propTypes = {
+        'aria-describedby': PropTypes.string,
+        'aria-errormessage': PropTypes.string,
+        'aria-invalid': PropTypes.bool,
+        'aria-labelledby': PropTypes.string,
+        'aria-required': PropTypes.bool,
         defaultValue: PropTypes.array,
         disabled: PropTypes.bool,
         name: PropTypes.string,
@@ -159,6 +169,11 @@ class CheckboxGroup extends BaseComponent<CheckboxGroupProps, CheckboxGroupState
                 aria-label={this.props['aria-label']}
                 className={prefixClsDisplay} 
                 style={style}
+                aria-labelledby={this.props['aria-labelledby']}
+                aria-describedby={this.props['aria-describedby']}
+                // aria-errormessage={this.props['aria-errormessage']}
+                // aria-invalid={this.props['aria-invalid']}
+                // aria-required={this.props['aria-required']}
             >
                 <Context.Provider
                     value={{

+ 14 - 1
packages/semi-ui/checkbox/checkboxInner.tsx

@@ -8,6 +8,11 @@ import { Context } from './context';
 import { IconCheckboxTick, IconCheckboxIndeterminate } from '@douyinfe/semi-icons';
 
 export interface CheckboxInnerProps {
+    'aria-describedby'?: React.AriaAttributes['aria-describedby'];
+    'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
+    'aria-invalid'?: React.AriaAttributes['aria-invalid'];
+    'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
+    'aria-required'?: React.AriaAttributes['aria-required'];
     indeterminate?: boolean;
     checked?: boolean;
     disabled?: boolean;
@@ -24,6 +29,11 @@ class CheckboxInner extends PureComponent<CheckboxInnerProps> {
     static contextType = Context;
 
     static propTypes = {
+        'aria-describedby': PropTypes.string,
+        'aria-errormessage': PropTypes.string,
+        'aria-invalid': PropTypes.bool,
+        'aria-labelledby': PropTypes.string,
+        'aria-required': PropTypes.bool,
         checked: PropTypes.bool,
         disabled: PropTypes.bool,
         onChange: PropTypes.func,
@@ -79,7 +89,10 @@ class CheckboxInner extends PureComponent<CheckboxInnerProps> {
                     aria-disabled={disabled}
                     aria-checked={checked}
                     aria-labelledby={addonId}
-                    aria-describedby={extraId}
+                    aria-describedby={extraId || this.props['aria-describedby']}
+                    aria-invalid={this.props['aria-invalid']}
+                    aria-errormessage={this.props['aria-errormessage']}
+                    aria-required={this.props['aria-required']}
                     ref={ref => {
                         this.inputEntity = ref;
                     }}

+ 27 - 0
packages/semi-ui/configProvider/_story/configProvider.stories.tsx

@@ -0,0 +1,27 @@
+import React, { useState } from 'react';
+import { ButtonGroup, Button, ConfigProvider } from '@douyinfe/semi-ui';
+
+export default function RTLWrapper({ children, onDirectionChange }: { children: React.ReactNode; onDirectionChange?: (direction: 'ltr' | 'rtl') => void }) {
+    const [direction, setDirection] = useState();
+    const handleDirectionChange = dir => {
+        setDirection(dir);
+        
+        if (typeof onDirectionChange === 'function') {
+            onDirectionChange(dir);
+        }
+    };
+
+    return (
+        <>
+            <div style={{ marginBottom: 20 }}>
+                <ButtonGroup>
+                    <Button onClick={() => handleDirectionChange('ltr')}>LTR</Button>
+                    <Button onClick={() => handleDirectionChange('rtl')}>RTL</Button>
+                </ButtonGroup>
+            </div>
+            <ConfigProvider direction={direction}>
+                {children}
+            </ConfigProvider>
+        </>
+    );
+}

+ 19 - 0
packages/semi-ui/datePicker/datePicker.tsx

@@ -21,9 +21,15 @@ import { RangeType } from '@douyinfe/semi-foundation/datePicker/inputFoundation'
 import { TimePickerProps } from '../timePicker/TimePicker';
 
 export interface DatePickerProps extends DatePickerFoundationProps {
+    'aria-describedby'?: React.AriaAttributes['aria-describedby'];
+    'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
+    'aria-invalid'?: React.AriaAttributes['aria-invalid'];
+    'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
+    'aria-required'?: React.AriaAttributes['aria-required'];
     timePickerOpts?: TimePickerProps;
     bottomSlot?: React.ReactNode;
     insetLabel?: React.ReactNode;
+    insetLabelId?: string;
     prefix?: React.ReactNode;
     topSlot?: React.ReactNode;
     renderDate?: (dayNumber?: number, fullDate?: string) => React.ReactNode;
@@ -42,6 +48,11 @@ export type DatePickerState = DatePickerFoundationState;
 export default class DatePicker extends BaseComponent<DatePickerProps, DatePickerState> {
     static contextType = ConfigContext;
     static propTypes = {
+        'aria-describedby': PropTypes.string,
+        'aria-errormessage': PropTypes.string,
+        'aria-invalid': PropTypes.bool,
+        'aria-labelledby': PropTypes.string,
+        'aria-required': PropTypes.bool,
         type: PropTypes.oneOf(strings.TYPE_SET),
         size: PropTypes.oneOf(strings.SIZE_SET),
         density: PropTypes.oneOf(strings.DENSITY_SET),
@@ -76,6 +87,7 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
         prefixCls: PropTypes.string,
         prefix: PropTypes.node,
         insetLabel: PropTypes.node,
+        insetLabelId: PropTypes.string,
         zIndex: PropTypes.number,
         position: PropTypes.oneOf(popoverStrings.POSITION_SET),
         getPopupContainer: PropTypes.func,
@@ -447,6 +459,7 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
             disabled,
             showClear,
             insetLabel,
+            insetLabelId,
             placeholder,
             validateStatus,
             inputStyle,
@@ -483,6 +496,7 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
             inputStyle,
             showClear,
             insetLabel,
+            insetLabelId,
             type,
             format,
             multiple,
@@ -637,6 +651,11 @@ export default class DatePicker extends BaseComponent<DatePickerProps, DatePicke
             style,
             className: classnames(className, { [prefixCls]: true }),
             ref: this.setTriggerRef,
+            'aria-invalid': this.props['aria-invalid'],
+            'aria-errormessage': this.props['aria-errormessage'],
+            'aria-labelledby': this.props['aria-labelledby'],
+            'aria-describedby': this.props['aria-describedby'],
+            'aria-required': this.props['aria-required'],
         };
 
         const inner = this.renderInner();

+ 3 - 2
packages/semi-ui/form/_story/FormApi/formApiDemo.jsx

@@ -17,6 +17,7 @@ import {
 } from '../../../index';
 const { Input, Select, DatePicker, Switch, Slider, CheckboxGroup, Checkbox, RadioGroup, Radio, TimePicker, InputNumber, InputGroup } = Form;
 
+import { IconPlusCircle, IconMinusCircle } from '@douyinfe/semi-icons';
 
 const SetValueUsingParentPath = () => {
     const { Option } = Form.Select;
@@ -51,7 +52,7 @@ const SetValueUsingParentPath = () => {
                     <React.Fragment>
                         <Button onClick={add} icon="plus_circle" theme="light">新增空白行</Button>
                         <Button
-                            icon="plus_circle" onClick={() => {
+                            icon={<IconPlusCircle></IconPlusCircle>} onClick={() => {
                                 addWithInitValue({ name: '自定义贴纸', type: '2D' });
                             }} style={{ marginLeft: 8 }}>新增带有初始值的行
                         </Button>
@@ -68,7 +69,7 @@ const SetValueUsingParentPath = () => {
                                         label={`素材类型:(${field}.type)`}
                                         style={{ width: 90 }}
                                     />
-                                    <Button type="danger" theme="borderless" icon="minus_circle" onClick={remove} style={{ margin: 12 }} />
+                                    <Button type="danger" theme="borderless" icon={<IconMinusCircle></IconMinusCircle>} onClick={remove} style={{ margin: 12 }} />
                                 </div>
                             ))
                         }

+ 12 - 3
packages/semi-ui/form/_story/demo.jsx

@@ -196,6 +196,8 @@ class BasicDemoWithInit extends Component {
                 onValueChange={(v)=>console.log(v)}
                 style={{ padding: '10px', width: 600 }}
                 autoScrollToError
+                aria-label='Demo Form'
+                id='demo-form-id'
                 >
                 <Form.Section text={'基本信息'}>
                     <Row>
@@ -265,13 +267,20 @@ class BasicDemoWithInit extends Component {
                     <Row>
                         <Col span={12}>
                             <Form.TimePicker
-                                treeData={treeData}
                                 field='time'
+                                helpText='原则上应当在 9:00 - 18:00 之间'
                                 label='时间选择'
                             >
-                            </Form.TimePicker>    
+                            </Form.TimePicker>
+                        </Col>
+                        <Col span={12}>
+                            <Form.AutoComplete
+                                field='typeData'
+                                label='类型选择'
+                                data={['1', '2' , '3']}
+                            >
+                            </Form.AutoComplete>
                         </Col>
-               
                     </Row>
                 </Form.Section>
                 <Form.Section text='资源详情'>

+ 0 - 7
packages/semi-ui/form/_story/form.stories.js

@@ -54,7 +54,6 @@ import { AssistComponent } from './Layout/slotDemo';
 import { ModalFormDemo } from './Layout/modalFormDemo';
 
 import { WithFieldDemo, CustomFieldDemo, NumberRange } from './HOC/withFieldDemo';
-import { WithDisplayName } from './HOC/displayName';
 import {
   CustomValidateDemo,
   ValidateFieldsDemo,
@@ -426,12 +425,6 @@ DebugRerenderTwice.story = {
   name: 'Debug-RerenderTwice',
 };
 
-export const FieldDisplayName = () => <WithDisplayName attr="form" />;
-
-FieldDisplayName.story = {
-  name: 'Field displayName',
-};
-
 export const _ChildDidMount = () => <ChildDidMount />;
 
 _ChildDidMount.story = {

+ 2 - 0
packages/semi-ui/form/baseForm.tsx

@@ -49,6 +49,7 @@ interface BaseFormState {
 }
 class Form extends BaseComponent<BaseFormProps, BaseFormState> {
     static propTypes = {
+        'aria-label': PropTypes.string,
         onSubmit: PropTypes.func,
         onSubmitFail: PropTypes.func,
         /* Triggered from update, including field mount/unmount/value change/blur/verification status change/error prompt change, input parameter is formState, currentField */
@@ -74,6 +75,7 @@ class Form extends BaseComponent<BaseFormProps, BaseFormState> {
         disabled: PropTypes.bool,
         showValidateIcon: PropTypes.bool,
         extraTextPosition: PropTypes.oneOf(strings.EXTRA_POS),
+        id: PropTypes.string,
     };
 
     static defaultProps = {

+ 13 - 2
packages/semi-ui/form/errorMessage.tsx

@@ -17,6 +17,8 @@ export interface ErrorMessageProps {
     validateStatus?: string;
     helpText?: React.ReactNode;
     isInInputGroup?: boolean;
+    errorMessageId?: string;
+    helpTextId?: string;
 }
 
 export default class ErrorMessage extends PureComponent<ErrorMessageProps> {
@@ -28,14 +30,23 @@ export default class ErrorMessage extends PureComponent<ErrorMessageProps> {
         showValidateIcon: PropTypes.bool,
         helpText: PropTypes.node,
         isInInputGroup: PropTypes.bool,
+        // internal props
+        errorMessageId: PropTypes.string,
+        helpTextId: PropTypes.string,
     };
 
     generatorText(error: ReactFieldError) {
+        const { helpTextId, errorMessageId } = this.props;
+        const propsError = this.props.error;
+        let id = errorMessageId;
+        if (!propsError) {
+            id = helpTextId;
+        }
         if (typeof error === 'string') {
-            return <span>{error}</span>;
+            return <span id={id}>{error}</span>;
         } else if (Array.isArray(error)) {
             const err = error.filter(e => e);
-            return err.length ? <span>{err.join(', ')}</span> : null;
+            return err.length ? <span id={id}>{err.join(', ')}</span> : null;
         } else if (React.isValidElement(error)) {
             return error;
         }

+ 37 - 8
packages/semi-ui/form/hoc/withField.tsx

@@ -63,6 +63,7 @@ function withField<
             extraText,
             extraTextPosition,
             pure,
+            id,
             rest,
         } = mergeProps(props);
         let { options, shouldInject } = mergeOptions(opts, props);
@@ -406,11 +407,27 @@ function withField<
         let mergeWrapperCol = wrapperCol || formProps.wrapperCol;
         let mergeExtraPos = extraTextPosition || formProps.extraTextPosition || 'bottom';
 
+        // id attribute to improve a11y
+        const a11yId = id ? id : field;
+        const labelId = `${a11yId}-label`;
+        const helpTextId = `${a11yId}-helpText`;
+        const extraTextId = `${a11yId}-extraText`;
+        const errorMessageId = `${a11yId}-errormessage`;
+
         let FieldComponent = (() => {
             // prefer to use validateStatus which pass by user throught props
             let blockStatus = validateStatus ? validateStatus : status;
 
+            const extraCls = classNames(`${prefix}-field-extra`, {
+                [`${prefix}-field-extra-string`]: typeof extraText === 'string',
+                [`${prefix}-field-extra-middle`]: mergeExtraPos === 'middle',
+                [`${prefix}-field-extra-botttom`]: mergeExtraPos === 'bottom',
+            });
+
+            const extraContent = extraText ? <div className={extraCls} id={extraTextId}>{extraText}</div> : null;
+
             let newProps: Record<string, any> = {
+                id: a11yId,
                 disabled: formProps.disabled,
                 ...rest,
                 ref,
@@ -418,8 +435,23 @@ function withField<
                 [options.onKeyChangeFnName]: handleChange,
                 [options.valueKey]: value,
                 validateStatus: blockStatus,
+                'aria-required': required,
+                'aria-labelledby': labelId,
             };
 
+            if (helpText) {
+                newProps['aria-describedby'] = extraText ? `${helpTextId} ${extraTextId}` : helpTextId;
+            }
+            
+            if (extraText) {
+                newProps['aria-describedby'] = helpText ? `${helpTextId} ${extraTextId}` : extraTextId;
+            }
+            
+            if (status === 'error') {
+                newProps['aria-errormessage'] = errorMessageId;
+                newProps['aria-invalid'] = true;
+            }
+
             const fieldCls = classNames({
                 [`${prefix}-field`]: true,
                 [`${prefix}-field-${name}`]: Boolean(name),
@@ -431,8 +463,10 @@ function withField<
 
             if (mergeLabelPos === 'inset' && !noLabel) {
                 newProps.insetLabel = label || field;
+                newProps.insetLabelId = labelId;
                 if (typeof label === 'object' && !isElement(label)) {
                     newProps.insetLabel = label.text;
+                    newProps.insetLabelId = labelId;
                 }
             }
 
@@ -463,6 +497,7 @@ function withField<
                 labelContent = (
                     <Label
                         text={label || field}
+                        id={labelId}
                         required={required}
                         name={name || field}
                         width={mergeLabelWidth}
@@ -472,14 +507,6 @@ function withField<
                 );
             }
 
-            const extraCls = classNames(`${prefix}-field-extra`, {
-                [`${prefix}-field-extra-string`]: typeof extraText === 'string',
-                [`${prefix}-field-extra-middle`]: mergeExtraPos === 'middle',
-                [`${prefix}-field-extra-botttom`]: mergeExtraPos === 'bottom',
-            });
-
-            const extraContent = extraText ? <div className={extraCls}>{extraText}</div> : null;
-
             const fieldMainContent = (
                 <div className={fieldMaincls}>
                     {mergeExtraPos === 'middle' ? extraContent : null}
@@ -489,6 +516,8 @@ function withField<
                             error={error}
                             validateStatus={blockStatus}
                             helpText={helpText}
+                            helpTextId={helpTextId}
+                            errorMessageId={errorMessageId}
                             showValidateIcon={formProps.showValidateIcon}
                         />
                     ) : null}

+ 2 - 0
packages/semi-ui/form/interface.ts

@@ -103,6 +103,7 @@ interface setValuesConfig {
 }
 
 export interface BaseFormProps {
+    'aria-label'?: React.AriaAttributes['aria-label'];
     onSubmit?: (values: Record<string, any>) => void;
     onSubmitFail?: (errors: Record<string, FieldError>, values: any) => void;
     onReset?: () => void;
@@ -111,6 +112,7 @@ export interface BaseFormProps {
     validateFields?: (values: Record<string, any>) => string | Record<string, any>;
     /** Use this if you want to populate the form with initial values. */
     initValues?: Record<string, any>;
+    id?: string;
     /** getFormApi will be call once when Form mounted, u can save formApi reference in your component  */
     getFormApi?: (formApi: FormApi) => void;
     style?: React.CSSProperties;

+ 4 - 2
packages/semi-ui/form/label.tsx

@@ -6,6 +6,7 @@ import { cssClasses } from '@douyinfe/semi-foundation/form/constants';
 const prefixCls = cssClasses.PREFIX;
 
 export interface LabelProps {
+    id?: string;
     /** Whether to display the required * symbol */
     required?: boolean;
     /** Content of label */
@@ -31,6 +32,7 @@ export default class Label extends PureComponent<LabelProps> {
     };
 
     static propTypes = {
+        id: PropTypes.string,
         children: PropTypes.node,
         required: PropTypes.bool,
         text: PropTypes.node,
@@ -44,7 +46,7 @@ export default class Label extends PureComponent<LabelProps> {
     };
 
     render() {
-        const { children, required, text, disabled, name, width, align, style, className, extra } = this.props;
+        const { children, required, text, disabled, name, width, align, style, className, extra, id } = this.props;
 
         const labelCls = classNames(className, {
             [`${prefixCls}-field-label`]: true,
@@ -71,7 +73,7 @@ export default class Label extends PureComponent<LabelProps> {
         );
 
         return (
-            <label className={labelCls} htmlFor={name} style={labelStyle}>
+            <label className={labelCls} htmlFor={name} style={labelStyle} id={id}>
                 {extra ? contentWithExtra : textContent}
             </label>
         );

+ 16 - 2
packages/semi-ui/input/index.tsx

@@ -26,6 +26,12 @@ export type ValidateStatus = "default" | "error" | "warning" | "success";
 
 export interface InputProps extends
     Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'prefix' | 'size' | 'autoFocus' | 'placeholder' | 'onFocus' | 'onBlur'> {
+    'aria-label'?: React.AriaAttributes['aria-label'];
+    'aria-describedby'?: React.AriaAttributes['aria-describedby'];
+    'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
+    'aria-invalid'?: React.AriaAttributes['aria-invalid'];
+    'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
+    'aria-required'?: React.AriaAttributes['aria-required'];
     addonBefore?: React.ReactNode;
     addonAfter?: React.ReactNode;
     prefix?: React.ReactNode;
@@ -41,6 +47,7 @@ export interface InputProps extends
     hideSuffix?: boolean;
     placeholder?: React.ReactText;
     insetLabel?: React.ReactNode;
+    insetLabelId?: string;
     size?: InputSize;
     className?: string;
     style?: React.CSSProperties;
@@ -73,6 +80,12 @@ export interface InputState {
 
 class Input extends BaseComponent<InputProps, InputState> {
     static propTypes = {
+        'aria-label': PropTypes.string,
+        'aria-labelledby': PropTypes.string,
+        'aria-invalid': PropTypes.bool,
+        'aria-errormessage': PropTypes.string,
+        'aria-describedby': PropTypes.string,
+        'aria-required': PropTypes.bool,
         addonBefore: PropTypes.node,
         addonAfter: PropTypes.node,
         prefix: PropTypes.node,
@@ -101,6 +114,7 @@ class Input extends BaseComponent<InputProps, InputState> {
         onKeyPress: PropTypes.func,
         onEnterPress: PropTypes.func,
         insetLabel: PropTypes.node,
+        insetLabelId: PropTypes.string,
         inputStyle: PropTypes.object,
         getValueLength: PropTypes.func,
     };
@@ -325,7 +339,7 @@ class Input extends BaseComponent<InputProps, InputState> {
     }
 
     renderPrefix() {
-        const { prefix, insetLabel } = this.props;
+        const { prefix, insetLabel, insetLabelId } = this.props;
         const labelNode = prefix || insetLabel;
         if (!labelNode) {
             return null;
@@ -338,7 +352,7 @@ class Input extends BaseComponent<InputProps, InputState> {
         });
 
         // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
-        return <div className={prefixWrapperCls} onMouseDown={this.handlePreventMouseDown} onClick={this.handleClickPrefixOrSuffix}>{labelNode}</div>;
+        return <div className={prefixWrapperCls} onMouseDown={this.handlePreventMouseDown} onClick={this.handleClickPrefixOrSuffix} id={insetLabelId}>{labelNode}</div>;
     }
 
     showClearBtn() {

+ 8 - 0
packages/semi-ui/inputNumber/index.tsx

@@ -29,6 +29,7 @@ export interface InputNumberProps extends InputProps {
     hideButtons?: boolean;
     innerButtons?: boolean;
     insetLabel?: React.ReactNode;
+    insetLabelId?: string;
     keepFocus?: boolean;
     max?: number;
     min?: number;
@@ -62,6 +63,12 @@ export interface InputNumberState {
 
 class InputNumber extends BaseComponent<InputNumberProps, InputNumberState> {
     static propTypes = {
+        'aria-label': PropTypes.string,
+        'aria-labelledby': PropTypes.string,
+        'aria-invalid': PropTypes.bool,
+        'aria-errormessage': PropTypes.string,
+        'aria-describedby': PropTypes.string,
+        'aria-required': PropTypes.bool,
         autofocus: PropTypes.bool,
         className: PropTypes.string,
         defaultValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
@@ -71,6 +78,7 @@ class InputNumber extends BaseComponent<InputNumberProps, InputNumberState> {
         hideButtons: PropTypes.bool,
         innerButtons: PropTypes.bool,
         insetLabel: PropTypes.node,
+        insetLabelId: PropTypes.string,
         keepFocus: PropTypes.bool,
         max: PropTypes.number,
         min: PropTypes.number,

+ 25 - 2
packages/semi-ui/radio/radioGroup.tsx

@@ -37,6 +37,12 @@ export type RadioGroupProps = {
     buttonSize?: RadioGroupButtonSize;
     prefixCls?: string;
     'aria-label'?: React.AriaAttributes['aria-label'];
+    'aria-describedby'?: React.AriaAttributes['aria-describedby'];
+    'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
+    'aria-invalid'?: React.AriaAttributes['aria-invalid'];
+    'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
+    'aria-required'?: React.AriaAttributes['aria-required'];
+    id?: string;
 };
 
 export interface RadioGroupState {
@@ -60,6 +66,12 @@ class RadioGroup extends BaseComponent<RadioGroupProps, RadioGroupState> {
         direction: PropTypes.oneOf(strings.DIRECTION_SET),
         mode: PropTypes.oneOf(strings.MODE),
         'aria-label': PropTypes.string,
+        'aria-describedby': PropTypes.string,
+        'aria-errormessage': PropTypes.string,
+        'aria-invalid': PropTypes.bool,
+        'aria-labelledby': PropTypes.string,
+        'aria-required': PropTypes.bool,
+        id: PropTypes.string,
     };
 
     static defaultProps: Partial<RadioGroupProps> = {
@@ -124,7 +136,8 @@ class RadioGroup extends BaseComponent<RadioGroupProps, RadioGroupState> {
             style,
             direction,
             type,
-            buttonSize
+            buttonSize,
+            id,
         } = this.props;
 
         const isButtonRadio = type === strings.TYPE_BUTTON;
@@ -180,7 +193,17 @@ class RadioGroup extends BaseComponent<RadioGroupProps, RadioGroupState> {
         }
 
         return (
-            <div className={prefixClsDisplay} style={style} aria-label={this.props['aria-label']}>
+            <div
+                className={prefixClsDisplay}
+                style={style}
+                id={id}
+                aria-label={this.props['aria-label']}
+                aria-invalid={this.props['aria-invalid']}
+                aria-errormessage={this.props['aria-errormessage']}
+                aria-labelledby={this.props['aria-labelledby']}
+                aria-describedby={this.props['aria-describedby']}
+                aria-required={this.props['aria-required']}
+            >
                 <Context.Provider
                     value={{
                         radioGroup: {

+ 16 - 4
packages/semi-ui/rating/index.tsx

@@ -14,6 +14,12 @@ import '@douyinfe/semi-foundation/rating/rating.scss';
 
 export { RatingItemProps } from './item';
 export interface RatingProps {
+    'aria-describedby'?: string;
+    'aria-errormessage'?: string;
+    'aria-invalid'?: boolean;
+    'aria-label'?: string;
+    'aria-labelledby'?: string;
+    'aria-required'?: boolean;
     disabled?: boolean;
     value?: number;
     defaultValue?: number;
@@ -34,9 +40,7 @@ export interface RatingProps {
     autoFocus?: boolean;
     size?: 'small' | 'default' | number;
     tooltips?: string[];
-    'aria-label'?: React.AriaAttributes['aria-label'];
-    'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
-    'aria-describedby'?: React.AriaAttributes['aria-describedby'];
+    id?: string;
 }
 
 export interface RatingState {
@@ -49,6 +53,12 @@ export interface RatingState {
 export default class Rating extends BaseComponent<RatingProps, RatingState> {
     static contextType = ConfigContext;
     static propTypes = {
+        'aria-describedby': PropTypes.string,
+        'aria-errormessage': PropTypes.string,
+        'aria-invalid': PropTypes.bool,
+        'aria-label': PropTypes.string,
+        'aria-labelledby': PropTypes.string,
+        'aria-required': PropTypes.bool,
         disabled: PropTypes.bool,
         value: PropTypes.number,
         defaultValue: PropTypes.number,
@@ -68,6 +78,7 @@ export default class Rating extends BaseComponent<RatingProps, RatingState> {
         autoFocus: PropTypes.bool,
         size: PropTypes.oneOfType([PropTypes.oneOf(strings.SIZE_SET), PropTypes.number]),
         tooltips: PropTypes.arrayOf(PropTypes.string),
+        id: PropTypes.string,
     };
 
     static defaultProps = {
@@ -228,7 +239,7 @@ export default class Rating extends BaseComponent<RatingProps, RatingState> {
     };
 
     render() {
-        const { count, allowHalf, style, prefixCls, disabled, className, character, tabIndex, size, tooltips } =
+        const { count, allowHalf, style, prefixCls, disabled, className, character, tabIndex, size, tooltips, id } =
             this.props;
         const { value, hoverValue, focused } = this.state;
         const itemList = [...Array(count).keys()].map(ind => {
@@ -280,6 +291,7 @@ export default class Rating extends BaseComponent<RatingProps, RatingState> {
                 onBlur={disabled ? null : this.onBlur}
                 onKeyDown={disabled ? null : this.onKeyDown}
                 ref={this.saveRate as any}
+                id={id}
             >
                 {itemList}
             </ul>

+ 20 - 2
packages/semi-ui/select/index.tsx

@@ -85,6 +85,11 @@ export type RenderMultipleSelectedItemFn = (optionNode: Record<string, any>, mul
 export type RenderSelectedItemFn = RenderSingleSelectedItemFn | RenderMultipleSelectedItemFn;
 
 export type SelectProps = {
+    'aria-describedby'?: React.AriaAttributes['aria-describedby'];
+    'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
+    'aria-invalid'?: React.AriaAttributes['aria-invalid'];
+    'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
+    'aria-required'?: React.AriaAttributes['aria-required'];
     id?: string;
     autoFocus?: boolean;
     arrowIcon?: React.ReactNode;
@@ -122,6 +127,7 @@ export type SelectProps = {
     suffix?: React.ReactNode;
     prefix?: React.ReactNode;
     insetLabel?: React.ReactNode;
+    insetLabelId?: string;
     inputProps?: Subtract<InputProps, ExcludeInputType>;
     showClear?: boolean;
     showArrow?: boolean;
@@ -180,6 +186,11 @@ class Select extends BaseComponent<SelectProps, SelectState> {
     static OptGroup = OptionGroup;
 
     static propTypes = {
+        'aria-describedby': PropTypes.string,
+        'aria-errormessage': PropTypes.string,
+        'aria-invalid': PropTypes.bool,
+        'aria-labelledby': PropTypes.string,
+        'aria-required': PropTypes.bool,
         autoFocus: PropTypes.bool,
         children: PropTypes.node,
         defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object]),
@@ -227,6 +238,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
         suffix: PropTypes.node,
         prefix: PropTypes.node,
         insetLabel: PropTypes.node,
+        insetLabelId: PropTypes.string,
         showClear: PropTypes.bool,
         showArrow: PropTypes.bool,
 
@@ -987,7 +999,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
     }
 
     renderPrefix() {
-        const { prefix, insetLabel } = this.props;
+        const { prefix, insetLabel, insetLabelId } = this.props;
         const labelNode = (prefix || insetLabel) as React.ReactElement<any, any>;
 
         const prefixWrapperCls = cls({
@@ -997,7 +1009,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
             [`${prefixcls}-prefix-icon`]: isSemiIcon(labelNode),
         });
 
-        return <div className={prefixWrapperCls}>{labelNode}</div>;
+        return <div className={prefixWrapperCls} id={insetLabelId}>{labelNode}</div>;
     }
 
     renderSelection() {
@@ -1018,6 +1030,7 @@ class Select extends BaseComponent<SelectProps, SelectState> {
             triggerRender,
             arrowIcon,
         } = this.props;
+
         const { selections, isOpen, keyboardEventSet, inputValue, isHovering, isFocus } = this.state;
         const useCustomTrigger = typeof triggerRender === 'function';
         const filterable = Boolean(filter); // filter(boolean || function)
@@ -1098,6 +1111,11 @@ class Select extends BaseComponent<SelectProps, SelectState> {
                 aria-controls={`${prefixcls}-${this.selectOptionListID}`}
                 aria-haspopup="listbox"
                 aria-label="select value"
+                aria-invalid={this.props['aria-invalid']}
+                aria-errormessage={this.props['aria-errormessage']}
+                aria-labelledby={this.props['aria-labelledby']}
+                aria-describedby={this.props['aria-describedby']}
+                aria-required={this.props['aria-required']}
                 className={selectionCls}
                 ref={ref => ((this.triggerRef as any).current = ref)}
                 onClick={e => this.foundation.handleClick(e)}

+ 18 - 8
packages/semi-ui/switch/index.tsx

@@ -1,4 +1,4 @@
-/* eslint-disable max-len */
+/* eslint-disable max-len, jsx-a11y/role-supports-aria-props */
 import React from 'react';
 import cls from 'classnames';
 import PropTypes from 'prop-types';
@@ -10,8 +10,11 @@ import '@douyinfe/semi-foundation/switch/switch.scss';
 import { noop } from 'lodash';
 import Spin from '../spin';
 export interface SwitchProps {
-    'aria-label'?: string | undefined;
-    'aria-labelledby'?: string | undefined;
+    'aria-label'?: React.AriaAttributes['aria-label'];
+    'aria-describedby'?: React.AriaAttributes['aria-describedby'];
+    'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
+    'aria-invalid'?: React.AriaAttributes['aria-invalid'];
+    'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
     defaultChecked?: boolean;
     checked?: boolean;
     disabled?: boolean;
@@ -24,6 +27,7 @@ export interface SwitchProps {
     size?: 'large' | 'default' | 'small';
     checkedText?: React.ReactNode;
     uncheckedText?: React.ReactNode;
+    id?: string;
 } 
 
 export interface SwitchState {
@@ -35,6 +39,9 @@ class Switch extends BaseComponent<SwitchProps, SwitchState> {
     static propTypes = {
         'aria-label': PropTypes.string,
         'aria-labelledby': PropTypes.string,
+        'aria-invalid': PropTypes.bool,
+        'aria-errormessage': PropTypes.string,
+        'aria-describedby': PropTypes.string,
         className: PropTypes.string,
         checked: PropTypes.bool,
         checkedText: PropTypes.node,
@@ -47,6 +54,7 @@ class Switch extends BaseComponent<SwitchProps, SwitchState> {
         style: PropTypes.object,
         size: PropTypes.oneOf<SwitchProps['size']>(strings.SIZE_MAP),
         uncheckedText: PropTypes.node,
+        id: PropTypes.string,
     };
 
     static defaultProps: Partial<SwitchProps> = {
@@ -105,9 +113,7 @@ class Switch extends BaseComponent<SwitchProps, SwitchState> {
 
     render() {
         const { nativeControlChecked, nativeControlDisabled } = this.state;
-        const { className, style, onMouseEnter, onMouseLeave, size, checkedText, uncheckedText, loading } = this.props;
-        const ariaLabel = this.props['aria-label'];
-        const ariaLabelledBy = this.props['aria-labelledby'];
+        const { className, style, onMouseEnter, onMouseLeave, size, checkedText, uncheckedText, loading, id } = this.props;
         const wrapperCls = cls(className, {
             [cssClasses.PREFIX]: true,
             [cssClasses.CHECKED]: nativeControlChecked,
@@ -150,9 +156,13 @@ class Switch extends BaseComponent<SwitchProps, SwitchState> {
                 <input
                     {...switchProps}
                     ref={this.switchRef}
-                    aria-labelledby={ariaLabelledBy}
-                    aria-label={ariaLabel}
+                    id={id}
                     aria-checked={nativeControlChecked}
+                    aria-invalid={this.props['aria-invalid']}
+                    aria-errormessage={this.props['aria-errormessage']}
+                    aria-label={this.props['aria-label']}
+                    aria-labelledby={this.props['aria-labelledby']}
+                    aria-describedby={this.props["aria-describedby"]}
                     onChange={e => this.foundation.handleChange(e.target.checked, e)}
                 />
             </div>

+ 14 - 1
packages/semi-ui/timePicker/TimePicker.tsx

@@ -36,6 +36,11 @@ export type BaseValueType = string | number | Date;
 export type Type = 'time' | 'timeRange';
 
 export type TimePickerProps = {
+    'aria-describedby'?: React.AriaAttributes['aria-describedby'];
+    'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
+    'aria-invalid'?: React.AriaAttributes['aria-invalid'];
+    'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
+    'aria-required'?: React.AriaAttributes['aria-required'];
     autoAdjustOverflow?: boolean;
     autoFocus?: boolean; // TODO: autoFocus did not take effect
     className?: string;
@@ -56,6 +61,7 @@ export type TimePickerProps = {
     inputReadOnly?: boolean;
     inputStyle?: React.CSSProperties;
     insetLabel?: React.ReactNode;
+    insetLabelId?: string;
     locale?: Locale['TimePicker'];
     localeCode?: string;
     minuteStep?: number;
@@ -100,10 +106,14 @@ export interface TimePickerState {
     invalid: boolean;
 }
 
-
 export default class TimePicker extends BaseComponent<TimePickerProps, TimePickerState> {
     static contextType = ConfigContext;
     static propTypes = {
+        'aria-labelledby': PropTypes.string,
+        'aria-invalid': PropTypes.bool,
+        'aria-errormessage': PropTypes.string,
+        'aria-describedby': PropTypes.string,
+        'aria-required': PropTypes.bool,
         prefixCls: PropTypes.string,
         clearText: PropTypes.string,
         value: TimeShape,
@@ -142,6 +152,7 @@ export default class TimePicker extends BaseComponent<TimePickerProps, TimePicke
         dateFnsLocale: PropTypes.object,
         zIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
         insetLabel: PropTypes.node,
+        insetLabelId: PropTypes.string,
         validateStatus: PropTypes.oneOf(strings.STATUS),
         type: PropTypes.oneOf<TimePickerProps['type']>(strings.TYPES),
         rangeSeparator: PropTypes.string,
@@ -429,6 +440,7 @@ export default class TimePicker extends BaseComponent<TimePickerProps, TimePicke
             zIndex,
             getPopupContainer,
             insetLabel,
+            insetLabelId,
             inputStyle,
             showClear,
             panelHeader,
@@ -473,6 +485,7 @@ export default class TimePicker extends BaseComponent<TimePickerProps, TimePicke
             value: inputValue,
             onFocus: this.handleFocus,
             insetLabel,
+            insetLabelId,
             format,
             locale,
             localeCode,

+ 4 - 4
packages/semi-ui/tooltip/index.tsx

@@ -614,19 +614,20 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
         }
 
         // eslint-disable-next-line prefer-const
-        let ariaAttribute = {
-            'aria-describedby': id,
-        };
+        let ariaAttribute = {};
 
         // Take effect when used by Popover component
         if (role === 'dialog') {
             ariaAttribute['aria-expanded'] = visible ? 'true' : 'false';
             ariaAttribute['aria-haspopup'] = 'dialog';
             ariaAttribute['aria-controls'] = id;
+        } else {
+            ariaAttribute['aria-describedby'] = id;
         }
 
         // The incoming children is a single valid element, otherwise wrap a layer with span
         const newChild = React.cloneElement(children as React.ReactElement, {
+            ...ariaAttribute,
             ...(children as React.ReactElement).props,
             ...this.mergeEvents((children as React.ReactElement).props, triggerEventSet),
             style: {
@@ -649,7 +650,6 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
                     ref.current = node;
                 }
             },
-            ...ariaAttribute
         });
 
         // If you do not add a layer of div, in order to bind the events and className in the tooltip, you need to cloneElement children, but this time it may overwrite the children's original ref reference

+ 1 - 1
packages/semi-ui/tree/index.tsx

@@ -739,7 +739,7 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
                     labelEllipsis: typeof labelEllipsis === 'undefined' ? virtualize : labelEllipsis,
                 }}
             >
-                <div className={wrapperCls} role="list-box" style={style}>
+                <div className={wrapperCls} role="listbox" style={style}>
                     {filterTreeNode ? this.renderInput() : null}
                     <div className={listCls} role="tree">
                         {noData ? this.renderEmpty() : this.renderNodeList()}

+ 29 - 6
packages/semi-ui/treeSelect/index.tsx

@@ -99,6 +99,11 @@ export type OverrideCommonProps =
 */
 // eslint-disable-next-line max-len
 export interface TreeSelectProps extends Omit<BasicTreeSelectProps, OverrideCommonProps | 'validateStatus' | 'searchRender'>, Pick<TreeProps, OverrideCommonProps>{
+    'aria-describedby'?: React.AriaAttributes['aria-describedby'];
+    'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
+    'aria-invalid'?: React.AriaAttributes['aria-invalid'];
+    'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
+    'aria-required'?: React.AriaAttributes['aria-required'];
     motion?: Motion;
     mouseEnterDelay?: number;
     mouseLeaveDelay?: number;
@@ -110,6 +115,7 @@ export interface TreeSelectProps extends Omit<BasicTreeSelectProps, OverrideComm
     dropdownMatchSelectWidth?: boolean;
     dropdownStyle?: React.CSSProperties;
     insetLabel?: React.ReactNode;
+    insetLabelId?: string;
     maxTagCount?: number;
     motionExpand?: boolean;
     optionListStyle?: React.CSSProperties;
@@ -164,6 +170,11 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
     static contextType = ConfigContext;
 
     static propTypes = {
+        'aria-describedby': PropTypes.string,
+        'aria-errormessage': PropTypes.string,
+        'aria-invalid': PropTypes.bool,
+        'aria-labelledby': PropTypes.string,
+        'aria-required': PropTypes.bool,
         loadedKeys: PropTypes.arrayOf(PropTypes.string),
         loadData: PropTypes.func,
         onLoad: PropTypes.func,
@@ -218,6 +229,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
         suffix: PropTypes.node,
         prefix: PropTypes.node,
         insetLabel: PropTypes.node,
+        insetLabelId: PropTypes.string,
         zIndex: PropTypes.number,
         getPopupContainer: PropTypes.func,
         dropdownMatchSelectWidth: PropTypes.bool,
@@ -614,7 +626,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
     };
 
     renderPrefix = () => {
-        const { prefix, insetLabel }: any = this.props;
+        const { prefix, insetLabel, insetLabelId }: any = this.props;
         const labelNode = prefix || insetLabel;
         const prefixWrapperCls = cls({
             [`${prefixcls}-prefix`]: true,
@@ -624,7 +636,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
             [`${prefixcls}-prefix-icon`]: isSemiIcon(labelNode),
         });
 
-        return <div className={prefixWrapperCls}>{labelNode}</div>;
+        return <div className={prefixWrapperCls} id={insetLabelId}>{labelNode}</div>;
     };
 
     renderContent = () => {
@@ -633,7 +645,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
         const style = { minWidth: dropdownMinWidth, ...dropdownStyle };
         const popoverCls = cls(dropdownClassName, `${prefixcls}-popover`);
         return (
-            <div className={popoverCls} role="list-box" style={style}>
+            <div className={popoverCls} role="listbox" style={style}>
                 {this.renderTree()}
             </div>
         );
@@ -812,7 +824,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
         const clearCls = cls(`${prefixcls}-clearbtn`);
         if (showClearBtn) {
             return (
-                <div className={clearCls} onClick={this.handleClear}>
+                <div className={clearCls} onClick={this.handleClear} role='button' tabIndex={0}>
                     <IconClear />
                 </div>
             );
@@ -902,14 +914,25 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
                 <Fragment key={'arrow'}>{this.renderArrow()}</Fragment>,
             ]
         );
-
+        /**
+         * Reasons for disabling the a11y eslint rule:
+         * The following attributes(aria-controls,aria-expanded) will be automatically added by Tooltip, no need to declare here
+         */
         return (
             <div
                 className={classNames}
                 style={style}
                 ref={this.triggerRef}
                 onClick={this.handleClick}
+                aria-invalid={this.props['aria-invalid']}
+                aria-errormessage={this.props['aria-errormessage']}
+                aria-labelledby={this.props['aria-labelledby']}
+                aria-describedby={this.props['aria-describedby']}
+                aria-required={this.props['aria-required']}
                 {...mouseEvent}
+                // eslint-disable-next-line jsx-a11y/role-has-required-aria-props
+                role='combobox'
+                tabIndex={0}
             >
                 {inner}
             </div>
@@ -1232,7 +1255,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
                     labelEllipsis: typeof labelEllipsis === 'undefined' ? virtualize : labelEllipsis,
                 }}
             >
-                <div className={wrapperCls} role="list-box">
+                <div className={wrapperCls} role="listbox">
                     {outerTopSlot}
                     {
                         !outerTopSlot &&

+ 3 - 3
src/components/Footer/index.jsx

@@ -10,15 +10,15 @@ export class Footer extends Component {
 
     render() {
         return (
-            <div className='footerMini8'><img src="https://lf9-static.semi.design/obj/semi-tos/images/a5768a90-324e-11ec-b393-ab4adc2e449f.svg" className='group6' />
+            <div className='footerMini8'><img alt="semi logo" aria-hidden src="https://lf9-static.semi.design/obj/semi-tos/images/a5768a90-324e-11ec-b393-ab4adc2e449f.svg" className='group6' />
                 <div className='links'>
                     <a href={`/${getLocale()}/start/getting-started`} className='text'>{_t('footer.component')}</a>
                     <a href='https://figma.com/@semi' className='figmaUIKit' target="_blank" rel="noreferrer">Figma UIKit</a>
                     <p className='text_d3ba282e'><a href="https://semi.design/dsm/landing" className='text_8b88424e' target="_blank" rel="noreferrer">{_t('footer.dsm')}</a></p>
                     <a href="https://github.com/DouyinFE/semi-design" className='github' target="_blank" rel="noreferrer">GitHub</a>
-                </div><img src="https://lf9-static.semi.design/obj/semi-tos/images/a577c310-324e-11ec-8b14-8fb159794ae4.svg" className='divider' />
+                </div><img src="https://lf9-static.semi.design/obj/semi-tos/images/a577c310-324e-11ec-8b14-8fb159794ae4.svg" className='divider' aria-hidden alt='' />
                 <div className='autoWrapper'>
-                    <div className='icpWrapper'><p className={'a2021SemiDesignAllRi'}>© 2021 Semi Design. All rights reserved.</p><a href="https://beian.miit.gov.cn/" target="_blank" className={'beianText'} rel="noreferrer">京ICP备19058139号-13</a><img src="https://lf9-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/beian.png" className={'image45'} /><a href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=33011002016131" target="_blank" className={'beianText'} rel="noreferrer">浙公网安备 33011002016131号</a></div>
+                    <div className='icpWrapper'><p className={'a2021SemiDesignAllRi'}>© 2021 Semi Design. All rights reserved.</p><a href="https://beian.miit.gov.cn/" target="_blank" className={'beianText'} rel="noreferrer">京ICP备19058139号-13</a><img src="https://lf9-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/beian.png" className={'image45'} alt=''/><a href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=33011002016131" target="_blank" className={'beianText'} rel="noreferrer">浙公网安备 33011002016131号</a></div>
                     <p className='designedDevelopedWit'><span className='designedDevelopedWit_c0c5d39b'>Designed & Developed with love by </span><span className='designedDevelopedWit_6eaa79ba'>Douyin FE</span><span className='designedDevelopedWit_c0c5d39b'> & </span><a href="https://dribbble.com/MetaEnterpriseDesign" className='designedDevelopedWit_6eaa79ba'>MED</a></p>
                 </div>
             </div>

+ 9 - 2
src/sitePages/newHome/components/banner/banner.jsx

@@ -1,3 +1,4 @@
+/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { _t } from "src/utils/locale";
 import { Button } from '@douyinfe/semi-ui';
 import { navigate } from 'gatsby-link';
@@ -23,11 +24,17 @@ function Banner() {
                     </div>
                     <div className={styles.group2835}>
                         <Button onClick={goStart} size="large" theme="solid" className={styles.extraLarge}>{_t("start_using", { }, "开始使用")}</Button>
-                        <div onClick={goGithub} className={styles.buttonSecondarySolid_4427b030}><IconGithubLogo size="extra-large" /><p className={styles.text_bff7eaeb}>GitHub</p>
+                        <div onClick={goGithub} className={styles.buttonSecondarySolid_4427b030} role="button" tabIndex={0}><IconGithubLogo size="extra-large" /><p className={styles.text_bff7eaeb}>GitHub</p>
                         </div>
                     </div>
                 </div>
-                <div className={styles.autoWrapper_4fa00029}><div className={styles.background}></div><img src="https://lf9-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/37361.png" className={styles.group3736} /></div>
+                <div className={styles.autoWrapper_4fa00029}>
+                    <div className={styles.background}></div>
+                    <img
+                        src="https://lf9-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/37361.png"
+                        alt="semi application demo"
+                        className={styles.group3736} />
+                </div>
             </div>
         </div>
     );