Skip to content

Commit dbdefef

Browse files
authored
Merge pull request testing-library#330 from testing-library/pr/make-tab-respect-radio-groups
fix(tab): make tab respect radio groups (fix testing-library#207)
2 parents 9c6be8c + 93da220 commit dbdefef

File tree

2 files changed

+76
-4
lines changed

2 files changed

+76
-4
lines changed

src/__tests__/tab.js

+48
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,51 @@ test('should keep focus on the document if there are no enabled, focusable eleme
273273
userEvent.tab({shift: true})
274274
expect(document.body).toHaveFocus()
275275
})
276+
277+
test('should respect radio groups', () => {
278+
render(
279+
<>
280+
<input
281+
data-testid="element"
282+
type="radio"
283+
name="first"
284+
value="first_left"
285+
/>
286+
<input
287+
data-testid="element"
288+
type="radio"
289+
name="first"
290+
value="first_right"
291+
/>
292+
<input
293+
data-testid="element"
294+
type="radio"
295+
name="second"
296+
value="second_left"
297+
/>
298+
<input
299+
data-testid="element"
300+
type="radio"
301+
name="second"
302+
value="second_right"
303+
defaultChecked
304+
/>
305+
</>,
306+
)
307+
308+
const [firstLeft, firstRight, , secondRight] = screen.getAllByTestId(
309+
'element',
310+
)
311+
312+
userEvent.tab()
313+
314+
expect(firstLeft).toHaveFocus()
315+
316+
userEvent.tab()
317+
318+
expect(secondRight).toHaveFocus()
319+
320+
userEvent.tab({shift: true})
321+
322+
expect(firstRight).toHaveFocus()
323+
})

src/index.js

+28-4
Original file line numberDiff line numberDiff line change
@@ -408,15 +408,39 @@ function tab({shift = false, focusTrap = document} = {}) {
408408

409409
return diff === 0 ? a.idx - b.idx : diff
410410
})
411+
.map(({el}) => el)
412+
413+
if (shift) orderedElements.reverse()
414+
415+
// keep only the checked or first element in each radio group
416+
const prunedElements = []
417+
for (const el of orderedElements) {
418+
if (el.type === 'radio' && el.name) {
419+
const replacedIndex = prunedElements.findIndex(
420+
({name}) => name === el.name,
421+
)
422+
423+
if (replacedIndex === -1) {
424+
prunedElements.push(el)
425+
} else if (el.checked) {
426+
prunedElements.splice(replacedIndex, 1)
427+
prunedElements.push(el)
428+
}
429+
} else {
430+
prunedElements.push(el)
431+
}
432+
}
433+
434+
if (shift) prunedElements.reverse()
411435

412-
const index = orderedElements.findIndex(
413-
({el}) => el === el.ownerDocument.activeElement,
436+
const index = prunedElements.findIndex(
437+
el => el === el.ownerDocument.activeElement,
414438
)
415439

416440
const nextIndex = shift ? index - 1 : index + 1
417-
const defaultIndex = shift ? orderedElements.length - 1 : 0
441+
const defaultIndex = shift ? prunedElements.length - 1 : 0
418442

419-
const {el: next} = orderedElements[nextIndex] || orderedElements[defaultIndex]
443+
const next = prunedElements[nextIndex] || prunedElements[defaultIndex]
420444

421445
if (next.getAttribute('tabindex') === null) {
422446
next.setAttribute('tabindex', '0') // jsdom requires tabIndex=0 for an item to become 'document.activeElement'

0 commit comments

Comments
 (0)