Skip to content

Commit a633130

Browse files
authored
Fix RadioGroup lost roving tab index (#4985)
* Fix RadioGroup lost roving tab index
1 parent 6f39866 commit a633130

File tree

3 files changed

+102
-11
lines changed

3 files changed

+102
-11
lines changed

packages/@react-aria/radio/src/useRadio.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,14 @@ export function useRadio(props: AriaRadioProps, state: RadioGroupState, ref: Ref
6868
}), ref);
6969
let interactions = mergeProps(pressProps, focusableProps);
7070
let domProps = filterDOMProps(props, {labelable: true});
71-
let tabIndex = state.lastFocusedValue === value || state.lastFocusedValue == null ? 0 : -1;
71+
let tabIndex = -1;
72+
if (state.selectedValue != null) {
73+
if (state.selectedValue === value) {
74+
tabIndex = 0;
75+
}
76+
} else if (state.lastFocusedValue === value || state.lastFocusedValue == null) {
77+
tabIndex = 0;
78+
}
7279
if (isDisabled) {
7380
tabIndex = undefined;
7481
}

packages/@react-spectrum/radio/stories/Radio.stories.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {action} from '@storybook/addon-actions';
14-
import {Content, ContextualHelp, Flex, Heading} from '@adobe/react-spectrum';
14+
import {Button, Content, ContextualHelp, Flex, Heading} from '@adobe/react-spectrum';
1515
import {Provider} from '@react-spectrum/provider';
1616
import {Radio, RadioGroup} from '../src';
1717
import React, {useState} from 'react';
@@ -253,3 +253,30 @@ function renderWithDescriptionErrorMessageAndValidation(props) {
253253

254254
return <Example />;
255255
}
256+
257+
export const ControlledRovingTab = () => {
258+
const [selected, setSelected] = useState('1');
259+
260+
return (
261+
<Flex direction="column" gap="16px" alignItems="center" margin="16px">
262+
<Button variant="primary" onPress={() => setSelected('2')}>
263+
Make it "Two"
264+
</Button>
265+
<RadioGroup
266+
label="Lucky number? (controlled)"
267+
value={selected}
268+
onChange={setSelected}>
269+
<Radio value="1">One</Radio>
270+
<Radio value="2">Two</Radio>
271+
<Radio value="3">Three</Radio>
272+
<Radio value="4">Four</Radio>
273+
</RadioGroup>
274+
<Button variant="primary" onPress={() => setSelected('3')}>
275+
Make it "Three"
276+
</Button>
277+
</Flex>
278+
);
279+
};
280+
ControlledRovingTab.story = {
281+
name: 'controlled roving tab'
282+
};

packages/@react-spectrum/radio/test/Radio.test.js

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -534,15 +534,72 @@ describe('Radios', function () {
534534
expect(radios[2]).toHaveAttribute('tabIndex', '-1');
535535
});
536536

537-
it('RadioGroup roving tabIndex for autoFocus', async () => {
538-
jest.useFakeTimers();
539-
let {getAllByRole} = renderRadioGroup(RadioGroup, Radio, {}, [{}, {autoFocus: true}, {}]);
540-
let radios = getAllByRole('radio');
541-
act(() => {jest.runAllTimers();});
542-
expect(radios[0]).toHaveAttribute('tabIndex', '-1');
543-
expect(radios[1]).toHaveAttribute('tabIndex', '0');
544-
expect(radios[2]).toHaveAttribute('tabIndex', '-1');
545-
jest.useRealTimers();
537+
describe('roving tab timers', () => {
538+
beforeAll(() => {
539+
jest.useFakeTimers();
540+
});
541+
afterAll(() => {
542+
jest.useRealTimers();
543+
});
544+
it('RadioGroup roving tabIndex for controlled radios', async () => {
545+
function ControlledRadioGroup(props) {
546+
let [value, setValue] = React.useState(null);
547+
return (
548+
<>
549+
<Button variant="primary" onPress={() => setValue('cats')}>
550+
Make it "Two"
551+
</Button>
552+
<RadioGroup aria-label="favorite pet" value={value} onChange={setValue}>
553+
<Radio value="dogs">Dogs</Radio>
554+
<Radio value="cats">Cats</Radio>
555+
<Radio value="dragons">Dragons</Radio>
556+
<Radio value="unicorns">Unicorns</Radio>
557+
</RadioGroup>
558+
<Button variant="primary" onPress={() => setValue('dragons')}>
559+
Make it "Three"
560+
</Button>
561+
</>
562+
);
563+
}
564+
565+
let {getAllByRole} = render(
566+
<ControlledRadioGroup />
567+
);
568+
let radios = getAllByRole('radio');
569+
let buttons = getAllByRole('button');
570+
expect(radios[0]).toHaveAttribute('tabIndex', '0');
571+
expect(radios[1]).toHaveAttribute('tabIndex', '0');
572+
expect(radios[2]).toHaveAttribute('tabIndex', '0');
573+
expect(radios[3]).toHaveAttribute('tabIndex', '0');
574+
575+
userEvent.tab();
576+
act(() => {jest.runAllTimers();});
577+
expect(document.activeElement).toBe(buttons[0]);
578+
fireEvent.keyDown(document.activeElement, {key: 'Enter'});
579+
fireEvent.keyUp(document.activeElement, {key: 'Enter'});
580+
userEvent.tab();
581+
act(() => {jest.runAllTimers();});
582+
expect(document.activeElement).toBe(radios[1]);
583+
userEvent.tab();
584+
act(() => {jest.runAllTimers();});
585+
expect(document.activeElement).toBe(buttons[1]);
586+
fireEvent.keyDown(document.activeElement, {key: 'Enter'});
587+
fireEvent.keyUp(document.activeElement, {key: 'Enter'});
588+
589+
expect(radios[0]).toHaveAttribute('tabIndex', '-1');
590+
expect(radios[1]).toHaveAttribute('tabIndex', '-1');
591+
expect(radios[2]).toHaveAttribute('tabIndex', '0');
592+
expect(radios[3]).toHaveAttribute('tabIndex', '-1');
593+
});
594+
595+
it('RadioGroup roving tabIndex for autoFocus', async () => {
596+
let {getAllByRole} = renderRadioGroup(RadioGroup, Radio, {}, [{}, {autoFocus: true}, {}]);
597+
let radios = getAllByRole('radio');
598+
act(() => {jest.runAllTimers();});
599+
expect(radios[0]).toHaveAttribute('tabIndex', '-1');
600+
expect(radios[1]).toHaveAttribute('tabIndex', '0');
601+
expect(radios[2]).toHaveAttribute('tabIndex', '-1');
602+
});
546603
});
547604

548605
it.each`

0 commit comments

Comments
 (0)