Skip to content

Commit c51d83c

Browse files
authored
refactor: enhance tab navigation and accessibility features (#763)
* refactor: enhance tab navigation and accessibility features * chore: adjust some logic * chore: code clean * fix: enhance accessibility by adding role="tab" to outer div * test: add test case for keyboard operation * fix: fix some test case * chore: adjust field name * refactor: remove unnecessary global event listener * chore: code fix * feat: enhance tab navigation with Backspace support * feat: improve delete keyboard operation * chore: adjust some logic * feat: improve delete logic * chore: adjust some logic * fix: add delete logic * test: add test case * chore: add a11y attr * test: update snapshot * feat: add a11y support for screen reader * chore: adjust some logic * chore: adjust field names
1 parent 0538f10 commit c51d83c

File tree

8 files changed

+491
-46
lines changed

8 files changed

+491
-46
lines changed

assets/index.less

+26-20
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,32 @@
1010
@effect-duration: 0.3s;
1111

1212
.@{tabs-prefix-cls} {
13-
border: 1px solid gray;
14-
font-size: 14px;
1513
overflow: hidden;
14+
font-size: 14px;
15+
border: 1px solid gray;
1616

1717
// ========================== Navigation ==========================
1818
&-nav {
19+
position: relative;
1920
display: flex;
2021
flex: none;
21-
position: relative;
2222

2323
&-measure,
2424
&-wrap {
25-
transform: translate(0);
2625
position: relative;
2726
display: inline-block;
27+
display: flex;
2828
flex: auto;
29-
white-space: nowrap;
3029
overflow: hidden;
31-
display: flex;
30+
white-space: nowrap;
31+
transform: translate(0);
3232

3333
&-ping-left::before,
3434
&-ping-right::after {
35-
content: '';
3635
position: absolute;
3736
top: 0;
3837
bottom: 0;
38+
content: '';
3939
}
4040
&-ping-left::before {
4141
left: 0;
@@ -48,10 +48,10 @@
4848

4949
&-ping-top::before,
5050
&-ping-bottom::after {
51-
content: '';
5251
position: absolute;
53-
left: 0;
5452
right: 0;
53+
left: 0;
54+
content: '';
5555
}
5656
&-ping-top::before {
5757
top: 0;
@@ -64,8 +64,8 @@
6464
}
6565

6666
&-list {
67-
display: flex;
6867
position: relative;
68+
display: flex;
6969
transition: transform 0.3s;
7070
}
7171

@@ -81,36 +81,39 @@
8181
}
8282

8383
&-more {
84-
border: 1px solid blue;
8584
background: rgba(255, 0, 0, 0.1);
85+
border: 1px solid blue;
8686
}
8787
&-add {
88-
border: 1px solid green;
8988
background: rgba(0, 255, 0, 0.1);
89+
border: 1px solid green;
9090
}
9191
}
9292

9393
&-tab {
94-
border: 0;
94+
position: relative;
95+
display: flex;
96+
align-items: center;
97+
margin: 0;
98+
font-weight: lighter;
9599
font-size: 20px;
96100
background: rgba(255, 255, 255, 0.5);
97-
margin: 0;
98-
display: flex;
101+
border: 0;
99102
outline: none;
100103
cursor: pointer;
101-
position: relative;
102-
font-weight: lighter;
103-
align-items: center;
104104

105105
&-btn,
106106
&-remove {
107-
border: 0;
108107
background: transparent;
108+
border: 0;
109109
}
110110

111111
&-btn {
112112
font-weight: inherit;
113113
line-height: 32px;
114+
&:focus {
115+
outline: none;
116+
}
114117
}
115118

116119
&-remove {
@@ -120,9 +123,12 @@
120123
}
121124

122125
&-active {
123-
// padding-left: 30px;
124126
font-weight: bolder;
125127
}
128+
129+
&-focus {
130+
outline: 1px auto #1677ff;
131+
}
126132
}
127133

128134
&-ink-bar {

docs/examples/basic.tsx

+25-1
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,31 @@ export default () => {
2424
disabled: true,
2525
icon: <span>🐼</span>,
2626
},
27+
{
28+
label: 'Yo',
29+
key: 'yo',
30+
children: 'Yo!',
31+
icon: <span>👋</span>,
32+
},
2733
]);
34+
const [direction, setDirection] = React.useState<'ltr' | 'rtl'>('ltr');
2835

2936
if (destroy) {
3037
return null;
3138
}
3239

40+
const onTabClick = (key: string) => {
41+
console.log('key', key);
42+
};
43+
3344
return (
3445
<React.StrictMode>
35-
<Tabs tabBarExtraContent="extra" items={items} />
46+
<Tabs
47+
tabBarExtraContent="extra"
48+
onTabClick={onTabClick}
49+
direction={direction}
50+
items={items}
51+
/>
3652
<button
3753
type="button"
3854
onClick={() => {
@@ -56,6 +72,14 @@ export default () => {
5672
>
5773
Destroy
5874
</button>
75+
<button
76+
type="button"
77+
onClick={() => {
78+
setDirection(direction === 'ltr' ? 'rtl' : 'ltr');
79+
}}
80+
>
81+
{direction === 'ltr' ? 'rtl' : 'ltr'}
82+
</button>
5983
</React.StrictMode>
6084
);
6185
};

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"@rc-component/trigger": "^2.0.0",
5151
"@testing-library/jest-dom": "^6.1.4",
5252
"@testing-library/react": "^16.0.1",
53+
"@testing-library/user-event": "^14.5.2",
5354
"@types/classnames": "^2.2.10",
5455
"@types/enzyme": "^3.10.5",
5556
"@types/jest": "^29.4.0",

src/TabNavList/TabNode.tsx

+38-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import classNames from 'classnames';
2-
import KeyCode from 'rc-util/lib/KeyCode';
32
import * as React from 'react';
43
import type { EditableConfig, Tab } from '../interface';
54
import { genDataNodeKey, getRemovable } from '../util';
@@ -9,14 +8,21 @@ export interface TabNodeProps {
98
prefixCls: string;
109
tab: Tab;
1110
active: boolean;
11+
focus: boolean;
1212
closable?: boolean;
1313
editable?: EditableConfig;
1414
onClick?: (e: React.MouseEvent | React.KeyboardEvent) => void;
1515
onResize?: (width: number, height: number, left: number, top: number) => void;
1616
renderWrapper?: (node: React.ReactElement) => React.ReactElement;
1717
removeAriaLabel?: string;
18+
tabCount: number;
19+
currentPosition: number;
1820
removeIcon?: React.ReactNode;
21+
onKeyDown: React.KeyboardEventHandler;
22+
onMouseDown: React.MouseEventHandler;
23+
onMouseUp: React.MouseEventHandler;
1924
onFocus: React.FocusEventHandler;
25+
onBlur: React.FocusEventHandler;
2026
style?: React.CSSProperties;
2127
}
2228

@@ -25,14 +31,21 @@ const TabNode: React.FC<TabNodeProps> = props => {
2531
prefixCls,
2632
id,
2733
active,
34+
focus,
2835
tab: { key, label, disabled, closeIcon, icon },
2936
closable,
3037
renderWrapper,
3138
removeAriaLabel,
3239
editable,
3340
onClick,
3441
onFocus,
42+
onBlur,
43+
onKeyDown,
44+
onMouseDown,
45+
onMouseUp,
3546
style,
47+
tabCount,
48+
currentPosition,
3649
} = props;
3750
const tabPrefix = `${prefixCls}-tab`;
3851

@@ -56,40 +69,55 @@ const TabNode: React.FC<TabNodeProps> = props => {
5669
[label, icon],
5770
);
5871

72+
const btnRef = React.useRef<HTMLDivElement>(null);
73+
74+
React.useEffect(() => {
75+
if (focus && btnRef.current) {
76+
btnRef.current.focus();
77+
}
78+
}, [focus]);
79+
5980
const node: React.ReactElement = (
6081
<div
6182
key={key}
62-
// ref={ref}
6383
data-node-key={genDataNodeKey(key)}
6484
className={classNames(tabPrefix, {
6585
[`${tabPrefix}-with-remove`]: removable,
6686
[`${tabPrefix}-active`]: active,
6787
[`${tabPrefix}-disabled`]: disabled,
88+
[`${tabPrefix}-focus`]: focus,
6889
})}
6990
style={style}
7091
onClick={onInternalClick}
7192
>
7293
{/* Primary Tab Button */}
7394
<div
95+
ref={btnRef}
7496
role="tab"
7597
aria-selected={active}
7698
id={id && `${id}-tab-${key}`}
7799
className={`${tabPrefix}-btn`}
78100
aria-controls={id && `${id}-panel-${key}`}
79101
aria-disabled={disabled}
80-
tabIndex={disabled ? null : 0}
102+
tabIndex={disabled ? null : active ? 0 : -1}
81103
onClick={e => {
82104
e.stopPropagation();
83105
onInternalClick(e);
84106
}}
85-
onKeyDown={e => {
86-
if ([KeyCode.SPACE, KeyCode.ENTER].includes(e.which)) {
87-
e.preventDefault();
88-
onInternalClick(e);
89-
}
90-
}}
107+
onKeyDown={onKeyDown}
108+
onMouseDown={onMouseDown}
109+
onMouseUp={onMouseUp}
91110
onFocus={onFocus}
111+
onBlur={onBlur}
92112
>
113+
{focus && (
114+
<div
115+
aria-live="polite"
116+
style={{ width: 0, height: 0, position: 'absolute', overflow: 'hidden', opacity: 0 }}
117+
>
118+
{`Tab ${currentPosition} of ${tabCount}`}
119+
</div>
120+
)}
93121
{icon && <span className={`${tabPrefix}-icon`}>{icon}</span>}
94122
{label && labelNode}
95123
</div>
@@ -99,7 +127,7 @@ const TabNode: React.FC<TabNodeProps> = props => {
99127
<button
100128
type="button"
101129
aria-label={removeAriaLabel || 'remove'}
102-
tabIndex={0}
130+
tabIndex={active ? 0 : -1}
103131
className={`${tabPrefix}-remove`}
104132
onClick={e => {
105133
e.stopPropagation();

0 commit comments

Comments
 (0)