Browse Source

feat(a11y): optimize keyboard event to autoComplete (#909)

* feat(a11y): add keyboard event to autoComplete

* feat(a11y): add keyboard event to autoComplete

* feat(a11y): add keyboard event to autoComplete

* feat(a11y): add keyboard event to autoComplete
YannLynn 3 years ago
parent
commit
036bc41d87

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

@@ -391,5 +391,14 @@ import { IllustrationNoContent } from '@douyinfe/semi-illustrations';
 | onSearch | Callback when input changes | Function(value: string) | |
 | onSelect | Callback when the drop-down menu candidate is selected | Function(item: string\|number\|Item) | |
 
+## Accessibility
+### Keyboard and Focus
+- AutoComplete's input box can be focused, and once focused, keyboard users can use `Up Arrow` or `Down Arrow` to open the options panel (if there is a panel)
+- AutoComplete also supports opening and closing panels via `Enter` key
+- If the user sets the defaultActiveFirstOption property to true, the first option is highlighted by default when the options panel is opened
+- If the drop-down menu is open:
+   - Use `Esc` to close the menu
+   - Use `Up Arrow` or `Down Arrow` to toggle options
+   - The focused option can be selected with the `Enter` key and the panel will be collapsed
 ## Design Token
 <DesignToken/>

+ 11 - 0
content/input/autocomplete/index.md

@@ -406,5 +406,16 @@ import { IllustrationNoContent } from '@douyinfe/semi-illustrations';
 | onSearch | 输入变化时的回调 | Function(value: string) | |
 | onSelect | 下拉菜单候选项被选中时的回调 | Function(item: string\|number\|Item) | |
 
+## Accessibility
+### 键盘和焦点
+
+- AutoComplete 的 input 框可被聚焦,聚焦后,键盘用户可以通过 `上箭头` 或 `下箭头` 打开选项面板(如有)
+- AutoComplete 也支持通过 `Enter` 键打开和收起面板
+- 若用户将 defaultActiveFirstOption 属性设置为 true 时,选项面板打开后默认高亮第一个选项
+- 若下拉菜单打开时:
+  - 使用 `Esc` 可以关闭菜单
+  - 使用 `上箭头` 或 `下箭头` 可以切换选项
+  - 被聚焦的选项可以通过 `Enter` 键选中,并收起面板
+
 ## 设计变量
 <DesignToken/>

+ 19 - 3
cypress/integration/autoComplete.spec.js

@@ -3,8 +3,19 @@ describe('autoComplete', () => {
     it('key press', () => {
         cy.visit('http://127.0.0.1:6006/iframe.html?id=autocomplete--basic-usage&args=&viewMode=story');
 
-        // test downArrow and upArrow
+        cy.get('body').tab();
         cy.get('input').type('123');
+        // open panel
+        cy.get('input').type('{enter}');
+        cy.get('.semi-popover');
+
+        // close panel
+        cy.get('input').type('{enter}');
+        cy.get('.semi-popover').not('.exit');
+        cy.get('input').should('have.value', '123');
+    
+        // test downArrow and upArrow
+        cy.get('input').type('{enter}');
         cy.get('input').type('{downArrow}');
         cy.get('input').type('{downArrow}');
         cy.get('input').type('{downArrow}');
@@ -17,11 +28,16 @@ describe('autoComplete', () => {
         cy.get('input').type('{downArrow}');
         cy.get('input').type('{downArrow}');
         cy.get('input').type('{enter}');
-        cy.get('input').should('have.value', '123');
+        cy.get('input').should('have.value', '[email protected]');
+
+        // test upArrow when panel hidden
+        cy.get('input').type('{upArrow}');
+        cy.get('input').type('{upArrow}');
+        cy.get('input').type('{enter}');
+        cy.get('input').should('have.value', '[email protected]');
 
         cy.get('input').trigger('mouseover');
         cy.get('.semi-input-clearbtn').click();
-        cy.wait(100);
         cy.get('#root').click('right');
         cy.get('input').should('have.value', '');
 

+ 29 - 18
packages/semi-foundation/autoComplete/foundation.ts

@@ -78,7 +78,7 @@ class AutoCompleteFoundation<P = Record<string, any>, S = Record<string, any>> e
 
     destroy(): void {
         // this._adapter.unregisterClickOutsideHandler();
-        this.unBindKeyBoardEvent();
+        // this.unBindKeyBoardEvent();
     }
 
     _setDropdownWidth(): void {
@@ -114,7 +114,6 @@ class AutoCompleteFoundation<P = Record<string, any>, S = Record<string, any>> e
         this._setDropdownWidth();
         // this._adapter.registerClickOutsideHandler(e => this.closeDropdown(e));
         this._adapter.notifyDropdownVisibleChange(true);
-        this.bindKeyBoardEvent();
         this._modifyFocusIndexOnPanelOpen();
     }
 
@@ -123,7 +122,8 @@ class AutoCompleteFoundation<P = Record<string, any>, S = Record<string, any>> e
         this._adapter.toggleListVisible(false);
         // this._adapter.unregisterClickOutsideHandler();
         this._adapter.notifyDropdownVisibleChange(false);
-        this.unBindKeyBoardEvent();
+        // After closing the panel, you can still open the panel by pressing the enter key
+        // this.unBindKeyBoardEvent();
     }
 
     // props.data => optionList
@@ -301,19 +301,16 @@ class AutoCompleteFoundation<P = Record<string, any>, S = Record<string, any>> e
         this._adapter.registerKeyDown(this._keydownHandler);
     }
 
-    unBindKeyBoardEvent() {
-        if (this._keydownHandler) {
-            this._adapter.unregisterKeyDown(this._keydownHandler);
-        }
-    }
+    // unBindKeyBoardEvent() {
+    //     if (this._keydownHandler) {
+    //         this._adapter.unregisterKeyDown(this._keydownHandler);
+    //     }
+    // }
 
     _handleKeyDown(event: KeyboardEvent) {
         const key = event.keyCode;
         const { visible } = this.getStates();
 
-        if (!visible) {
-            return;
-        }
         switch (key) {
             case KeyCode.UP:
                 // Prevent Input's cursor from following the movement
@@ -326,6 +323,8 @@ class AutoCompleteFoundation<P = Record<string, any>, S = Record<string, any>> e
                 this._handleArrowKeyDown(1);
                 break;
             case KeyCode.ENTER:
+                // when custom trigger, prevent outer open panel again
+                event.preventDefault();
                 this._handleEnterKeyDown();
                 break;
             case KeyCode.ESC:
@@ -377,17 +376,26 @@ class AutoCompleteFoundation<P = Record<string, any>, S = Record<string, any>> e
     }
 
     _handleArrowKeyDown(offset: number): void {
-        this._getEnableFocusIndex(offset);
+        const { visible } = this.getStates();
+        if (!visible){
+            this.openDropdown();
+        } else {
+            this._getEnableFocusIndex(offset);  
+        }
     }
 
     _handleEnterKeyDown() {
         const { visible, options, focusIndex } = this.getStates();
-        if (focusIndex !== -1 && options.length !== 0) {
-            const visibleOptions = options.filter((item: StateOptionItem) => item.show);
-            const selectedOption = visibleOptions[focusIndex];
-            this.handleSelect(selectedOption, focusIndex);
-        } else if (visible) {
-            // this.close();
+        if (!visible){
+            this.openDropdown();
+        } else {
+            if (focusIndex !== undefined  && focusIndex !== -1 && options.length !== 0) {
+                const visibleOptions = options.filter((item: StateOptionItem) => item.show);
+                const selectedOption = visibleOptions[focusIndex];
+                this.handleSelect(selectedOption, focusIndex);
+            } else {
+                this.closeDropdown();
+            }
         }
     }
 
@@ -396,6 +404,9 @@ class AutoCompleteFoundation<P = Record<string, any>, S = Record<string, any>> e
     }
 
     handleFocus(e: FocusEvent) {
+        // If you get the focus through the tab key, you need to manually bind keyboard events
+        // Then you can open the panel by pressing the enter key
+        this.bindKeyBoardEvent();
         this._adapter.notifyFocus(e);
     }
 

+ 1 - 1
packages/semi-ui/autoComplete/_story/CustomTrigger/index.jsx

@@ -98,7 +98,7 @@ export default class ObjectDemo extends React.Component {
                     renderItem={this.renderItem}
                     renderSelectedItem={this.renderSelectedItem}
                     onSelect={this.handleSelect}
-                    triggerRender={({ value, inputValue }) => <Button>{inputValue}</Button>}
+                    triggerRender={({ value, inputValue, onFocus }) => <Button onFocus={onFocus}>{inputValue}</Button>}
                 />
             </div>
         );