1
0
Эх сурвалжийг харах

feat(a11y): tab a11y focus

linyan 3 жил өмнө
parent
commit
4af109921b

+ 63 - 0
packages/semi-foundation/tabs/foundation.ts

@@ -74,6 +74,69 @@ class TabsFoundation<P = Record<string, any>, S = Record<string, any>> extends B
         this._adapter.notifyTabDelete(tabKey);
     }
 
+    handleKeyDown = (event: any, index: number, closable: boolean) => {
+        switch (event.key) {
+            case "ArrowLeft":
+            case "ArrowRight":
+            case "ArrowUp":
+            case "ArrowDown":
+                this.determineOrientation(event, index);
+                break;
+            case "Delete":
+                this.determineDeletable(event, closable);
+                break;
+            case "Enter":
+                this.handleTabClick(event.target.id.split('semiTab')[1], event);
+                break;
+        }
+    }
+
+    determineOrientation(event: any, index: number): void {
+        const { tabPosition } = this.getProps();
+        const isVertical = tabPosition === 'left';
+
+        if (isVertical) {
+            if (event.key ===  "ArrowUp" || event.key ===  "ArrowDown") {
+                event.preventDefault();
+                this.switchTabOnArrowPress(event, index);
+            }
+        } else {
+            if (event.key ===  "ArrowLeft" || event.key === "ArrowRight") {
+                this.switchTabOnArrowPress(event, index);
+            }
+        }
+    }
+
+    determineDeletable(event: any, closable: boolean): void {
+        if (closable) {
+            this.handleTabDelete(event.target.id.split('semiTab')[1]);
+        }
+    }
+
+    switchTabOnArrowPress(event: any, index: number): void {
+        // get all sibling nodes
+        const tabs = event.target.parentNode.childNodes;
+
+        const direction = {
+            37: -1,
+            38: -1,
+            39: 1,
+            40: 1,
+        };
+
+        if (direction[event.keyCode]) {
+            if (index !== undefined) {
+                if (tabs[index + direction[event.keyCode]]) {
+                    tabs[index+ direction[event.keyCode]].focus();
+                } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
+                    tabs[tabs.length - 1].focus(); // focus last tab
+                } else if (event.key ===  "ArrowRight" || event.key == "ArrowDown") {
+                    tabs[0].focus(); // focus first tab
+                }
+            }
+        }
+    }
+
 }
 
 export default TabsFoundation;

+ 0 - 85
packages/semi-foundation/tabs/itemFoundation.ts

@@ -1,85 +0,0 @@
-import BaseFoundation, { DefaultAdapter } from '../base/foundation';
-import { get } from 'lodash';
-import keyCode, { ENTER_KEY } from './../utils/keyCode';
-
-export interface TabsItemAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
-    notifyClick: (item: any, e: any) => void;
-}
-
-export default class TabsItemFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<TabsItemAdapter<P, S>, P, S> {
-
-    constructor(adapter: TabsItemAdapter<P, S>) {
-        super({ ...adapter });
-    }
-
-    // handleNewActiveKey(activeKey: string): void {
-    //     const { activeKey: stateActiveKey } = this.getStates();
-    //     if (stateActiveKey !== activeKey) {
-    //         this._adapter.setNewActiveKey(activeKey);
-    //     }
-    // }
-
-    handleKeyDown(event: any): void {
-        console.log("key", event);
-        const key = get(event, 'key');
-        console.log("key", key, key === keyCode.LEFT);
-
-        switch (key) {
-            case "ArrowLeft":
-            case 'ArrowRight':
-                this.determineOrientation(event);
-                break;
-            // case keys.delete:
-            //     determineDeletable(event);
-            //     break;
-            case ENTER_KEY:
-            // case keys.space:
-                // activateTab(event.target);
-                // break;
-        }
-    }
-
-    determineOrientation(event: any): void {
-        const { tabPosition } = this.getProps();
-        const key = get(event, 'key');
-        const isVertical = tabPosition === 'left';
-        console.log("key", key);
-
-        if (isVertical) {
-            if (key === keyCode.UP || key === keyCode.DOWN) {
-                event.preventDefault();
-                this.switchTabOnArrowPress(event);
-            }
-        } else {
-            if (key === keyCode.LEFT || key === keyCode.RIGHT) {
-                this.switchTabOnArrowPress(event);
-            }
-        }
-    }
-
-    switchTabOnArrowPress(event: any): void {
-        const key = get(event, 'key');
-        const tabs = document.querySelectorAll('[role="tab"]');
-
-        const direction = {
-            37: -1,
-            38: -1,
-            39: 1,
-            40: 1,
-        };
-
-        if (direction[key]) {
-            const target = event.target;
-            if (target.index !== undefined) {
-                if (tabs[target.index + direction[key]]) {
-                    // tabs[target.index + direction[key]].focus();
-                } else if (key === keyCode.LEFT || key === keyCode.UP) {
-                    // focusLastTab();
-                } else if (key === keyCode.RIGHT || key == keyCode.DOWN) {
-                    // focusFirstTab();
-                }
-            }
-        }
-    }
-
-}

+ 28 - 0
packages/semi-foundation/tabs/tabs.scss

@@ -164,6 +164,10 @@ $module: #{$prefix}-tabs;
                 &:hover {
                     border-bottom: $width-tabs_bar_line_tab-border solid $color-tabs_tab_line_default-border-hover;
                 }
+
+                &:focus-visible {
+                    outline: $width-tabs-outline solid $color-tabs_tab-outline-focus;
+                }
                 
                 &:active {
                     border-bottom: $width-tabs_bar_line_tab-border solid $color-tabs_tab_line_default-border-active;
@@ -202,6 +206,10 @@ $module: #{$prefix}-tabs;
                     border-left: $width-tabs_bar_line_tab-border solid $color-tabs_tab_line_default-border-hover;
                     background-color: $color-tabs_tab_line_vertical-bg-hover;
                 }
+
+                &:focus-visible {
+                    outline: $width-tabs-outline solid $color-tabs_tab-outline-focus;
+                }
                 
                 &:active {
                     border-left: $width-tabs_bar_line_tab-border solid $color-tabs_tab_line_default-border-active;
@@ -263,6 +271,10 @@ $module: #{$prefix}-tabs;
                     border-bottom: none;
                 }
                 
+                &:focus-visible {
+                    outline: $width-tabs-outline solid $color-tabs_tab-outline-focus;
+                }
+                
                 &:not(:last-of-type) {
                     margin-right: $spacing-tabs_bar_card_tab-marginRight;
                 }
@@ -292,6 +304,10 @@ $module: #{$prefix}-tabs;
                 &:hover {
                     border-right: none;
                 }
+
+                &:focus-visible {
+                    outline: $width-tabs-outline solid $color-tabs_tab-outline-focus;
+                }
                 
                 &:not(:last-of-type) {
                     margin-bottom: $spacing-tabs_bar_card_tab_left-marginBottom;
@@ -328,6 +344,10 @@ $module: #{$prefix}-tabs;
                 background: $color-tabs_tab_card-bg-hover;
             }
             
+            &:focus-visible {
+                outline: $width-tabs-outline solid $color-tabs_tab-outline-focus;
+            }
+
             &:active {
                 background: $color-tabs_tab_card-bg-active;
             }
@@ -365,6 +385,10 @@ $module: #{$prefix}-tabs;
                 border: none;
                 background-color: $color-tabs_tab_button-bg-hover;
             }
+
+            &:focus-visible {
+                outline: $width-tabs-outline solid $color-tabs_tab-outline-focus;
+            }
             
             &:active {
                 background-color: $color-tabs_tab_button-bg-active;
@@ -400,6 +424,10 @@ $module: #{$prefix}-tabs;
         // position: absolute;
         // flex-shrink: 0;
         // position: absolute;
+
+        &:focus-visible {
+            outline: $width-tabs-outline solid $color-tabs_tab-outline-focus;
+        }
     }
     
     &-pane-inactive,

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

@@ -1,4 +1,5 @@
 $color-tabs_tab_line_default-border-default: var(--semi-color-border); // 线条式页签描边颜色默认
+$color-tabs_tab-outline-focus: var(--semi-color-primary-light-active); // 页签轮廓 - 聚焦
 
 $color-tabs_tab_line_default-bg-default: transparent; // 线条式页签背景颜色 - 默认
 $color-tabs_tab_line_default-text-default: var(--semi-color-text-2); // 线条式页签文本颜色 - 默认
@@ -54,6 +55,8 @@ $width-tabs_bar_line-border: $border-thickness-control; // 线条式页签底部
 $width-tabs_bar_line_tab-border: 2px; // 页签标示线宽度
 
 $width-tabs_bar_card-border: $border-thickness-control; // 卡片式页签底部分割线宽度
+$width-tabs-outline: 2px; // 页签轮廓宽度
+
 
 $height-tabs_bar_extra_large: 50px; // 大尺寸页签高度
 $font-tabs_bar_extra_large-lineHeight: $height-tabs_bar_extra_large; // 大尺寸页签文字行高

+ 6 - 20
packages/semi-ui/tabs/TabBar.tsx

@@ -10,7 +10,6 @@ import { TabBarProps, PlainTab } from './interface';
 import { isEmpty } from 'lodash';
 import { IconChevronRight, IconChevronLeft, IconClose } from '@douyinfe/semi-icons';
 import { getUuidv4 } from '@douyinfe/semi-foundation/utils/uuid';
-import TabsItemFoundation, { TabsItemAdapter } from '@douyinfe/semi-foundation/tabs/itemFoundation';
 import BaseComponent from '../_base/baseComponent';
 
 
@@ -42,17 +41,6 @@ class TabBar extends BaseComponent<TabBarProps, TabBarState> {
     };
 
     uuid: string;
-    foundation: TabsItemFoundation;
-    // foundation: TabsFoundation;
-    
-    get adapter(): TabsItemAdapter<TabBarProps, TabBarState>  {
-        return {
-            ...super.adapter,
-            notifyClick: (activeKey: string, event: MouseEvent<HTMLDivElement>): void => {
-                this.props.onTabClick(activeKey, event);
-            },
-        };
-    }
 
     constructor(props: TabBarProps) {
         super(props);
@@ -61,7 +49,6 @@ class TabBar extends BaseComponent<TabBarProps, TabBarState> {
             rePosKey: 0,
             startInd: 0,
         };
-        this.foundation = new TabsItemFoundation(this.adapter);
         this.uuid = getUuidv4();
     }
 
@@ -103,11 +90,11 @@ class TabBar extends BaseComponent<TabBarProps, TabBarState> {
         }
     };
 
-    handleKeyDown = (event: React.KeyboardEvent) => {
-        this.foundation.handleKeyDown(event);
+    handleKeyDown = (event: React.KeyboardEvent, index: number, closable: boolean) => {
+        this.props.handleKeyDown(event, index, closable);
     }
 
-    renderTabItem = (panel: PlainTab): ReactNode => {
+    renderTabItem = (panel: PlainTab, index: number): ReactNode => {
         const { size, type, deleteTabItem } = this.props;
         const panelIcon = panel.icon ? this.renderIcon(panel.icon) : null;
         const closableIcon = (type === 'card' && panel.closable) ? <IconClose aria-label="Close" role="button" className={`${cssClasses.TABS_TAB}-icon-close`} onClick={(e: React.MouseEvent<HTMLSpanElement>) => deleteTabItem(panel.itemKey, e)} /> : null;
@@ -132,9 +119,8 @@ class TabBar extends BaseComponent<TabBarProps, TabBarState> {
                 aria-controls={`semiTabPanel${key}`}
                 aria-disabled={panel.disabled ? 'true' : 'false'}
                 aria-selected={isSelected ? 'true' : 'false'}
-                // tabIndex={isSelected ? 0 : -1}
-                tabIndex={0}
-                onKeyDown={this.handleKeyDown}
+                tabIndex={isSelected ? 0 : -1}
+                onKeyDown={e => this.handleKeyDown(e, index, panel.closable)}
                 {...events}
                 className={className}
                 key={this._getItemKey(key)}
@@ -146,7 +132,7 @@ class TabBar extends BaseComponent<TabBarProps, TabBarState> {
         );
     };
 
-    renderTabComponents = (list: Array<PlainTab>): Array<ReactNode> => list.map(panel => this.renderTabItem(panel));
+    renderTabComponents = (list: Array<PlainTab>): Array<ReactNode> => list.map((panel, index) => this.renderTabItem(panel, index));
 
     handleArrowClick = (items: Array<OverflowItem>, pos: 'start' | 'end'): void => {
         const inline = pos === 'start' ? 'end' : 'start';

+ 34 - 0
packages/semi-ui/tabs/_story/tabs.stories.js

@@ -867,3 +867,37 @@ export const TabListChange = () => <TabListChangeDemo />;
 TabListChange.story = {
   name: 'tablist change',
 };
+
+class CloseableDemo extends React.Component {
+    constructor(props){
+        super(props);
+        this.state = {
+            tabList: [
+                {tab: '文档', itemKey:'1', text:'文档', closable:true},
+                {tab: '快速起步', itemKey:'2', text:'快速起步', closable:true},
+                {tab: '帮助', itemKey:'3', text:'帮助'},
+            ]
+        }
+    }
+    close(key){
+        const newTabList = [...this.state.tabList];
+        const closeIndex = newTabList.findIndex(t=>t.itemKey===key);
+        newTabList.splice(closeIndex, 1);
+        this.setState({tabList:newTabList});
+    }
+    render() {
+        return (
+            <Tabs type="card" defaultActiveKey="1" onTabClose={this.close.bind(this)}>
+                {
+                    this.state.tabList.map(t=><TabPane closable={t.closable} tab={t.tab} itemKey={t.itemKey} key={t.itemKey}>{t.text}</TabPane>)
+                }
+            </Tabs>
+        );
+    }
+}
+
+export const Close = () => <CloseableDemo />;
+
+Close.story = {
+  name: 'close',
+};

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

@@ -225,6 +225,10 @@ class Tabs extends BaseComponent<TabsProps, TabsState> {
         this.foundation.handleTabDelete(tabKey);
     }
 
+    handleKeyDown = (event: React.KeyboardEvent, index: number, closable: boolean) => {
+        this.foundation.handleKeyDown(event, index, closable);
+    }
+
     render(): ReactNode {
         const {
             children,
@@ -266,7 +270,8 @@ class Tabs extends BaseComponent<TabsProps, TabsState> {
             tabBarExtraContent,
             tabPosition,
             type,
-            deleteTabItem: this.deleteTabItem
+            deleteTabItem: this.deleteTabItem,
+            handleKeyDown: (event, index, closable) => this.handleKeyDown(event, index, closable)
         } as TabBarProps;
 
         const tabBar = renderTabBar ? renderTabBar(tabBarProps, TabBar) : <TabBar {...tabBarProps} />;

+ 1 - 0
packages/semi-ui/tabs/interface.ts

@@ -53,6 +53,7 @@ export interface TabBarProps {
     dropdownStyle?: CSSProperties;
     closable?: boolean;
     deleteTabItem?: (tabKey: string, event: MouseEvent<Element>) => void;
+    handleKeyDown?: (event: React.KeyboardEvent, index: number, closable: boolean) => void
 }
 
 export interface TabPaneProps {