Răsfoiți Sursa

feat(input): WIP a11y

shijia.me 3 ani în urmă
părinte
comite
5d0198e4c5

+ 11 - 2
packages/semi-foundation/input/foundation.ts

@@ -4,6 +4,7 @@ import { noop, set, isNumber, isString, isFunction } from 'lodash';
 
 import isEnterPress from '../utils/isEnterPress';
 import { ENTER_KEY } from './../utils/keyCode';
+import getSelections from './util/getSelections';
 
 export interface InputDefaultAdapter {
     notifyChange: noopFunction;
@@ -24,6 +25,7 @@ export interface InputAdapter extends Partial<DefaultAdapter>, Partial<InputDefa
     notifyEnterPress(e: any): void;
     setPaddingLeft(paddingLeft: string): void;
     isEventTarget(e: any): boolean;
+    toggleEditing(isEdit: boolean): void;
 }
 
 class InputFoundation extends BaseFoundation<InputAdapter> {
@@ -238,12 +240,19 @@ class InputFoundation extends BaseFoundation<InputAdapter> {
     handleBlur(e: any) {
         const { value } = this.getStates();
         this._adapter.toggleFocusing(false);
+        this._adapter.toggleEditing(false);
         this._adapter.notifyBlur(value, e);
     }
 
     handleFocus(e: any) {
         const { value } = this.getStates();
         this._adapter.toggleFocusing(true);
+        const selections = getSelections(e);
+        if (selections && selections.selectionStart === selections.selectionEnd) {
+            this._adapter.toggleEditing(true);
+        } else {
+            this._adapter.toggleEditing(false);
+        }
         // this.checkAllowClear(this.getState('value'), true);
         this._adapter.notifyFocus(value, e);
     }
@@ -272,9 +281,9 @@ class InputFoundation extends BaseFoundation<InputAdapter> {
     }
 
     isAllowClear() {
-        const { value, isFocus, isHovering } = this._adapter.getStates();
+        const { value, isEdit, isHovering } = this._adapter.getStates();
         const { showClear, disabled } = this._adapter.getProps();
-        const allowClear = value && showClear && !disabled && (isFocus || isHovering);
+        const allowClear = value && showClear && !disabled && (isEdit || isHovering);
         return allowClear;
     }
 

+ 6 - 0
packages/semi-foundation/input/input.scss

@@ -173,6 +173,12 @@ $module: #{$prefix}-input;
                 color: $color-input-icon-hover;
             }
         }
+
+        &:focus-visible {
+            border-radius: $radius-input_wrapper;
+            outline: $width-input_icon-outline solid $color-input_icon-outline;
+            outline-offset: $width-input_icon-outlineOffset;
+        }
     }
 
     &__with-suffix-icon.#{$module}-wrapper-clearable:not(.#{$module}-wrapper__with-suffix-hidden) {

+ 12 - 2
packages/semi-foundation/input/textareaFoundation.ts

@@ -8,6 +8,7 @@ import {
 import calculateNodeHeight from './util/calculateNodeHeight';
 import getSizingData from './util/getSizingData';
 import isEnterPress from '../utils/isEnterPress';
+import getSelections from './util/getSelections';
 
 export interface TextAreaDefaultAdapter {
     notifyChange: noopFunction;
@@ -19,6 +20,7 @@ export interface TextAreaDefaultAdapter {
     notifyEnterPress: noopFunction;
     toggleHovering(hovering: boolean): void;
     notifyClear(e: any): void;
+    toggleEditing(editing: boolean): void;
 }
 
 export interface TextAreaAdapter extends Partial<DefaultAdapter>, Partial<TextAreaDefaultAdapter> {
@@ -155,13 +157,20 @@ export default class TextAreaFoundation extends BaseFoundation<TextAreaAdapter>
 
     handleFocus(e: any) {
         const { value } = this.getStates();
+        const selections = getSelections(e);
         this._adapter.toggleFocusing(true);
+        if (selections && selections.selectionStart === selections.selectionEnd) {
+            this._adapter.toggleEditing(true);
+        } else {
+            this._adapter.toggleEditing(false);
+        }
         this._adapter.notifyFocus(value, e);
     }
 
     handleBlur(e: any) {
         const { value } = this.getStates();
         this._adapter.toggleFocusing(false);
+        this._adapter.toggleEditing(false);
         this._adapter.notifyBlur(value, e);
     }
 
@@ -208,9 +217,10 @@ export default class TextAreaFoundation extends BaseFoundation<TextAreaAdapter>
     }
 
     isAllowClear() {
-        const { value, isFocus, isHover } = this._adapter.getStates();
+        const { value, isEdit, isHover } = this._adapter.getStates();
         const { showClear, disabled, readonly } = this._adapter.getProps();
-        const allowClear = value && showClear && !disabled && (isFocus || isHover) && !readonly;
+        const allowClear = value && showClear && !disabled && (isEdit || isHover) && !readonly;
+        console.log('allowClear', allowClear, isEdit, isHover);
         return allowClear;
     }
 

+ 9 - 0
packages/semi-foundation/input/util/getSelections.ts

@@ -0,0 +1,9 @@
+import React from "react";
+
+export default function getSelections(e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) {
+    if (e && e.target) {
+        const { selectionStart, selectionEnd } = e.target;
+        return { selectionStart, selectionEnd };
+    }
+    return null;
+}

+ 3 - 0
packages/semi-foundation/input/variables.scss

@@ -11,6 +11,7 @@ $color-input_default-border-active: $color-input_default-bg-active; // 输入框
 
 $color-input_default-bg-focus: var(--semi-color-fill-0); // 输入框背景颜色 - 选中
 $color-input_default-border-focus: var(--semi-color-focus-border); // 输入框描边颜色 - 选中
+$color-input_icon-outline: var(--semi-color-primary-light-active); // 输入框 icon outline 颜色
 
 // error
 $color-input_danger-bg-default: var(--semi-color-danger-light-default); // 错误输入框背景颜色 - 默认
@@ -75,6 +76,8 @@ $width-input_prepend-border: $border-thickness-control; // 前置标签描边宽
 $width-input_group_pseudo-border: $border-thickness-control;
 $width-input_wrapper-border: $border-thickness-control-focus; // 输入框描边宽度
 $width-input_wrapper_focus-border: $border-thickness-control-focus; // 输入框描边宽度 - 选中态
+$width-input_icon-outline: 2px; // 输入框 icon outline 宽度
+$width-input_icon-outlineOffset: -1px; // 输入框 icon outline-offset 宽度
 
 $radius-input_wrapper: var(--semi-border-radius-small); // 输入框圆角大小
 $spacing-input_icon-marginLeft: -$spacing-base-tight; // 输入框图标左侧内边距

+ 28 - 1
packages/semi-ui/input/_story/input.stories.js

@@ -22,6 +22,7 @@ import {
   Switch,
   Form,
   Space,
+  Radio
 } from '../../index';
 import './input.scss';
 import RTLWrapper from '../../configProvider/_story/RTLDirection/RTLWrapper';
@@ -906,4 +907,30 @@ export const TextAreaAutosize = () => {
     </div>
   )
 };
-TextAreaAutosize.storyName = "textarea autosize";
+TextAreaAutosize.storyName = "textarea autosize";
+
+export const InputA11y = () => {
+  return (
+    <div style={{ width: 300 }}>
+      <Input prefix="search" value="Semi Design" showClear />
+      <br/><br/>
+      <Input prefix="search" value="Semi Design" showClear suffix="semi" />
+      <br/><br/>
+      <Input value="Semi Design" mode="password" showClear />
+      <br/><br/>
+      <Input value="Semi Design" mode="password" showClear suffix="semi" />
+      <br/><br/>
+      <Input value='semi' validateStatus='warning' showClear></Input>
+      <br/><br/>
+      <Input value='semi' validateStatus='error' showClear></Input>
+      <br/><br/>
+      <TextArea defaultValue='semi' showClear />
+      <textarea defaultValue='semi' />
+      <InputGroup label={{ text: '成绩信息' }}>
+          <Input placeholder="Name" style={{ width: 100 }} />
+          <Input placeholder="Score" style={{ width: 140 }} />
+      </InputGroup>
+    </div>
+  );
+}
+InputA11y.storyName = "input a11y";

+ 12 - 4
packages/semi-ui/input/index.tsx

@@ -76,6 +76,7 @@ export interface InputState {
     isHovering: boolean;
     eyeClosed: boolean;
     minLength: number;
+    isEdit: boolean;
 }
 
 class Input extends BaseComponent<InputProps, InputState> {
@@ -161,6 +162,7 @@ class Input extends BaseComponent<InputProps, InputState> {
             isHovering: false,
             eyeClosed: props.mode === 'password',
             minLength: props.minLength,
+            isEdit: false,
         };
         this.inputRef = React.createRef();
         this.prefixRef = React.createRef();
@@ -183,6 +185,12 @@ class Input extends BaseComponent<InputProps, InputState> {
                 this.setState({ isFocus });
             },
             toggleHovering: (isHovering: boolean) => this.setState({ isHovering }),
+            toggleEditing: (isEdit: boolean) => {
+                const { isEdit: stateIsEdit } = this.state;
+                if (isEdit !== stateIsEdit) {
+                    this.setState({ isEdit });
+                }
+            },
             getIfFocusing: () => this.state.isFocus,
             notifyChange: (cbValue: string, e: React.ChangeEvent<HTMLInputElement>) => this.props.onChange(cbValue, e),
             notifyBlur: (val: string, e: React.FocusEvent<HTMLInputElement>) => this.props.onBlur(e),
@@ -313,11 +321,11 @@ class Input extends BaseComponent<InputProps, InputState> {
     }
 
     renderModeBtn() {
-        const { value, isFocus, isHovering, eyeClosed } = this.state;
+        const { value, isEdit, isHovering, eyeClosed } = this.state;
         const { mode, disabled } = this.props;
         const modeCls = cls(`${prefixCls}-modebtn`);
         const modeIcon = eyeClosed ? <IconEyeClosedSolid /> : <IconEyeOpened />;
-        const showModeBtn = mode === 'password' && value && !disabled && (isFocus || isHovering);
+        const showModeBtn = mode === 'password' && value && !disabled && (isEdit || isHovering);
         const ariaLabel = eyeClosed ? 'Show password' : 'Hidden password';
         if (showModeBtn) {
             return (
@@ -356,9 +364,9 @@ class Input extends BaseComponent<InputProps, InputState> {
     }
 
     showClearBtn() {
-        const { value, isFocus, isHovering } = this.state;
+        const { value, isEdit, isHovering } = this.state;
         const { disabled, showClear } = this.props;
-        return Boolean(value) && showClear && !disabled && (isFocus || isHovering);
+        return Boolean(value) && showClear && !disabled && (isEdit || isHovering);
     }
 
     renderSuffix(suffixAllowClear: boolean) {

+ 3 - 1
packages/semi-ui/input/inputGroup.tsx

@@ -66,9 +66,11 @@ export default class inputGroup extends BaseComponent<InputGroupProps, InputGrou
         // const labelCls = cls(label.className, '');
         const defaultName = 'input-group';
         return (
-            <div role="group" aria-label="Input group" aria-disabled={this.props.disabled} className={groupWrapperCls}>
+            <div className={groupWrapperCls}>
                 {label && label.text ? <Label name={defaultName} {...label} /> : null}
                 <span
+                    role="group"
+                    aria-disabled={this.props.disabled}
                     id={label && label.name || defaultName}
                     className={groupCls}
                     style={this.props.style}

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

@@ -59,6 +59,7 @@ export interface TextAreaState {
     height: number;
     minLength: number;
     cachedValue?: string;
+    isEdit: boolean;
 }
 
 class TextArea extends BaseComponent<TextAreaProps, TextAreaState> {
@@ -111,6 +112,7 @@ class TextArea extends BaseComponent<TextAreaProps, TextAreaState> {
             isHover: false,
             height: 0,
             minLength: props.minLength,
+            isEdit: false,
         };
         this.focusing = false;
         this.foundation = new TextAreaFoundation(this.adapter);
@@ -130,6 +132,7 @@ class TextArea extends BaseComponent<TextAreaProps, TextAreaState> {
             getRef: () => this.libRef,
             toggleFocusing: (focusing: boolean) => this.setState({ isFocus: focusing }),
             toggleHovering: (hovering: boolean) => this.setState({ isHover: hovering }),
+            toggleEditing: (editing: boolean) => this.setState({ isEdit: editing }),
             notifyChange: (val: string, e: React.MouseEvent<HTMLTextAreaElement>) => {
                 this.props.onChange(val, e);
             },
@@ -243,12 +246,7 @@ class TextArea extends BaseComponent<TextAreaProps, TextAreaState> {
                 }
             );
             counter = (
-                <div
-                    aria-label="Textarea value length counter"
-                    aria-valuemax={maxCount}
-                    aria-valuenow={current}
-                    className={countCls}
-                >
+                <div className={countCls}>
                     {current}{total ? '/' : null}{total}
                 </div>
             );