Skip to content

Commit

Permalink
fix(tab): make tab respect radio groups (fix #207)
Browse files Browse the repository at this point in the history
  • Loading branch information
nickmccurdy committed Jun 9, 2020
1 parent 9c6be8c commit 93da220
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 4 deletions.
48 changes: 48 additions & 0 deletions src/__tests__/tab.js
Expand Up @@ -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(
<>
<input
data-testid="element"
type="radio"
name="first"
value="first_left"
/>
<input
data-testid="element"
type="radio"
name="first"
value="first_right"
/>
<input
data-testid="element"
type="radio"
name="second"
value="second_left"
/>
<input
data-testid="element"
type="radio"
name="second"
value="second_right"
defaultChecked
/>
</>,
)

const [firstLeft, firstRight, , secondRight] = screen.getAllByTestId(
'element',
)

userEvent.tab()

expect(firstLeft).toHaveFocus()

userEvent.tab()

expect(secondRight).toHaveFocus()

userEvent.tab({shift: true})

expect(firstRight).toHaveFocus()
})
32 changes: 28 additions & 4 deletions src/index.js
Expand Up @@ -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'
Expand Down

0 comments on commit 93da220

Please sign in to comment.