import React, { useState, useRef } from 'react'
import { render } from '@testing-library/react'
import { FocusTrap } from './focus-trap'
import { assertActiveElement } from '../../test-utils/accessibility-assertions'
import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs'
import { click, press, shift, Keys } from '../../test-utils/interactions'
it('should focus the first focusable element inside the FocusTrap', () => {
let { getByText } = render(
)
assertActiveElement(getByText('Trigger'))
})
it('should focus the autoFocus element inside the FocusTrap if that exists', () => {
render(
)
assertActiveElement(document.getElementById('b'))
})
it('should focus the initialFocus element inside the FocusTrap if that exists', () => {
function Example() {
let initialFocusRef = useRef(null)
return (
)
}
render()
assertActiveElement(document.getElementById('c'))
})
it('should focus the initialFocus element inside the FocusTrap even if another element has autoFocus', () => {
function Example() {
let initialFocusRef = useRef(null)
return (
)
}
render()
assertActiveElement(document.getElementById('c'))
})
it('should warn when there is no focusable element inside the FocusTrap', () => {
let spy = jest.spyOn(console, 'warn').mockImplementation(jest.fn())
function Example() {
return (
Nothing to see here...
)
}
render()
expect(spy.mock.calls[0][0]).toBe('There are no focusable elements inside the ')
})
it(
'should not be possible to programmatically escape the focus trap',
suppressConsoleLogs(async () => {
function Example() {
return (
<>
>
)
}
render()
let [a, b, c, d] = Array.from(document.querySelectorAll('input'))
// Ensure that input-b is the active element
assertActiveElement(b)
// Tab to the next item
await press(Keys.Tab)
// Ensure that input-c is the active element
assertActiveElement(c)
// Try to move focus
a?.focus()
// Ensure that input-c is still the active element
assertActiveElement(c)
// Click on an element within the FocusTrap
await click(b)
// Ensure that input-b is the active element
assertActiveElement(b)
// Try to move focus again
a?.focus()
// Ensure that input-b is still the active element
assertActiveElement(b)
// Focus on an element within the FocusTrap
d?.focus()
// Ensure that input-d is the active element
assertActiveElement(d)
// Try to move focus again
a?.focus()
// Ensure that input-d is still the active element
assertActiveElement(d)
})
)
it('should restore the previously focused element, before entering the FocusTrap, after the FocusTrap unmounts', async () => {
function Example() {
let [visible, setVisible] = useState(false)
return (
<>
{visible && (
)}
>
)
}
render()
// The input should have focus by default because of the autoFocus prop
assertActiveElement(document.getElementById('item-1'))
// Open the modal
await click(document.getElementById('item-2')) // This will also focus this button
// Ensure that the first item inside the focus trap is focused
assertActiveElement(document.getElementById('item-3'))
// Close the modal
await click(document.getElementById('item-3'))
// Ensure that we restored focus correctly
assertActiveElement(document.getElementById('item-2'))
})
it('should be possible tab to the next focusable element within the focus trap', async () => {
render(
<>
>
)
// Item A should be focused because the FocusTrap will focus the first item
assertActiveElement(document.getElementById('item-a'))
// Next
await press(Keys.Tab)
assertActiveElement(document.getElementById('item-b'))
// Next
await press(Keys.Tab)
assertActiveElement(document.getElementById('item-c'))
// Loop around!
await press(Keys.Tab)
assertActiveElement(document.getElementById('item-a'))
})
it('should be possible shift+tab to the previous focusable element within the focus trap', async () => {
render(
<>
>
)
// Item A should be focused because the FocusTrap will focus the first item
assertActiveElement(document.getElementById('item-a'))
// Previous (loop around!)
await press(shift(Keys.Tab))
assertActiveElement(document.getElementById('item-c'))
// Previous
await press(shift(Keys.Tab))
assertActiveElement(document.getElementById('item-b'))
// Previous
await press(shift(Keys.Tab))
assertActiveElement(document.getElementById('item-a'))
})
it('should skip the initial "hidden" elements within the focus trap', async () => {
render(
<>
>
)
// Item C should be focused because the FocusTrap had to skip the first 2
assertActiveElement(document.getElementById('item-c'))
})
it('should be possible skip "hidden" elements within the focus trap', async () => {
render(
<>
>
)
// Item A should be focused because the FocusTrap will focus the first item
assertActiveElement(document.getElementById('item-a'))
// Next
await press(Keys.Tab)
assertActiveElement(document.getElementById('item-b'))
// Notice that we skipped item-c
// Next
await press(Keys.Tab)
assertActiveElement(document.getElementById('item-d'))
// Loop around!
await press(Keys.Tab)
assertActiveElement(document.getElementById('item-a'))
})
it('should be possible skip disabled elements within the focus trap', async () => {
render(
<>
>
)
// Item A should be focused because the FocusTrap will focus the first item
assertActiveElement(document.getElementById('item-a'))
// Next
await press(Keys.Tab)
assertActiveElement(document.getElementById('item-b'))
// Notice that we skipped item-c
// Next
await press(Keys.Tab)
assertActiveElement(document.getElementById('item-d'))
// Loop around!
await press(Keys.Tab)
assertActiveElement(document.getElementById('item-a'))
})