#useFocusTrap() gets confused when you shift + tab back to a Radio Group

3 messages · Page 1 of 1 (latest)

flint cypress
#

When you shift + tab backwards into a Radio Group, the last Radio button in the Group is selected 0.
But useFocusTrap() always calculates finalTabbable as the first Radio button in the Group 1.
This means it fails to preventDefault() or manually set the focus to the last element in tabbable, and the user is able to escape the focus trap.

Here is a minimal reproduction: https://codesandbox.io/s/nervous-surf-8xdhy7?file=/src/App.tsx

nervous-surf-8xdhy7 by Devon-Dickson using @emotion/react, @mantine/carousel, @mantine/core, @mantine/dates, @mantine/dropzone, @mantine/form, @mantine/hooks, @mantine/modals, @mantine/notifications

GitHub

A fully featured React components library. Contribute to mantinedev/mantine development by creating an account on GitHub.

flint cypress
#

Here's a rough fix that address my use case. I suspect there are other examples where something similar or a totally different approach would be warranted.

export function scopeTab(node: HTMLElement, event: KeyboardEvent) {
    const tabbable = findTabbableDescendants(node)
    if (!tabbable.length) {
        event.preventDefault()
        return
    }
    const finalTabbable = tabbable[event.shiftKey ? 0 : tabbable.length - 1]
    const root = node.getRootNode() as unknown as DocumentOrShadowRoot
    let leavingFinalTabbable = finalTabbable === root.activeElement || node === root.activeElement

    // Handle the case of the active element being in a RadioGroup with the finalTabbable element
    if (root.activeElement.type === "radio") {
        const activeRadioGroup = tabbable.filter(
            (element) => element.type === "radio" && element.name === root.activeElement.name
        )
        leavingFinalTabbable = activeRadioGroup.includes(finalTabbable)
    }

    if (!leavingFinalTabbable) {
        return
    }

    event.preventDefault()

    const target = tabbable[event.shiftKey ? tabbable.length - 1 : 0]

    if (target) {
        target.focus()
    }
}
#

I'll make an issue in GH and perhaps a PR to get this some more attention and find a better and more generic solution.