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')) })