Преглед на файлове

feat(a11y): Input, InputGroup, TextArea #205

走鹃 преди 3 години
родител
ревизия
8d23bf926e

+ 19 - 0
packages/semi-foundation/input/foundation.ts

@@ -1,6 +1,7 @@
 import BaseFoundation, { DefaultAdapter, noopFunction } from '../base/foundation';
 import { strings } from './constants';
 import { noop, set, isNumber, isString, isFunction } from 'lodash-es';
+import isEnterPress from '../utils/isEnterPress';
 
 const ENTER_KEY_CODE = 'Enter';
 
@@ -294,5 +295,23 @@ class InputFoundation extends BaseFoundation<InputAdapter> {
             e.preventDefault();
         }
     }
+
+    /**
+     * A11y: simulate clear button click
+     */
+    handleClearEnterPress(e: any) {
+        if(isEnterPress(e)) {
+            this.handleClear(e);
+        }
+    }
+
+    /**
+     * A11y: simulate password button click
+     */
+    handleModeEnterPress(e: any) {
+        if (isEnterPress(e)) {
+            this.handleClickEye(e);
+        }
+    }
 }
 export default InputFoundation;

+ 11 - 1
packages/semi-foundation/input/textareaFoundation.ts

@@ -7,6 +7,7 @@ import {
 } from 'lodash-es';
 import calculateNodeHeight from './util/calculateNodeHeight';
 import getSizingData from './util/getSizingData';
+import isEnterPress from '../utils/isEnterPress';
 
 export interface TextAreaDefaultAdpter {
     notifyChange: noopFunction;
@@ -171,7 +172,7 @@ export default class TextAreaFoundation extends BaseFoundation<TextAreaAdpter> {
         }
     }
 
-    resizeTextarea = (cb: any) => {
+    resizeTextarea = (cb?: any) => {
         const { height } = this.getStates();
         const { rows } = this.getProps();
         const node = this._adapter.getRef().current;
@@ -232,4 +233,13 @@ export default class TextAreaFoundation extends BaseFoundation<TextAreaAdpter> {
         this._adapter.notifyClear(e);
         this.stopPropagation(e);
     }
+
+    /**
+     * A11y: simulate clear button click
+     */
+    handleClearEnterPress(e: any) {
+        if (isEnterPress(e)) {
+            this.handleClear(e);
+        }
+    }
 }

+ 8 - 0
packages/semi-foundation/utils/isEnterPress.ts

@@ -0,0 +1,8 @@
+import { get } from 'lodash-es';
+import { ENTER_KEY_CODE } from './keyCode';
+
+function isEnterPress<T extends { key: string }>(e: T) {
+    return get(e, 'key') === ENTER_KEY_CODE ? true : false;
+}
+
+export default isEnterPress;

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

@@ -426,4 +426,6 @@ const keyCode = {
     WIN_IME: 229,
 };
 
+export const ENTER_KEY_CODE = 'Enter';
+
 export default keyCode;

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

@@ -207,6 +207,10 @@ class Input extends BaseComponent<InputProps, InputState> {
         this.foundation.handleClear(e);
     };
 
+    handleClearEnterPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
+        this.foundation.handleClearEnterPress(e);
+    };
+
     handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
         this.foundation.handleClick(e);
     };
@@ -235,6 +239,10 @@ class Input extends BaseComponent<InputProps, InputState> {
         this.foundation.handleMouseUp(e);
     };
 
+    handleModeEnterPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
+        this.foundation.handleModeEnterPress(e);
+    }
+
     handleClickPrefixOrSuffix = (e: React.MouseEvent<HTMLInputElement>) => {
         this.foundation.handleClickPrefixOrSuffix(e);
     };
@@ -275,7 +283,14 @@ class Input extends BaseComponent<InputProps, InputState> {
         // use onMouseDown to fix issue 1203
         if (allowClear) {
             return (
-                <div className={clearCls} onMouseDown={this.handleClear}>
+                <div
+                    role="button"
+                    tabIndex={0}
+                    aria-label="Clear input value"
+                    className={clearCls}
+                    onMouseDown={this.handleClear}
+                    onKeyPress={this.handleClearEnterPress}
+                >
                     <IconClear />
                 </div>
             );
@@ -289,9 +304,19 @@ class Input extends BaseComponent<InputProps, InputState> {
         const modeCls = cls(`${prefixCls}-modebtn`);
         const modeIcon = eyeClosed ? <IconEyeClosedSolid /> : <IconEyeOpened />;
         const showModeBtn = mode === 'password' && value && !disabled && (isFocus || isHovering);
+        const ariaLabel = eyeClosed ? 'Show password' : 'Hidden password'
         if (showModeBtn) {
             return (
-                <div className={modeCls} onClick={this.handleClickEye} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
+                <div
+                    role="button"
+                    tabIndex={0}
+                    aria-label={ariaLabel}
+                    className={modeCls}
+                    onClick={this.handleClickEye}
+                    onMouseDown={this.handleMouseDown}
+                    onMouseUp={this.handleMouseUp}
+                    onKeyPress={this.handleModeEnterPress}
+                >
                     {modeIcon}
                 </div>
             );
@@ -312,6 +337,7 @@ class Input extends BaseComponent<InputProps, InputState> {
             [`${prefixCls}-prefix-icon`]: isSemiIcon(labelNode),
         });
 
+        // 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>;
     }
 
@@ -332,6 +358,7 @@ class Input extends BaseComponent<InputProps, InputState> {
             [`${prefixCls }-suffix-icon`]: isSemiIcon(suffix),
             [`${prefixCls}-suffix-hidden`]: suffixAllowClear && Boolean(hideSuffix),
         });
+        // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
         return <div className={suffixWrapperCls} onMouseDown={this.handlePreventMouseDown} onClick={this.handleClickPrefixOrSuffix}>{suffix}</div>;
     }
 
@@ -417,7 +444,11 @@ class Input extends BaseComponent<InputProps, InputState> {
         if (stateMinLength) {
             inputProps.minLength = stateMinLength;
         }
+        if (validateStatus === 'error') {
+            inputProps['aria-invalid'] = "true";
+        }
         return (
+            // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
             <div
                 className={wrapperCls}
                 style={style}

+ 9 - 4
packages/semi-ui/input/inputGroup.tsx

@@ -4,7 +4,7 @@ import cls from 'classnames';
 import PropTypes from 'prop-types';
 import { cssClasses, strings } from '@douyinfe/semi-foundation/input/constants';
 import BaseComponent from '../_base/baseComponent';
-import Label from '../form/label';
+import Label, { LabelProps } from '../form/label';
 
 import { noop } from '@douyinfe/semi-foundation/utils/function';
 import { isFunction } from 'lodash-es';
@@ -21,7 +21,7 @@ export interface InputGroupProps {
     style?: Record<string, any>;
     onBlur?: (e: React.FocusEvent<HTMLSpanElement>) => void;
     onFocus?: (e: React.FocusEvent<HTMLSpanElement>) => void;
-    label?: Record<string, any>;
+    label?: LabelProps;
     labelPosition?: string;
     disabled?: boolean;
 }
@@ -64,10 +64,12 @@ export default class inputGroup extends BaseComponent<InputGroupProps, InputGrou
             }
         );
         // const labelCls = cls(label.className, '');
+        const defaultName = 'input-group';
         return (
-            <div className={groupWrapperCls}>
-                {label && label.text ? <Label {...label} /> : null}
+            <div role="group" aria-label="Input group" aria-disabled={this.props.disabled} className={groupWrapperCls}>
+                {label && label.text ? <Label name={defaultName} {...label} /> : null}
                 <span
+                    id={label && label.name || defaultName}
                     className={groupCls}
                     style={this.props.style}
                     onFocus={this.props.onFocus}
@@ -107,6 +109,9 @@ export default class inputGroup extends BaseComponent<InputGroupProps, InputGrou
 
         return (
             <span
+                role="group"
+                aria-label="Input group"
+                aria-disabled={this.props.disabled}
                 className={groupCls}
                 style={style}
                 onFocus={this.props.onFocus}

+ 25 - 6
packages/semi-ui/input/textarea.tsx

@@ -101,6 +101,7 @@ class TextArea extends BaseComponent<TextAreaProps, TextAreaState> {
     libRef: React.RefObject<React.ReactNode>;
     _resizeLock: boolean;
     _resizeListener: any;
+    foundation: TextAreaFoundation;
 
     constructor(props: TextAreaProps) {
         super(props);
@@ -197,6 +198,10 @@ class TextArea extends BaseComponent<TextAreaProps, TextAreaState> {
         this.foundation.handleClear(e);
     };
 
+    handleClearEnterPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
+        this.foundation.handleClearEnterPress(e);
+    }
+
     renderClearBtn() {
         const { showClear } = this.props;
         const displayClearBtn = this.foundation.isAllowClear();
@@ -205,7 +210,14 @@ class TextArea extends BaseComponent<TextAreaProps, TextAreaState> {
         });
         if (showClear) {
             return (
-                <div className={clearCls} onClick={this.handleClear}>
+                <div
+                    role="button"
+                    tabIndex={0}
+                    aria-label="Clear textarea value"
+                    className={clearCls}
+                    onClick={this.handleClear}
+                    onKeyPress={this.handleClearEnterPress}
+                >
                     <IconClear />
                 </div>
             );
@@ -214,10 +226,10 @@ class TextArea extends BaseComponent<TextAreaProps, TextAreaState> {
     }
 
     renderCounter() {
-        let counter,
-            current,
-            total,
-            countCls;
+        let counter: React.ReactNode,
+            current: number,
+            total: number,
+            countCls: string;
         const { showCounter, maxCount, getValueLength } = this.props;
         if (showCounter || maxCount) {
             const { value } = this.state;
@@ -231,7 +243,14 @@ class TextArea extends BaseComponent<TextAreaProps, TextAreaState> {
                 }
             );
             counter = (
-                <div className={countCls}>{current}{total ? '/' : null}{total}</div>
+                <div
+                    aria-label="Textarea value length counter"
+                    aria-valuemax={maxCount}
+                    aria-valuenow={current}
+                    className={countCls}
+                >
+                    {current}{total ? '/' : null}{total}
+                </div>
             );
         } else {
             counter = null;