Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit b5b286d

Browse files
aojunhao123小豪
and
小豪
authored
feat: improve keyboard operation accessibility (#285)
* feat: improve keyboard accessibility * feat: do not add focus style when clicking * chore: remove some logic * test: add keyboard operation test case * fix: lint fix * style: adjust focus style * refactor: optimize keyboard navigation logic with modular arithmetic * chore: adjust code style * fix: click item should not have foucs style * test: add test case --------- Co-authored-by: 小豪 <aojunhao@cai-inc.com>
1 parent f29b18c commit b5b286d

File tree

6 files changed

+219
-96
lines changed

6 files changed

+219
-96
lines changed

‎assets/index.less‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,14 @@
117117
direction: rtl;
118118
}
119119
}
120+
121+
.rc-segmented-item {
122+
&:focus {
123+
outline: none;
124+
}
125+
126+
&-focused {
127+
border-radius: 2px;
128+
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
129+
}
130+
}

‎docs/demo/basic.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ export default function App() {
88
<div className="wrapper">
99
<Segmented
1010
options={['iOS', 'Android', 'Web']}
11+
defaultValue="Android"
12+
name="segmented1"
1113
onChange={(value) => console.log(value, typeof value)}
1214
/>
1315
</div>
1416
<div className="wrapper">
1517
<Segmented
1618
vertical
1719
options={['iOS', 'Android', 'Web']}
20+
name="segmented2"
1821
onChange={(value) => console.log(value, typeof value)}
1922
/>
2023
</div>

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"@rc-component/father-plugin": "^1.0.1",
5656
"@testing-library/jest-dom": "^5.16.5",
5757
"@testing-library/react": "^14.2.1",
58+
"@testing-library/user-event": "^14.5.2",
5859
"@types/classnames": "^2.2.9",
5960
"@types/jest": "^29.2.4",
6061
"@types/react": "^18.3.11",

‎src/index.tsx‎

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ const InternalSegmentedOption: React.FC<{
8484
e: React.ChangeEvent<HTMLInputElement>,
8585
value: SegmentedRawOption,
8686
) => void;
87+
onFocus: (e: React.FocusEvent<HTMLInputElement>) => void;
88+
onBlur: (e?: React.FocusEvent<HTMLInputElement>) => void;
89+
onKeyDown: (e: React.KeyboardEvent) => void;
90+
onKeyUp: (e: React.KeyboardEvent) => void;
91+
onMouseDown: () => void;
8792
}> = ({
8893
prefixCls,
8994
className,
@@ -94,6 +99,11 @@ const InternalSegmentedOption: React.FC<{
9499
value,
95100
name,
96101
onChange,
102+
onFocus,
103+
onBlur,
104+
onKeyDown,
105+
onKeyUp,
106+
onMouseDown,
97107
}) => {
98108
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
99109
if (disabled) {
@@ -107,20 +117,23 @@ const InternalSegmentedOption: React.FC<{
107117
className={classNames(className, {
108118
[`${prefixCls}-item-disabled`]: disabled,
109119
})}
120+
onMouseDown={onMouseDown}
110121
>
111122
<input
112123
name={name}
113124
className={`${prefixCls}-item-input`}
114-
aria-hidden="true"
115125
type="radio"
116126
disabled={disabled}
117127
checked={checked}
118128
onChange={handleChange}
129+
onFocus={onFocus}
130+
onBlur={onBlur}
131+
onKeyDown={onKeyDown}
132+
onKeyUp={onKeyUp}
119133
/>
120134
<div
121135
className={`${prefixCls}-item-label`}
122136
title={title}
123-
role="option"
124137
aria-selected={checked}
125138
>
126139
{label}
@@ -176,10 +189,63 @@ const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
176189

177190
const divProps = omit(restProps, ['children']);
178191

192+
// ======================= Focus ========================
193+
const [isKeyboard, setIsKeyboard] = React.useState(false);
194+
const [isFocused, setIsFocused] = React.useState(false);
195+
196+
const handleFocus = () => {
197+
setIsFocused(true);
198+
};
199+
200+
const handleBlur = () => {
201+
setIsFocused(false);
202+
};
203+
204+
const handleMouseDown = () => {
205+
setIsKeyboard(false);
206+
};
207+
208+
// capture keyboard tab interaction for correct focus style
209+
const handleKeyUp = (event: React.KeyboardEvent) => {
210+
if (event.key === 'Tab') {
211+
setIsKeyboard(true);
212+
}
213+
};
214+
215+
// ======================= Keyboard ========================
216+
const onOffset = (offset: number) => {
217+
const currentIndex = segmentedOptions.findIndex(
218+
(option) => option.value === rawValue,
219+
);
220+
221+
const total = segmentedOptions.length;
222+
const nextIndex = (currentIndex + offset + total) % total;
223+
224+
const nextOption = segmentedOptions[nextIndex];
225+
if (nextOption) {
226+
setRawValue(nextOption.value);
227+
onChange?.(nextOption.value);
228+
}
229+
};
230+
231+
const handleKeyDown = (event: React.KeyboardEvent) => {
232+
switch (event.key) {
233+
case 'ArrowLeft':
234+
case 'ArrowUp':
235+
onOffset(-1);
236+
break;
237+
case 'ArrowRight':
238+
case 'ArrowDown':
239+
onOffset(1);
240+
break;
241+
}
242+
};
243+
179244
return (
180245
<div
181-
role="listbox"
246+
role="radiogroup"
182247
aria-label="segmented control"
248+
tabIndex={disabled ? undefined : 0}
183249
{...divProps}
184250
className={classNames(
185251
prefixCls,
@@ -222,10 +288,19 @@ const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
222288
{
223289
[`${prefixCls}-item-selected`]:
224290
segmentedOption.value === rawValue && !thumbShow,
291+
[`${prefixCls}-item-focused`]:
292+
isFocused &&
293+
isKeyboard &&
294+
segmentedOption.value === rawValue,
225295
},
226296
)}
227297
checked={segmentedOption.value === rawValue}
228298
onChange={handleChange}
299+
onFocus={handleFocus}
300+
onBlur={handleBlur}
301+
onKeyDown={handleKeyDown}
302+
onKeyUp={handleKeyUp}
303+
onMouseDown={handleMouseDown}
229304
disabled={!!disabled || !!segmentedOption.disabled}
230305
/>
231306
))}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /