From b727013abcb6eb54e614bf14d0d98c0fc41ae806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=B1=AA?= Date: Sat, 23 Nov 2024 21:37:23 +0800 Subject: [PATCH 01/10] feat: improve keyboard accessibility --- assets/index.less | 10 ++++ src/index.tsx | 74 ++++++++++++++++++++++++- tests/__snapshots__/index.test.tsx.snap | 53 +++++------------- 3 files changed, 96 insertions(+), 41 deletions(-) diff --git a/assets/index.less b/assets/index.less index e3589cf..2405d79 100644 --- a/assets/index.less +++ b/assets/index.less @@ -117,3 +117,13 @@ direction: rtl; } } + +.rc-segmented-item { + &:focus { + outline: none; + } + + &-focused { + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + } +} diff --git a/src/index.tsx b/src/index.tsx index b251c27..d8c2401 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,6 @@ import classNames from 'classnames'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; +import KeyCode from 'rc-util/lib/KeyCode'; import omit from 'rc-util/lib/omit'; import { composeRef } from 'rc-util/lib/ref'; import * as React from 'react'; @@ -84,6 +85,9 @@ const InternalSegmentedOption: React.FC<{ e: React.ChangeEvent, value: SegmentedRawOption, ) => void; + onFocus: () => void; + onBlur: () => void; + onKeyDown: (e: React.KeyboardEvent) => void; }> = ({ prefixCls, className, @@ -94,6 +98,9 @@ const InternalSegmentedOption: React.FC<{ value, name, onChange, + onFocus, + onBlur, + onKeyDown, }) => { const handleChange = (event: React.ChangeEvent) => { if (disabled) { @@ -111,11 +118,13 @@ const InternalSegmentedOption: React.FC<{
( const divProps = omit(restProps, ['children']); + // ======================= Focus ======================== + const [isFocused, setIsFocused] = React.useState(false); + + const currentIndex = segmentedOptions.findIndex( + (option) => option.value === rawValue, + ); + + const handleFocus = () => { + setIsFocused(true); + }; + + const handleBlur = () => { + setIsFocused(false); + }; + + // ======================= Keyboard ======================== + const getNextIndex = (current: number, offset: number) => { + if (disabled) { + return current; + } + + const total = segmentedOptions.length; + const nextIndex = (current + offset + total) % total; + + if (segmentedOptions[nextIndex]?.disabled) { + return getNextIndex(nextIndex, offset); + } + return nextIndex; + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + let offset = 0; + + switch (event.which) { + case KeyCode.LEFT: + case KeyCode.UP: + event.preventDefault(); + offset = -1; + break; + case KeyCode.RIGHT: + case KeyCode.DOWN: + event.preventDefault(); + offset = 1; + break; + default: + return; + } + + const nextIndex = getNextIndex(currentIndex, offset); + const nextOption = segmentedOptions[nextIndex]; + + if (nextOption) { + setRawValue(nextOption.value); + onChange?.(nextOption.value); + } + }; + return (
( { [`${prefixCls}-item-selected`]: segmentedOption.value === rawValue && !thumbShow, + [`${prefixCls}-item-focused`]: + isFocused && segmentedOption.value === rawValue, }, )} checked={segmentedOption.value === rawValue} onChange={handleChange} + onFocus={handleFocus} + onBlur={handleBlur} + onKeyDown={handleKeyDown} disabled={!!disabled || !!segmentedOption.disabled} /> ))} diff --git a/tests/__snapshots__/index.test.tsx.snap b/tests/__snapshots__/index.test.tsx.snap index 87e05e3..9865593 100644 --- a/tests/__snapshots__/index.test.tsx.snap +++ b/tests/__snapshots__/index.test.tsx.snap @@ -5,6 +5,7 @@ exports[`rc-segmented render empty segmented 1`] = ` aria-label="segmented control" class="rc-segmented" role="listbox" + tabindex="0" >
@@ -63,7 +63,6 @@ exports[`rc-segmented render label with ReactNode 1`] = ` class="rc-segmented-item" > @@ -86,6 +85,7 @@ exports[`rc-segmented render segmented ok 1`] = ` aria-label="segmented control" class="rc-segmented" role="listbox" + tabindex="0" >
@@ -129,7 +127,6 @@ exports[`rc-segmented render segmented ok 1`] = ` class="rc-segmented-item" > @@ -151,6 +148,7 @@ exports[`rc-segmented render segmented with CSSMotion basic 1`] = ` aria-label="segmented control" class="rc-segmented" role="listbox" + tabindex="0" >
@@ -194,7 +190,6 @@ exports[`rc-segmented render segmented with CSSMotion basic 1`] = ` class="rc-segmented-item" > @@ -216,6 +211,7 @@ exports[`rc-segmented render segmented with options 1`] = ` aria-label="segmented control" class="rc-segmented" role="listbox" + tabindex="0" >
@@ -259,7 +253,6 @@ exports[`rc-segmented render segmented with options 1`] = ` class="rc-segmented-item" > @@ -281,6 +274,7 @@ exports[`rc-segmented render segmented with options null/undefined 1`] = ` aria-label="segmented control" class="rc-segmented rc-segmented-disabled" role="listbox" + tabindex="0" >
@@ -384,7 +374,6 @@ exports[`rc-segmented render segmented with options: 1 1`] = ` class="rc-segmented-item" > @@ -401,7 +390,6 @@ exports[`rc-segmented render segmented with options: 1 1`] = ` class="rc-segmented-item" > @@ -418,7 +406,6 @@ exports[`rc-segmented render segmented with options: 1 1`] = ` class="rc-segmented-item" > @@ -440,6 +427,7 @@ exports[`rc-segmented render segmented with options: 2 1`] = ` aria-label="segmented control" class="rc-segmented" role="listbox" + tabindex="0" >
@@ -483,7 +469,6 @@ exports[`rc-segmented render segmented with options: 2 1`] = ` class="rc-segmented-item" > @@ -505,6 +490,7 @@ exports[`rc-segmented render segmented with options: disabled 1`] = ` aria-label="segmented control" class="rc-segmented" role="listbox" + tabindex="0" >
@@ -571,6 +554,7 @@ exports[`rc-segmented render segmented with title 1`] = ` aria-label="segmented control" class="rc-segmented" role="listbox" + tabindex="0" >
@@ -614,7 +596,6 @@ exports[`rc-segmented render segmented with title 1`] = ` class="rc-segmented-item" > @@ -632,7 +613,6 @@ exports[`rc-segmented render segmented with title 1`] = ` class="rc-segmented-item" > @@ -649,7 +629,6 @@ exports[`rc-segmented render segmented with title 1`] = ` class="rc-segmented-item" > @@ -671,6 +650,7 @@ exports[`rc-segmented render segmented: disabled 1`] = ` aria-label="segmented control" class="rc-segmented rc-segmented-disabled" role="listbox" + tabindex="0" >
@@ -782,7 +758,6 @@ exports[`rc-segmented should render vertical segmented 1`] = ` class="rc-segmented-item" > @@ -804,6 +779,7 @@ exports[`rc-segmented should render vertical segmented and handle thumb animatio aria-label="segmented control" class="rc-segmented rc-segmented-vertical" role="listbox" + tabindex="0" >
@@ -847,7 +821,6 @@ exports[`rc-segmented should render vertical segmented and handle thumb animatio class="rc-segmented-item" > From c3e086cadbc363b1a5c2de96edbf5d588e41bedd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=B1=AA?= Date: Sat, 23 Nov 2024 23:23:07 +0800 Subject: [PATCH 02/10] feat: do not add focus style when clicking --- src/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.tsx b/src/index.tsx index d8c2401..23fe9e9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -106,6 +106,8 @@ const InternalSegmentedOption: React.FC<{ if (disabled) { return; } + // Do not add focus style when clicking + onBlur(); onChange(event, value); }; From 78bb22798f10de82eebd8f3a42032cea8d6e7f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=B1=AA?= Date: Sun, 24 Nov 2024 21:34:45 +0800 Subject: [PATCH 03/10] chore: remove some logic --- src/index.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 23fe9e9..f62efbb 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -85,8 +85,8 @@ const InternalSegmentedOption: React.FC<{ e: React.ChangeEvent, value: SegmentedRawOption, ) => void; - onFocus: () => void; - onBlur: () => void; + onFocus: (e: React.FocusEvent) => void; + onBlur: (e?: React.FocusEvent) => void; onKeyDown: (e: React.KeyboardEvent) => void; }> = ({ prefixCls, @@ -131,7 +131,6 @@ const InternalSegmentedOption: React.FC<{
{label} @@ -194,11 +193,11 @@ const Segmented = React.forwardRef( (option) => option.value === rawValue, ); - const handleFocus = () => { + const handleFocus = (event: React.FocusEvent) => { setIsFocused(true); }; - const handleBlur = () => { + const handleBlur = (event?: React.FocusEvent) => { setIsFocused(false); }; @@ -223,16 +222,12 @@ const Segmented = React.forwardRef( switch (event.which) { case KeyCode.LEFT: case KeyCode.UP: - event.preventDefault(); offset = -1; break; case KeyCode.RIGHT: case KeyCode.DOWN: - event.preventDefault(); offset = 1; break; - default: - return; } const nextIndex = getNextIndex(currentIndex, offset); @@ -246,9 +241,9 @@ const Segmented = React.forwardRef( return (
Date: Sun, 24 Nov 2024 22:50:20 +0800 Subject: [PATCH 04/10] test: add keyboard operation test case --- docs/demo/basic.tsx | 3 + package.json | 1 + src/index.tsx | 34 +++------- tests/__snapshots__/index.test.tsx.snap | 68 ++++--------------- tests/index.test.tsx | 90 +++++++++++++++++++++++++ 5 files changed, 116 insertions(+), 80 deletions(-) diff --git a/docs/demo/basic.tsx b/docs/demo/basic.tsx index 9be5161..db4d63d 100644 --- a/docs/demo/basic.tsx +++ b/docs/demo/basic.tsx @@ -8,6 +8,8 @@ export default function App() {
console.log(value, typeof value)} />
@@ -15,6 +17,7 @@ export default function App() { console.log(value, typeof value)} />
diff --git a/package.json b/package.json index a5f5115..db3a299 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@rc-component/father-plugin": "^1.0.1", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.2.1", + "@testing-library/user-event": "^14.5.2", "@types/classnames": "^2.2.9", "@types/jest": "^29.2.4", "@types/react": "^18.3.11", diff --git a/src/index.tsx b/src/index.tsx index f62efbb..7a950f9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,5 @@ import classNames from 'classnames'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; -import KeyCode from 'rc-util/lib/KeyCode'; import omit from 'rc-util/lib/omit'; import { composeRef } from 'rc-util/lib/ref'; import * as React from 'react'; @@ -202,37 +201,22 @@ const Segmented = React.forwardRef( }; // ======================= Keyboard ======================== - const getNextIndex = (current: number, offset: number) => { - if (disabled) { - return current; - } - - const total = segmentedOptions.length; - const nextIndex = (current + offset + total) % total; - - if (segmentedOptions[nextIndex]?.disabled) { - return getNextIndex(nextIndex, offset); - } - return nextIndex; - }; - const handleKeyDown = (event: React.KeyboardEvent) => { - let offset = 0; + const total = segmentedOptions.length; + let nextIndex = currentIndex; - switch (event.which) { - case KeyCode.LEFT: - case KeyCode.UP: - offset = -1; + switch (event.key) { + case 'ArrowLeft': + case 'ArrowUp': + nextIndex = currentIndex === 0 ? total - 1 : currentIndex - 1; break; - case KeyCode.RIGHT: - case KeyCode.DOWN: - offset = 1; + case 'ArrowRight': + case 'ArrowDown': + nextIndex = currentIndex === total - 1 ? 0 : currentIndex + 1; break; } - const nextIndex = getNextIndex(currentIndex, offset); const nextOption = segmentedOptions[nextIndex]; - if (nextOption) { setRawValue(nextOption.value); onChange?.(nextOption.value); diff --git a/tests/__snapshots__/index.test.tsx.snap b/tests/__snapshots__/index.test.tsx.snap index 9865593..cba0121 100644 --- a/tests/__snapshots__/index.test.tsx.snap +++ b/tests/__snapshots__/index.test.tsx.snap @@ -4,7 +4,7 @@ exports[`rc-segmented render empty segmented 1`] = `
iOS @@ -50,7 +49,6 @@ exports[`rc-segmented render label with ReactNode 1`] = `

Web @@ -84,7 +81,7 @@ exports[`rc-segmented render segmented ok 1`] = `
iOS @@ -117,7 +113,6 @@ exports[`rc-segmented render segmented ok 1`] = `
Android @@ -133,7 +128,6 @@ exports[`rc-segmented render segmented ok 1`] = `
Web @@ -147,7 +141,7 @@ exports[`rc-segmented render segmented with CSSMotion basic 1`] = `
iOS @@ -180,7 +173,6 @@ exports[`rc-segmented render segmented with CSSMotion basic 1`] = `
Android @@ -196,7 +188,6 @@ exports[`rc-segmented render segmented with CSSMotion basic 1`] = `
Web3 @@ -210,7 +201,7 @@ exports[`rc-segmented render segmented with options 1`] = `
iOS @@ -243,7 +233,6 @@ exports[`rc-segmented render segmented with options 1`] = `
Android @@ -259,7 +248,6 @@ exports[`rc-segmented render segmented with options 1`] = `
Web @@ -273,8 +261,7 @@ exports[`rc-segmented render segmented with options null/undefined 1`] = `
@@ -331,7 +315,7 @@ exports[`rc-segmented render segmented with options: 1 1`] = `
1 @@ -364,7 +347,6 @@ exports[`rc-segmented render segmented with options: 1 1`] = `
2 @@ -380,7 +362,6 @@ exports[`rc-segmented render segmented with options: 1 1`] = `
3 @@ -396,7 +377,6 @@ exports[`rc-segmented render segmented with options: 1 1`] = `
4 @@ -412,7 +392,6 @@ exports[`rc-segmented render segmented with options: 1 1`] = `
5 @@ -426,7 +405,7 @@ exports[`rc-segmented render segmented with options: 2 1`] = `
iOS @@ -459,7 +437,6 @@ exports[`rc-segmented render segmented with options: 2 1`] = `
Android @@ -475,7 +452,6 @@ exports[`rc-segmented render segmented with options: 2 1`] = `
Web @@ -489,7 +465,7 @@ exports[`rc-segmented render segmented with options: disabled 1`] = `
iOS @@ -523,7 +498,6 @@ exports[`rc-segmented render segmented with options: disabled 1`] = `
Android @@ -539,7 +513,6 @@ exports[`rc-segmented render segmented with options: disabled 1`] = `
Web @@ -553,7 +526,7 @@ exports[`rc-segmented render segmented with title 1`] = `
Web @@ -586,7 +558,6 @@ exports[`rc-segmented render segmented with title 1`] = `
hello1 @@ -602,7 +573,6 @@ exports[`rc-segmented render segmented with title 1`] = `
test1 @@ -619,7 +589,6 @@ exports[`rc-segmented render segmented with title 1`] = `
hello1 @@ -635,7 +604,6 @@ exports[`rc-segmented render segmented with title 1`] = `
foo1 @@ -649,8 +617,7 @@ exports[`rc-segmented render segmented: disabled 1`] = `
iOS @@ -684,7 +650,6 @@ exports[`rc-segmented render segmented: disabled 1`] = `
Android @@ -701,7 +666,6 @@ exports[`rc-segmented render segmented: disabled 1`] = `
Web @@ -715,7 +679,7 @@ exports[`rc-segmented should render vertical segmented 1`] = `
iOS @@ -748,7 +711,6 @@ exports[`rc-segmented should render vertical segmented 1`] = `
Android @@ -764,7 +726,6 @@ exports[`rc-segmented should render vertical segmented 1`] = `
Web @@ -778,7 +739,7 @@ exports[`rc-segmented should render vertical segmented and handle thumb animatio
iOS @@ -811,7 +771,6 @@ exports[`rc-segmented should render vertical segmented and handle thumb animatio
Android @@ -827,7 +786,6 @@ exports[`rc-segmented should render vertical segmented and handle thumb animatio
Web diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 8641abf..d1246a5 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -1,4 +1,5 @@ import { act, fireEvent, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as React from 'react'; import Segmented from '../src'; @@ -664,3 +665,92 @@ describe('rc-segmented', () => { }); }); }); + +describe('Segmented keyboard navigation', () => { + it('should be focusable through Tab key', async () => { + const user = userEvent.setup(); + const { getByRole, container } = render( + , + ); + + const segmentedContainer = getByRole('radiogroup'); + const inputs = container.querySelectorAll('.rc-segmented-item-input'); + const firstInput = inputs[0]; + + await user.tab(); + // segmented container should be focused + expect(segmentedContainer).toHaveFocus(); + await user.tab(); + // first segmented item should be focused + expect(firstInput).toHaveFocus(); + }); + + it('should handle circular navigation with arrow keys', async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + render( + , + ); + + // focus on segmented + await user.tab(); + // focus on first item + await user.tab(); + + // Test right navigation from first item and back to first item + await user.keyboard('{ArrowRight}'); + expect(onChange).toHaveBeenCalledWith('Android'); + await user.keyboard('{ArrowRight}'); + expect(onChange).toHaveBeenCalledWith('Web'); + await user.keyboard('{ArrowRight}'); + expect(onChange).toHaveBeenCalledWith('iOS'); + + // Test left navigation from first item to last item + await user.keyboard('{ArrowLeft}'); + expect(onChange).toHaveBeenCalledWith('Web'); + }); + + it('should skip Tab navigation when disabled', async () => { + const user = userEvent.setup(); + const { container } = render( + , + ); + + const segmentedContainer = container.querySelector('.rc-segmented'); + + await user.tab(); + + // Disabled state should not get focus + expect(segmentedContainer).not.toHaveFocus(); + + // Verify container has no tabIndex attribute + expect(segmentedContainer?.getAttribute('tabIndex')).toBeNull(); + }); + + it('should handle keyboard navigation with disabled options', async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + render( + , + ); + + await user.tab(); + await user.tab(); + + await user.keyboard('{ArrowRight}'); + expect(onChange).toHaveBeenCalledWith('Web'); + + onChange.mockClear(); + + await user.keyboard('{ArrowLeft}'); + expect(onChange).toHaveBeenCalledWith('iOS'); + }); +}); From 429831fd7cfa16234f5eefba80b9aa3cbde1469e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=B1=AA?= Date: Sun, 24 Nov 2024 22:51:14 +0800 Subject: [PATCH 05/10] fix: lint fix --- src/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 7a950f9..6c366f0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -192,11 +192,11 @@ const Segmented = React.forwardRef( (option) => option.value === rawValue, ); - const handleFocus = (event: React.FocusEvent) => { + const handleFocus = () => { setIsFocused(true); }; - const handleBlur = (event?: React.FocusEvent) => { + const handleBlur = () => { setIsFocused(false); }; From 28be306af4381c50daba4171703ae0df89dfe5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=B1=AA?= <1844749591@qq.com> Date: Mon, 25 Nov 2024 00:26:03 +0800 Subject: [PATCH 06/10] style: adjust focus style --- assets/index.less | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/index.less b/assets/index.less index 2405d79..30890f8 100644 --- a/assets/index.less +++ b/assets/index.less @@ -124,6 +124,7 @@ } &-focused { + border-radius: 2px; box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); } } From fbd0d56159dd4288e4470de84c3858ffa6a0fec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=B1=AA?= <1844749591@qq.com> Date: Thu, 28 Nov 2024 01:04:40 +0800 Subject: [PATCH 07/10] refactor: optimize keyboard navigation logic with modular arithmetic --- src/index.tsx | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 6c366f0..e80c856 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -201,26 +201,28 @@ const Segmented = React.forwardRef( }; // ======================= Keyboard ======================== - const handleKeyDown = (event: React.KeyboardEvent) => { + const onOffset = (offset: number) => { const total = segmentedOptions.length; - let nextIndex = currentIndex; + const nextIndex = (currentIndex + offset + total) % total; + + const nextOption = segmentedOptions[nextIndex]; + if (nextOption) { + setRawValue(nextOption.value); + onChange?.(nextOption.value); + } + }; + const handleKeyDown = (event: React.KeyboardEvent) => { switch (event.key) { case 'ArrowLeft': case 'ArrowUp': - nextIndex = currentIndex === 0 ? total - 1 : currentIndex - 1; + onOffset(-1); break; case 'ArrowRight': case 'ArrowDown': - nextIndex = currentIndex === total - 1 ? 0 : currentIndex + 1; + onOffset(1); break; } - - const nextOption = segmentedOptions[nextIndex]; - if (nextOption) { - setRawValue(nextOption.value); - onChange?.(nextOption.value); - } }; return ( From f12a903ecf6fb6ecbf97ae6e272462b7a77ccf3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=B1=AA?= <1844749591@qq.com> Date: Thu, 28 Nov 2024 01:12:36 +0800 Subject: [PATCH 08/10] chore: adjust code style --- src/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index e80c856..227a2ca 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -188,10 +188,6 @@ const Segmented = React.forwardRef( // ======================= Focus ======================== const [isFocused, setIsFocused] = React.useState(false); - const currentIndex = segmentedOptions.findIndex( - (option) => option.value === rawValue, - ); - const handleFocus = () => { setIsFocused(true); }; @@ -202,6 +198,10 @@ const Segmented = React.forwardRef( // ======================= Keyboard ======================== const onOffset = (offset: number) => { + const currentIndex = segmentedOptions.findIndex( + (option) => option.value === rawValue, + ); + const total = segmentedOptions.length; const nextIndex = (currentIndex + offset + total) % total; From 913982b88a43201aeb5bac79fafeebe4df30f8e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=B1=AA?= <1844749591@qq.com> Date: Thu, 28 Nov 2024 02:20:02 +0800 Subject: [PATCH 09/10] fix: click item should not have foucs style --- src/index.tsx | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 227a2ca..0b6bb1b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -87,6 +87,8 @@ const InternalSegmentedOption: React.FC<{ onFocus: (e: React.FocusEvent) => void; onBlur: (e?: React.FocusEvent) => void; onKeyDown: (e: React.KeyboardEvent) => void; + onKeyUp: (e: React.KeyboardEvent) => void; + onMouseDown: () => void; }> = ({ prefixCls, className, @@ -100,13 +102,13 @@ const InternalSegmentedOption: React.FC<{ onFocus, onBlur, onKeyDown, + onKeyUp, + onMouseDown, }) => { const handleChange = (event: React.ChangeEvent) => { if (disabled) { return; } - // Do not add focus style when clicking - onBlur(); onChange(event, value); }; @@ -115,6 +117,7 @@ const InternalSegmentedOption: React.FC<{ className={classNames(className, { [`${prefixCls}-item-disabled`]: disabled, })} + onMouseDown={onMouseDown} >
( const divProps = omit(restProps, ['children']); // ======================= Focus ======================== + const [isKeyboard, setIsKeyboard] = React.useState(false); const [isFocused, setIsFocused] = React.useState(false); const handleFocus = () => { @@ -196,6 +201,17 @@ const Segmented = React.forwardRef( setIsFocused(false); }; + const handleMouseDown = () => { + setIsKeyboard(false); + }; + + // capture keyboard tab interaction for correct focus style + const handleKeyUp = (event: React.KeyboardEvent) => { + if (event.key === 'Tab') { + setIsKeyboard(true); + } + }; + // ======================= Keyboard ======================== const onOffset = (offset: number) => { const currentIndex = segmentedOptions.findIndex( @@ -273,7 +289,9 @@ const Segmented = React.forwardRef( [`${prefixCls}-item-selected`]: segmentedOption.value === rawValue && !thumbShow, [`${prefixCls}-item-focused`]: - isFocused && segmentedOption.value === rawValue, + isFocused && + isKeyboard && + segmentedOption.value === rawValue, }, )} checked={segmentedOption.value === rawValue} @@ -281,6 +299,8 @@ const Segmented = React.forwardRef( onFocus={handleFocus} onBlur={handleBlur} onKeyDown={handleKeyDown} + onKeyUp={handleKeyUp} + onMouseDown={handleMouseDown} disabled={!!disabled || !!segmentedOption.disabled} /> ))} From 6817d512518bfe59f202af9e33fa318102ae078f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=B1=AA?= <1844749591@qq.com> Date: Thu, 28 Nov 2024 02:21:30 +0800 Subject: [PATCH 10/10] test: add test case --- tests/index.test.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/index.test.tsx b/tests/index.test.tsx index d1246a5..2a52c98 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -753,4 +753,16 @@ describe('Segmented keyboard navigation', () => { await user.keyboard('{ArrowLeft}'); expect(onChange).toHaveBeenCalledWith('iOS'); }); + + it('should not have focus style when clicking', async () => { + const user = userEvent.setup(); + const { container } = render( + , + ); + + await user.click(container.querySelector('.rc-segmented-item-input')!); + expect(container.querySelector('.rc-segmented-item-input')).not.toHaveClass( + 'rc-segmented-item-input-focused', + ); + }); });