From 93da2206ca6db9c12a6d51b0f7afd05f8077aa8c Mon Sep 17 00:00:00 2001 From: Nick McCurdy Date: Tue, 9 Jun 2020 08:10:36 -0400 Subject: [PATCH] fix(tab): make tab respect radio groups (fix #207) --- src/__tests__/tab.js | 48 ++++++++++++++++++++++++++++++++++++++++++++ src/index.js | 32 +++++++++++++++++++++++++---- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/__tests__/tab.js b/src/__tests__/tab.js index b501988a..10038513 100644 --- a/src/__tests__/tab.js +++ b/src/__tests__/tab.js @@ -273,3 +273,51 @@ test('should keep focus on the document if there are no enabled, focusable eleme userEvent.tab({shift: true}) expect(document.body).toHaveFocus() }) + +test('should respect radio groups', () => { + render( + <> + + + + + , + ) + + const [firstLeft, firstRight, , secondRight] = screen.getAllByTestId( + 'element', + ) + + userEvent.tab() + + expect(firstLeft).toHaveFocus() + + userEvent.tab() + + expect(secondRight).toHaveFocus() + + userEvent.tab({shift: true}) + + expect(firstRight).toHaveFocus() +}) diff --git a/src/index.js b/src/index.js index 79694f1e..d09cb723 100644 --- a/src/index.js +++ b/src/index.js @@ -408,15 +408,39 @@ function tab({shift = false, focusTrap = document} = {}) { return diff === 0 ? a.idx - b.idx : diff }) + .map(({el}) => el) + + if (shift) orderedElements.reverse() + + // keep only the checked or first element in each radio group + const prunedElements = [] + for (const el of orderedElements) { + if (el.type === 'radio' && el.name) { + const replacedIndex = prunedElements.findIndex( + ({name}) => name === el.name, + ) + + if (replacedIndex === -1) { + prunedElements.push(el) + } else if (el.checked) { + prunedElements.splice(replacedIndex, 1) + prunedElements.push(el) + } + } else { + prunedElements.push(el) + } + } + + if (shift) prunedElements.reverse() - const index = orderedElements.findIndex( - ({el}) => el === el.ownerDocument.activeElement, + const index = prunedElements.findIndex( + el => el === el.ownerDocument.activeElement, ) const nextIndex = shift ? index - 1 : index + 1 - const defaultIndex = shift ? orderedElements.length - 1 : 0 + const defaultIndex = shift ? prunedElements.length - 1 : 0 - const {el: next} = orderedElements[nextIndex] || orderedElements[defaultIndex] + const next = prunedElements[nextIndex] || prunedElements[defaultIndex] if (next.getAttribute('tabindex') === null) { next.setAttribute('tabindex', '0') // jsdom requires tabIndex=0 for an item to become 'document.activeElement'