diff --git a/CHANGELOG.md b/CHANGELOG.md index 393dbb1651..8b8e88707d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove `focus()` from Listbox Option ([#1218](https://github.com/tailwindlabs/headlessui/pull/1218)) - Improve some internal code ([#1221](https://github.com/tailwindlabs/headlessui/pull/1221)) - Use `ownerDocument` instead of `document` ([#1158](https://github.com/tailwindlabs/headlessui/pull/1158)) +- Ensure focus trap, Tabs and Dialog play well together ([#1231](https://github.com/tailwindlabs/headlessui/pull/1231)) ### Added @@ -52,6 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Don’t drop initial character when searching in Combobox ([#1223](https://github.com/tailwindlabs/headlessui/pull/1223)) - Use `ownerDocument` instead of `document` ([#1158](https://github.com/tailwindlabs/headlessui/pull/1158)) - Re-expose `el` ([#1230](https://github.com/tailwindlabs/headlessui/pull/1230)) +- Ensure focus trap, Tabs and Dialog play well together ([#1231](https://github.com/tailwindlabs/headlessui/pull/1231)) ### Added diff --git a/packages/@headlessui-react/src/components/tabs/tabs.test.tsx b/packages/@headlessui-react/src/components/tabs/tabs.test.tsx index d824817405..cb3a3e3b8b 100644 --- a/packages/@headlessui-react/src/components/tabs/tabs.test.tsx +++ b/packages/@headlessui-react/src/components/tabs/tabs.test.tsx @@ -33,320 +33,236 @@ describe('safeguards', () => { }) ) - it('should be possible to render Tab.Group without crashing', async () => { - render( - - - Tab 1 - Tab 2 - Tab 3 - - - - Content 1 - Content 2 - Content 3 - - - ) + it( + 'should be possible to render Tab.Group without crashing', + suppressConsoleLogs(async () => { + render( + + + Tab 1 + Tab 2 + Tab 3 + - assertTabs({ active: 0 }) - }) + + Content 1 + Content 2 + Content 3 + + + ) + + assertTabs({ active: 0 }) + }) + ) }) describe('Rendering', () => { - it('should be possible to render the Tab.Panels first, then the Tab.List', async () => { - render( - - - Content 1 - Content 2 - Content 3 - - - - Tab 1 - Tab 2 - Tab 3 - - - ) - - assertTabs({ active: 0 }) - }) - - it('should guarantee the order of DOM nodes when performing actions', async () => { - function Example() { - let [hide, setHide] = useState(false) - - return ( - <> - - - - Tab 1 - {!hide && Tab 2} - Tab 3 - + it( + 'should be possible to render the Tab.Panels first, then the Tab.List', + suppressConsoleLogs(async () => { + render( + + + Content 1 + Content 2 + Content 3 + - - Content 1 - {!hide && Content 2} - Content 3 - - - + + Tab 1 + Tab 2 + Tab 3 + + ) - } - - render() - - await click(getByText('toggle')) // Remove Tab 2 - await click(getByText('toggle')) // Re-add Tab 2 - - await press(Keys.Tab) - assertTabs({ active: 0 }) - - await press(Keys.ArrowRight) - assertTabs({ active: 1 }) - await press(Keys.ArrowRight) - assertTabs({ active: 2 }) - }) + assertTabs({ active: 0 }) + }) + ) - describe('`renderProps`', () => { - it('should expose the `selectedIndex` on the `Tab.Group` component', async () => { - render( - - {(data) => ( - <> -
{JSON.stringify(data)}
+ it( + 'should guarantee the order of DOM nodes when performing actions', + suppressConsoleLogs(async () => { + function Example() { + let [hide, setHide] = useState(false) + return ( + <> + + Tab 1 - Tab 2 + {!hide && Tab 2} Tab 3 Content 1 - Content 2 + {!hide && Content 2} Content 3 - - )} - - ) - - expect(document.getElementById('exposed')).toHaveTextContent( - JSON.stringify({ selectedIndex: 0 }) - ) - - await click(getByText('Tab 2')) - - expect(document.getElementById('exposed')).toHaveTextContent( - JSON.stringify({ selectedIndex: 1 }) - ) - }) +
+ + ) + } - it('should expose the `selectedIndex` on the `Tab.List` component', async () => { - render( - - - {(data) => ( - <> -
{JSON.stringify(data)}
- Tab 1 - Tab 2 - Tab 3 - - )} -
+ render() - - Content 1 - Content 2 - Content 3 - -
- ) + await click(getByText('toggle')) // Remove Tab 2 + await click(getByText('toggle')) // Re-add Tab 2 - expect(document.getElementById('exposed')).toHaveTextContent( - JSON.stringify({ selectedIndex: 0 }) - ) + await press(Keys.Tab) + assertTabs({ active: 0 }) - await click(getByText('Tab 2')) + await press(Keys.ArrowRight) + assertTabs({ active: 1 }) - expect(document.getElementById('exposed')).toHaveTextContent( - JSON.stringify({ selectedIndex: 1 }) - ) + await press(Keys.ArrowRight) + assertTabs({ active: 2 }) }) + ) - it('should expose the `selectedIndex` on the `Tab.Panels` component', async () => { - render( - - - Tab 1 - Tab 2 - Tab 3 - - - + describe('`renderProps`', () => { + it( + 'should expose the `selectedIndex` on the `Tab.Group` component', + suppressConsoleLogs(async () => { + render( + {(data) => ( <>
{JSON.stringify(data)}
- Content 1 - Content 2 - Content 3 + + + Tab 1 + Tab 2 + Tab 3 + + + + Content 1 + Content 2 + Content 3 + )} -
-
- ) + + ) - expect(document.getElementById('exposed')).toHaveTextContent( - JSON.stringify({ selectedIndex: 0 }) - ) + expect(document.getElementById('exposed')).toHaveTextContent( + JSON.stringify({ selectedIndex: 0 }) + ) - await click(getByText('Tab 2')) + await click(getByText('Tab 2')) - expect(document.getElementById('exposed')).toHaveTextContent( - JSON.stringify({ selectedIndex: 1 }) - ) - }) + expect(document.getElementById('exposed')).toHaveTextContent( + JSON.stringify({ selectedIndex: 1 }) + ) + }) + ) - it('should expose the `selected` state on the `Tab` components', async () => { - render( - - - - {(data) => ( - <> -
{JSON.stringify(data)}
- Tab 1 - - )} -
- - {(data) => ( - <> -
{JSON.stringify(data)}
- Tab 2 - - )} -
- + it( + 'should expose the `selectedIndex` on the `Tab.List` component', + suppressConsoleLogs(async () => { + render( + + {(data) => ( <> -
{JSON.stringify(data)}
- Tab 3 +
{JSON.stringify(data)}
+ Tab 1 + Tab 2 + Tab 3 )} -
-
+ - - Content 1 - Content 2 - Content 3 - -
- ) + + Content 1 + Content 2 + Content 3 + + + ) - expect(document.querySelector('[data-tab="0"]')).toHaveTextContent( - JSON.stringify({ selected: true }) - ) - expect(document.querySelector('[data-tab="1"]')).toHaveTextContent( - JSON.stringify({ selected: false }) - ) - expect(document.querySelector('[data-tab="2"]')).toHaveTextContent( - JSON.stringify({ selected: false }) - ) + expect(document.getElementById('exposed')).toHaveTextContent( + JSON.stringify({ selectedIndex: 0 }) + ) - await click(getTabs()[1]) + await click(getByText('Tab 2')) - expect(document.querySelector('[data-tab="0"]')).toHaveTextContent( - JSON.stringify({ selected: false }) - ) - expect(document.querySelector('[data-tab="1"]')).toHaveTextContent( - JSON.stringify({ selected: true }) - ) - expect(document.querySelector('[data-tab="2"]')).toHaveTextContent( - JSON.stringify({ selected: false }) - ) - }) + expect(document.getElementById('exposed')).toHaveTextContent( + JSON.stringify({ selectedIndex: 1 }) + ) + }) + ) - it('should expose the `selected` state on the `Tab.Panel` components', async () => { - render( - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should expose the `selectedIndex` on the `Tab.Panels` component', + suppressConsoleLogs(async () => { + render( + + + Tab 1 + Tab 2 + Tab 3 + - - - {(data) => ( - <> -
{JSON.stringify(data)}
- Content 1 - - )} -
- - {(data) => ( - <> -
{JSON.stringify(data)}
- Content 2 - - )} -
- + {(data) => ( <> -
{JSON.stringify(data)}
- Content 3 +
{JSON.stringify(data)}
+ Content 1 + Content 2 + Content 3 )} -
-
-
- ) + +
+ ) - expect(document.querySelector('[data-panel="0"]')).toHaveTextContent( - JSON.stringify({ selected: true }) - ) - expect(document.querySelector('[data-panel="1"]')).toHaveTextContent( - JSON.stringify({ selected: false }) - ) - expect(document.querySelector('[data-panel="2"]')).toHaveTextContent( - JSON.stringify({ selected: false }) - ) + expect(document.getElementById('exposed')).toHaveTextContent( + JSON.stringify({ selectedIndex: 0 }) + ) - await click(getByText('Tab 2')) + await click(getByText('Tab 2')) - expect(document.querySelector('[data-panel="0"]')).toHaveTextContent( - JSON.stringify({ selected: false }) - ) - expect(document.querySelector('[data-panel="1"]')).toHaveTextContent( - JSON.stringify({ selected: true }) - ) - expect(document.querySelector('[data-panel="2"]')).toHaveTextContent( - JSON.stringify({ selected: false }) - ) - }) - }) + expect(document.getElementById('exposed')).toHaveTextContent( + JSON.stringify({ selectedIndex: 1 }) + ) + }) + ) - describe('`defaultIndex`', () => { - it('should jump to the nearest tab when the defaultIndex is out of bounds (-2)', async () => { - render( - <> - + it( + 'should expose the `selected` state on the `Tab` components', + suppressConsoleLogs(async () => { + render( + - Tab 1 - Tab 2 - Tab 3 + + {(data) => ( + <> +
{JSON.stringify(data)}
+ Tab 1 + + )} +
+ + {(data) => ( + <> +
{JSON.stringify(data)}
+ Tab 2 + + )} +
+ + {(data) => ( + <> +
{JSON.stringify(data)}
+ Tab 3 + + )} +
@@ -355,23 +271,37 @@ describe('Rendering', () => { Content 3
+ ) - - - ) - - assertActiveElement(document.body) + expect(document.querySelector('[data-tab="0"]')).toHaveTextContent( + JSON.stringify({ selected: true }) + ) + expect(document.querySelector('[data-tab="1"]')).toHaveTextContent( + JSON.stringify({ selected: false }) + ) + expect(document.querySelector('[data-tab="2"]')).toHaveTextContent( + JSON.stringify({ selected: false }) + ) - await press(Keys.Tab) + await click(getTabs()[1]) - assertTabs({ active: 0 }) - assertActiveElement(getByText('Tab 1')) - }) + expect(document.querySelector('[data-tab="0"]')).toHaveTextContent( + JSON.stringify({ selected: false }) + ) + expect(document.querySelector('[data-tab="1"]')).toHaveTextContent( + JSON.stringify({ selected: true }) + ) + expect(document.querySelector('[data-tab="2"]')).toHaveTextContent( + JSON.stringify({ selected: false }) + ) + }) + ) - it('should jump to the nearest tab when the defaultIndex is out of bounds (+5)', async () => { - render( - <> - + it( + 'should expose the `selected` state on the `Tab.Panel` components', + suppressConsoleLogs(async () => { + render( + Tab 1 Tab 2 @@ -379,89 +309,98 @@ describe('Rendering', () => { - Content 1 - Content 2 - Content 3 - - - - - - ) - - assertActiveElement(document.body) - - await press(Keys.Tab) - - assertTabs({ active: 2 }) - assertActiveElement(getByText('Tab 3')) - }) - - it('should jump to the next available tab when the defaultIndex is a disabled tab', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - - - - Content 1 - Content 2 - Content 3 + + {(data) => ( + <> +
{JSON.stringify(data)}
+ Content 1 + + )} +
+ + {(data) => ( + <> +
{JSON.stringify(data)}
+ Content 2 + + )} +
+ + {(data) => ( + <> +
{JSON.stringify(data)}
+ Content 3 + + )} +
+ ) - - - ) - - assertActiveElement(document.body) - - await press(Keys.Tab) + expect(document.querySelector('[data-panel="0"]')).toHaveTextContent( + JSON.stringify({ selected: true }) + ) + expect(document.querySelector('[data-panel="1"]')).toHaveTextContent( + JSON.stringify({ selected: false }) + ) + expect(document.querySelector('[data-panel="2"]')).toHaveTextContent( + JSON.stringify({ selected: false }) + ) - assertTabs({ active: 1 }) - assertActiveElement(getByText('Tab 2')) - }) + await click(getByText('Tab 2')) - it('should jump to the next available tab when the defaultIndex is a disabled tab and wrap around', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + expect(document.querySelector('[data-panel="0"]')).toHaveTextContent( + JSON.stringify({ selected: false }) + ) + expect(document.querySelector('[data-panel="1"]')).toHaveTextContent( + JSON.stringify({ selected: true }) + ) + expect(document.querySelector('[data-panel="2"]')).toHaveTextContent( + JSON.stringify({ selected: false }) + ) + }) + ) + }) - - Content 1 - Content 2 - Content 3 - - + describe('`defaultIndex`', () => { + it( + 'should jump to the nearest tab when the defaultIndex is out of bounds (-2)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - - ) + + Content 1 + Content 2 + Content 3 + + - assertActiveElement(document.body) + + + ) - await press(Keys.Tab) + assertActiveElement(document.body) - assertTabs({ active: 0 }) - assertActiveElement(getByText('Tab 1')) - }) + await press(Keys.Tab) - it('should not change the Tab if the defaultIndex changes', async () => { - function Example() { - let [defaultIndex, setDefaultIndex] = useState(1) + assertTabs({ active: 0 }) + assertActiveElement(getByText('Tab 1')) + }) + ) - return ( + it( + 'should jump to the nearest tab when the defaultIndex is out of bounds (+5)', + suppressConsoleLogs(async () => { + render( <> - + Tab 1 Tab 2 @@ -476,53 +415,60 @@ describe('Rendering', () => { - ) - } - - render() - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) + await press(Keys.Tab) - assertTabs({ active: 1 }) - assertActiveElement(getByText('Tab 2')) + assertTabs({ active: 2 }) + assertActiveElement(getByText('Tab 3')) + }) + ) - await click(getByText('Tab 3')) + it( + 'should jump to the next available tab when the defaultIndex is a disabled tab', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - assertTabs({ active: 2 }) - assertActiveElement(getByText('Tab 3')) + + Content 1 + Content 2 + Content 3 + + - // Change default index - await click(getByText('change')) + + + ) - // Nothing should change... - assertTabs({ active: 2 }) - }) - }) + assertActiveElement(document.body) - describe('`selectedIndex`', () => { - it('should be possible to change active tab controlled and uncontrolled', async () => { - let handleChange = jest.fn() + await press(Keys.Tab) - function ControlledTabs() { - let [selectedIndex, setSelectedIndex] = useState(0) + assertTabs({ active: 1 }) + assertActiveElement(getByText('Tab 2')) + }) + ) - return ( + it( + 'should jump to the next available tab when the defaultIndex is a disabled tab and wrap around', + suppressConsoleLogs(async () => { + render( <> - { - setSelectedIndex(value) - handleChange(value) - }} - > + Tab 1 Tab 2 - Tab 3 + Tab 3 @@ -533,1488 +479,1793 @@ describe('Rendering', () => { - ) - } - render() + assertActiveElement(document.body) - assertActiveElement(document.body) + await press(Keys.Tab) - // test uncontrolled behaviour - await click(getByText('Tab 2')) - expect(handleChange).toHaveBeenCalledTimes(1) - expect(handleChange).toHaveBeenNthCalledWith(1, 1) - assertTabs({ active: 1 }) + assertTabs({ active: 0 }) + assertActiveElement(getByText('Tab 1')) + }) + ) - // test controlled behaviour - await click(getByText('setSelectedIndex')) - assertTabs({ active: 2 }) + it( + 'should not change the Tab if the defaultIndex changes', + suppressConsoleLogs(async () => { + function Example() { + let [defaultIndex, setDefaultIndex] = useState(1) - // test uncontrolled behaviour again - await click(getByText('Tab 2')) - expect(handleChange).toHaveBeenCalledTimes(2) - expect(handleChange).toHaveBeenNthCalledWith(2, 1) - assertTabs({ active: 1 }) - }) + return ( + <> + + + Tab 1 + Tab 2 + Tab 3 + + + + Content 1 + Content 2 + Content 3 + + + + + + + ) + } - it('should jump to the nearest tab when the selectedIndex is out of bounds (-2)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + render() - - Content 1 - Content 2 - Content 3 - - + assertActiveElement(document.body) - - - ) + await press(Keys.Tab) - assertActiveElement(document.body) + assertTabs({ active: 1 }) + assertActiveElement(getByText('Tab 2')) - await press(Keys.Tab) + await click(getByText('Tab 3')) - assertTabs({ active: 0 }) - assertActiveElement(getByText('Tab 1')) - }) + assertTabs({ active: 2 }) + assertActiveElement(getByText('Tab 3')) - it('should jump to the nearest tab when the selectedIndex is out of bounds (+5)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + // Change default index + await click(getByText('change')) - - Content 1 - Content 2 - Content 3 - - + // Nothing should change... + assertTabs({ active: 2 }) + }) + ) + }) - - - ) + describe('`selectedIndex`', () => { + it( + 'should be possible to change active tab controlled and uncontrolled', + suppressConsoleLogs(async () => { + let handleChange = jest.fn() - assertActiveElement(document.body) + function ControlledTabs() { + let [selectedIndex, setSelectedIndex] = useState(0) - await press(Keys.Tab) + return ( + <> + { + setSelectedIndex(value) + handleChange(value) + }} + > + + Tab 1 + Tab 2 + Tab 3 + + + + Content 1 + Content 2 + Content 3 + + + + + + + ) + } - assertTabs({ active: 2 }) - assertActiveElement(getByText('Tab 3')) - }) + render() - it('should jump to the next available tab when the selectedIndex is a disabled tab', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + assertActiveElement(document.body) - - Content 1 - Content 2 - Content 3 - - + // test uncontrolled behaviour + await click(getByText('Tab 2')) + expect(handleChange).toHaveBeenCalledTimes(1) + expect(handleChange).toHaveBeenNthCalledWith(1, 1) + assertTabs({ active: 1 }) - - - ) + // test controlled behaviour + await click(getByText('setSelectedIndex')) + assertTabs({ active: 2 }) - assertActiveElement(document.body) + // test uncontrolled behaviour again + await click(getByText('Tab 2')) + expect(handleChange).toHaveBeenCalledTimes(2) + expect(handleChange).toHaveBeenNthCalledWith(2, 1) + assertTabs({ active: 1 }) + }) + ) - await press(Keys.Tab) + it( + 'should jump to the nearest tab when the selectedIndex is out of bounds (-2)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - assertTabs({ active: 1 }) - assertActiveElement(getByText('Tab 2')) - }) + + Content 1 + Content 2 + Content 3 + + - it('should jump to the next available tab when the selectedIndex is a disabled tab and wrap around', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + + + ) - - Content 1 - Content 2 - Content 3 - - + assertActiveElement(document.body) - - - ) + await press(Keys.Tab) - assertActiveElement(document.body) + assertTabs({ active: 0 }) + assertActiveElement(getByText('Tab 1')) + }) + ) - await press(Keys.Tab) + it( + 'should jump to the nearest tab when the selectedIndex is out of bounds (+5)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - assertTabs({ active: 0 }) - assertActiveElement(getByText('Tab 1')) - }) + + Content 1 + Content 2 + Content 3 + + - it('should prefer selectedIndex over defaultIndex', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + + + ) - - Content 1 - Content 2 - Content 3 - - + assertActiveElement(document.body) - - - ) + await press(Keys.Tab) - assertActiveElement(document.body) + assertTabs({ active: 2 }) + assertActiveElement(getByText('Tab 3')) + }) + ) - await press(Keys.Tab) + it( + 'should jump to the next available tab when the selectedIndex is a disabled tab', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - assertTabs({ active: 0 }) - assertActiveElement(getByText('Tab 1')) - }) - }) + + Content 1 + Content 2 + Content 3 + + - describe(`'Tab'`, () => { - describe('`type` attribute', () => { - it('should set the `type` to "button" by default', async () => { - render( - - - Trigger - - + + ) - expect(getTabs()[0]).toHaveAttribute('type', 'button') - }) + assertActiveElement(document.body) - it('should not set the `type` to "button" if it already contains a `type`', async () => { - render( - - - Trigger - - - ) + await press(Keys.Tab) - expect(getTabs()[0]).toHaveAttribute('type', 'submit') + assertTabs({ active: 1 }) + assertActiveElement(getByText('Tab 2')) }) + ) - it('should set the `type` to "button" when using the `as` prop which resolves to a "button"', async () => { - let CustomButton = React.forwardRef((props, ref) => ( - + ) - expect(getTabs()[0]).toHaveAttribute('type', 'button') + assertActiveElement(document.body) + + await press(Keys.Tab) + + assertTabs({ active: 0 }) + assertActiveElement(getByText('Tab 1')) }) + ) - it('should not set the type if the "as" prop is not a "button"', async () => { + it( + 'should prefer selectedIndex over defaultIndex', + suppressConsoleLogs(async () => { render( - - - Trigger - - + <> + + + Tab 1 + Tab 2 + Tab 3 + + + + Content 1 + Content 2 + Content 3 + + + + + ) - expect(getTabs()[0]).not.toHaveAttribute('type') + assertActiveElement(document.body) + + await press(Keys.Tab) + + assertTabs({ active: 0 }) + assertActiveElement(getByText('Tab 1')) }) + ) + }) - it('should not set the `type` to "button" when using the `as` prop which resolves to a "div"', async () => { - let CustomButton = React.forwardRef((props, ref) => ( -
- )) + describe(`'Tab'`, () => { + describe('`type` attribute', () => { + it( + 'should set the `type` to "button" by default', + suppressConsoleLogs(async () => { + render( + + + Trigger + + + ) - render( - - - Trigger - - - ) + expect(getTabs()[0]).toHaveAttribute('type', 'button') + }) + ) - expect(getTabs()[0]).not.toHaveAttribute('type') - }) + it( + 'should not set the `type` to "button" if it already contains a `type`', + suppressConsoleLogs(async () => { + render( + + + Trigger + + + ) + + expect(getTabs()[0]).toHaveAttribute('type', 'submit') + }) + ) + + it( + 'should set the `type` to "button" when using the `as` prop which resolves to a "button"', + suppressConsoleLogs(async () => { + let CustomButton = React.forwardRef((props, ref) => ( + - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) + await press(Keys.Tab) - assertTabs({ active: 0 }) - assertActiveElement(getByText('Tab 1')) + assertTabs({ active: 0 }) + assertActiveElement(getByText('Tab 1')) - await press(Keys.Tab) - assertActiveElement(getByText('Content 1')) + await press(Keys.Tab) + assertActiveElement(getByText('Content 1')) - await press(Keys.Tab) - assertActiveElement(getByText('after')) + await press(Keys.Tab) + assertActiveElement(getByText('after')) - await press(shift(Keys.Tab)) - assertActiveElement(getByText('Content 1')) + await press(shift(Keys.Tab)) + assertActiveElement(getByText('Content 1')) - await press(shift(Keys.Tab)) - assertActiveElement(getByText('Tab 1')) - }) + await press(shift(Keys.Tab)) + assertActiveElement(getByText('Tab 1')) + }) + ) - it('should be possible to tab to the default index tab', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should be possible to tab to the default index tab', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) + await press(Keys.Tab) - assertTabs({ active: 1 }) - assertActiveElement(getByText('Tab 2')) + assertTabs({ active: 1 }) + assertActiveElement(getByText('Tab 2')) - await press(Keys.Tab) - assertActiveElement(getByText('Content 2')) + await press(Keys.Tab) + assertActiveElement(getByText('Content 2')) - await press(Keys.Tab) - assertActiveElement(getByText('after')) + await press(Keys.Tab) + assertActiveElement(getByText('after')) - await press(shift(Keys.Tab)) - assertActiveElement(getByText('Content 2')) + await press(shift(Keys.Tab)) + assertActiveElement(getByText('Content 2')) - await press(shift(Keys.Tab)) - assertActiveElement(getByText('Tab 2')) - }) + await press(shift(Keys.Tab)) + assertActiveElement(getByText('Tab 2')) + }) + ) }) describe('`ArrowRight` key', () => { - it('should be possible to go to the next item (activation = `auto`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should be possible to go to the next item (activation = `auto`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 0 }) + await press(Keys.Tab) + assertTabs({ active: 0 }) - await press(Keys.ArrowRight) - assertTabs({ active: 1 }) + await press(Keys.ArrowRight) + assertTabs({ active: 1 }) - await press(Keys.ArrowRight) - assertTabs({ active: 2 }) - }) + await press(Keys.ArrowRight) + assertTabs({ active: 2 }) + }) + ) - it('should be possible to go to the next item (activation = `manual`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should be possible to go to the next item (activation = `manual`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 0 }) + await press(Keys.Tab) + assertTabs({ active: 0 }) - await press(Keys.ArrowRight) - assertTabs({ active: 0 }) - await press(Keys.Enter) - assertTabs({ active: 1 }) + await press(Keys.ArrowRight) + assertTabs({ active: 0 }) + await press(Keys.Enter) + assertTabs({ active: 1 }) - await press(Keys.ArrowRight) - assertTabs({ active: 1 }) - await press(Keys.Enter) - assertTabs({ active: 2 }) - }) + await press(Keys.ArrowRight) + assertTabs({ active: 1 }) + await press(Keys.Enter) + assertTabs({ active: 2 }) + }) + ) - it('should wrap around at the end (activation = `auto`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should wrap around at the end (activation = `auto`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 0 }) + await press(Keys.Tab) + assertTabs({ active: 0 }) - await press(Keys.ArrowRight) - assertTabs({ active: 1 }) + await press(Keys.ArrowRight) + assertTabs({ active: 1 }) - await press(Keys.ArrowRight) - assertTabs({ active: 2 }) + await press(Keys.ArrowRight) + assertTabs({ active: 2 }) - await press(Keys.ArrowRight) - assertTabs({ active: 0 }) + await press(Keys.ArrowRight) + assertTabs({ active: 0 }) - await press(Keys.ArrowRight) - assertTabs({ active: 1 }) - }) + await press(Keys.ArrowRight) + assertTabs({ active: 1 }) + }) + ) - it('should wrap around at the end (activation = `manual`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should wrap around at the end (activation = `manual`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 0 }) + await press(Keys.Tab) + assertTabs({ active: 0 }) - await press(Keys.ArrowRight) - assertTabs({ active: 0 }) - await press(Keys.Enter) - assertTabs({ active: 1 }) + await press(Keys.ArrowRight) + assertTabs({ active: 0 }) + await press(Keys.Enter) + assertTabs({ active: 1 }) - await press(Keys.ArrowRight) - assertTabs({ active: 1 }) - await press(Keys.Enter) - assertTabs({ active: 2 }) + await press(Keys.ArrowRight) + assertTabs({ active: 1 }) + await press(Keys.Enter) + assertTabs({ active: 2 }) - await press(Keys.ArrowRight) - assertTabs({ active: 2 }) - await press(Keys.Enter) - assertTabs({ active: 0 }) + await press(Keys.ArrowRight) + assertTabs({ active: 2 }) + await press(Keys.Enter) + assertTabs({ active: 0 }) - await press(Keys.ArrowRight) - assertTabs({ active: 0 }) - await press(Keys.Enter) - assertTabs({ active: 1 }) - }) + await press(Keys.ArrowRight) + assertTabs({ active: 0 }) + await press(Keys.Enter) + assertTabs({ active: 1 }) + }) + ) - it('should not be possible to go right when in vertical mode (activation = `auto`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should not be possible to go right when in vertical mode (activation = `auto`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 0, orientation: 'vertical' }) + await press(Keys.Tab) + assertTabs({ active: 0, orientation: 'vertical' }) - await press(Keys.ArrowRight) - // no-op - assertTabs({ active: 0, orientation: 'vertical' }) - }) + await press(Keys.ArrowRight) + // no-op + assertTabs({ active: 0, orientation: 'vertical' }) + }) + ) - it('should not be possible to go right when in vertical mode (activation = `manual`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should not be possible to go right when in vertical mode (activation = `manual`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 0, orientation: 'vertical' }) + await press(Keys.Tab) + assertTabs({ active: 0, orientation: 'vertical' }) - await press(Keys.ArrowRight) - assertTabs({ active: 0, orientation: 'vertical' }) - await press(Keys.Enter) - // no-op - assertTabs({ active: 0, orientation: 'vertical' }) - }) + await press(Keys.ArrowRight) + assertTabs({ active: 0, orientation: 'vertical' }) + await press(Keys.Enter) + // no-op + assertTabs({ active: 0, orientation: 'vertical' }) + }) + ) }) describe('`ArrowLeft` key', () => { - it('should be possible to go to the previous item (activation = `auto`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should be possible to go to the previous item (activation = `auto`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 2 }) + await press(Keys.Tab) + assertTabs({ active: 2 }) - await press(Keys.ArrowLeft) - assertTabs({ active: 1 }) + await press(Keys.ArrowLeft) + assertTabs({ active: 1 }) - await press(Keys.ArrowLeft) - assertTabs({ active: 0 }) - }) + await press(Keys.ArrowLeft) + assertTabs({ active: 0 }) + }) + ) - it('should be possible to go to the previous item (activation = `manual`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should be possible to go to the previous item (activation = `manual`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 2 }) + await press(Keys.Tab) + assertTabs({ active: 2 }) - await press(Keys.ArrowLeft) - assertTabs({ active: 2 }) - await press(Keys.Enter) - assertTabs({ active: 1 }) + await press(Keys.ArrowLeft) + assertTabs({ active: 2 }) + await press(Keys.Enter) + assertTabs({ active: 1 }) - await press(Keys.ArrowLeft) - assertTabs({ active: 1 }) - await press(Keys.Enter) - assertTabs({ active: 0 }) - }) + await press(Keys.ArrowLeft) + assertTabs({ active: 1 }) + await press(Keys.Enter) + assertTabs({ active: 0 }) + }) + ) - it('should wrap around at the beginning (activation = `auto`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should wrap around at the beginning (activation = `auto`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 2 }) + await press(Keys.Tab) + assertTabs({ active: 2 }) - await press(Keys.ArrowLeft) - assertTabs({ active: 1 }) + await press(Keys.ArrowLeft) + assertTabs({ active: 1 }) - await press(Keys.ArrowLeft) - assertTabs({ active: 0 }) + await press(Keys.ArrowLeft) + assertTabs({ active: 0 }) - await press(Keys.ArrowLeft) - assertTabs({ active: 2 }) + await press(Keys.ArrowLeft) + assertTabs({ active: 2 }) - await press(Keys.ArrowLeft) - assertTabs({ active: 1 }) - }) + await press(Keys.ArrowLeft) + assertTabs({ active: 1 }) + }) + ) - it('should wrap around at the beginning (activation = `manual`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should wrap around at the beginning (activation = `manual`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 2 }) + await press(Keys.Tab) + assertTabs({ active: 2 }) - await press(Keys.ArrowLeft) - assertTabs({ active: 2 }) - await press(Keys.Enter) - assertTabs({ active: 1 }) + await press(Keys.ArrowLeft) + assertTabs({ active: 2 }) + await press(Keys.Enter) + assertTabs({ active: 1 }) - await press(Keys.ArrowLeft) - assertTabs({ active: 1 }) - await press(Keys.Enter) - assertTabs({ active: 0 }) + await press(Keys.ArrowLeft) + assertTabs({ active: 1 }) + await press(Keys.Enter) + assertTabs({ active: 0 }) - await press(Keys.ArrowLeft) - assertTabs({ active: 0 }) - await press(Keys.Enter) - assertTabs({ active: 2 }) + await press(Keys.ArrowLeft) + assertTabs({ active: 0 }) + await press(Keys.Enter) + assertTabs({ active: 2 }) - await press(Keys.ArrowLeft) - assertTabs({ active: 2 }) - await press(Keys.Enter) - assertTabs({ active: 1 }) - }) + await press(Keys.ArrowLeft) + assertTabs({ active: 2 }) + await press(Keys.Enter) + assertTabs({ active: 1 }) + }) + ) - it('should not be possible to go left when in vertical mode (activation = `auto`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should not be possible to go left when in vertical mode (activation = `auto`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 0, orientation: 'vertical' }) + await press(Keys.Tab) + assertTabs({ active: 0, orientation: 'vertical' }) - await press(Keys.ArrowLeft) - // no-op - assertTabs({ active: 0, orientation: 'vertical' }) - }) + await press(Keys.ArrowLeft) + // no-op + assertTabs({ active: 0, orientation: 'vertical' }) + }) + ) - it('should not be possible to go left when in vertical mode (activation = `manual`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should not be possible to go left when in vertical mode (activation = `manual`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 0, orientation: 'vertical' }) + await press(Keys.Tab) + assertTabs({ active: 0, orientation: 'vertical' }) - await press(Keys.ArrowLeft) - assertTabs({ active: 0, orientation: 'vertical' }) - await press(Keys.Enter) + await press(Keys.ArrowLeft) + assertTabs({ active: 0, orientation: 'vertical' }) + await press(Keys.Enter) - // no-op - assertTabs({ active: 0, orientation: 'vertical' }) - }) + // no-op + assertTabs({ active: 0, orientation: 'vertical' }) + }) + ) }) describe('`ArrowDown` key', () => { - it('should be possible to go to the next item (activation = `auto`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - - - - Content 1 - Content 2 - Content 3 - - + it( + 'should be possible to go to the next item (activation = `auto`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - - ) + + Content 1 + Content 2 + Content 3 + + - assertActiveElement(document.body) + + + ) - await press(Keys.Tab) - assertTabs({ active: 0, orientation: 'vertical' }) + assertActiveElement(document.body) - await press(Keys.ArrowDown) - assertTabs({ active: 1, orientation: 'vertical' }) + await press(Keys.Tab) + assertTabs({ active: 0, orientation: 'vertical' }) - await press(Keys.ArrowDown) - assertTabs({ active: 2, orientation: 'vertical' }) - }) + await press(Keys.ArrowDown) + assertTabs({ active: 1, orientation: 'vertical' }) - it('should be possible to go to the next item (activation = `manual`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + await press(Keys.ArrowDown) + assertTabs({ active: 2, orientation: 'vertical' }) + }) + ) - - Content 1 - Content 2 - Content 3 - - + it( + 'should be possible to go to the next item (activation = `manual`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - - ) + + Content 1 + Content 2 + Content 3 + + - assertActiveElement(document.body) + + + ) - await press(Keys.Tab) - assertTabs({ active: 0, orientation: 'vertical' }) + assertActiveElement(document.body) - await press(Keys.ArrowDown) - assertTabs({ active: 0, orientation: 'vertical' }) - await press(Keys.Enter) - assertTabs({ active: 1, orientation: 'vertical' }) + await press(Keys.Tab) + assertTabs({ active: 0, orientation: 'vertical' }) - await press(Keys.ArrowDown) - assertTabs({ active: 1, orientation: 'vertical' }) - await press(Keys.Enter) - assertTabs({ active: 2, orientation: 'vertical' }) - }) + await press(Keys.ArrowDown) + assertTabs({ active: 0, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 1, orientation: 'vertical' }) - it('should wrap around at the end (activation = `auto`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + await press(Keys.ArrowDown) + assertTabs({ active: 1, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 2, orientation: 'vertical' }) + }) + ) - - Content 1 - Content 2 - Content 3 - - + it( + 'should wrap around at the end (activation = `auto`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - - ) + + Content 1 + Content 2 + Content 3 + + - assertActiveElement(document.body) + + + ) - await press(Keys.Tab) - assertTabs({ active: 0, orientation: 'vertical' }) + assertActiveElement(document.body) - await press(Keys.ArrowDown) - assertTabs({ active: 1, orientation: 'vertical' }) + await press(Keys.Tab) + assertTabs({ active: 0, orientation: 'vertical' }) - await press(Keys.ArrowDown) - assertTabs({ active: 2, orientation: 'vertical' }) + await press(Keys.ArrowDown) + assertTabs({ active: 1, orientation: 'vertical' }) - await press(Keys.ArrowDown) - assertTabs({ active: 0, orientation: 'vertical' }) + await press(Keys.ArrowDown) + assertTabs({ active: 2, orientation: 'vertical' }) - await press(Keys.ArrowDown) - assertTabs({ active: 1, orientation: 'vertical' }) - }) + await press(Keys.ArrowDown) + assertTabs({ active: 0, orientation: 'vertical' }) - it('should wrap around at the end (activation = `manual`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + await press(Keys.ArrowDown) + assertTabs({ active: 1, orientation: 'vertical' }) + }) + ) - - Content 1 - Content 2 - Content 3 - - + it( + 'should wrap around at the end (activation = `manual`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - - ) + + Content 1 + Content 2 + Content 3 + + - assertActiveElement(document.body) + + + ) - await press(Keys.Tab) - assertTabs({ active: 0, orientation: 'vertical' }) - - await press(Keys.ArrowDown) - assertTabs({ active: 0, orientation: 'vertical' }) - await press(Keys.Enter) - assertTabs({ active: 1, orientation: 'vertical' }) - - await press(Keys.ArrowDown) - assertTabs({ active: 1, orientation: 'vertical' }) - await press(Keys.Enter) - assertTabs({ active: 2, orientation: 'vertical' }) - - await press(Keys.ArrowDown) - assertTabs({ active: 2, orientation: 'vertical' }) - await press(Keys.Enter) - assertTabs({ active: 0, orientation: 'vertical' }) - - await press(Keys.ArrowDown) - assertTabs({ active: 0, orientation: 'vertical' }) - await press(Keys.Enter) - assertTabs({ active: 1, orientation: 'vertical' }) - }) + assertActiveElement(document.body) - it('should not be possible to go down when in horizontal mode (activation = `auto`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + await press(Keys.Tab) + assertTabs({ active: 0, orientation: 'vertical' }) - - Content 1 - Content 2 - Content 3 - - + await press(Keys.ArrowDown) + assertTabs({ active: 0, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 1, orientation: 'vertical' }) - - - ) + await press(Keys.ArrowDown) + assertTabs({ active: 1, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 2, orientation: 'vertical' }) - assertActiveElement(document.body) + await press(Keys.ArrowDown) + assertTabs({ active: 2, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 0, orientation: 'vertical' }) - await press(Keys.Tab) - assertTabs({ active: 0 }) + await press(Keys.ArrowDown) + assertTabs({ active: 0, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 1, orientation: 'vertical' }) + }) + ) - await press(Keys.ArrowDown) - // no-op - assertTabs({ active: 0 }) - }) + it( + 'should not be possible to go down when in horizontal mode (activation = `auto`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - it('should not be possible to go down when in horizontal mode (activation = `manual`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + + Content 1 + Content 2 + Content 3 + + - - Content 1 - Content 2 - Content 3 - - + + + ) - - - ) + assertActiveElement(document.body) - assertActiveElement(document.body) + await press(Keys.Tab) + assertTabs({ active: 0 }) - await press(Keys.Tab) - assertTabs({ active: 0 }) + await press(Keys.ArrowDown) + // no-op + assertTabs({ active: 0 }) + }) + ) - await press(Keys.ArrowDown) - assertTabs({ active: 0 }) - await press(Keys.Enter) + it( + 'should not be possible to go down when in horizontal mode (activation = `manual`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - // no-op - assertTabs({ active: 0 }) - }) - }) + + Content 1 + Content 2 + Content 3 + + - describe('`ArrowUp` key', () => { - it('should be possible to go to the previous item (activation = `auto`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + + + ) - - Content 1 - Content 2 - Content 3 - - + assertActiveElement(document.body) - - - ) + await press(Keys.Tab) + assertTabs({ active: 0 }) - assertActiveElement(document.body) + await press(Keys.ArrowDown) + assertTabs({ active: 0 }) + await press(Keys.Enter) - await press(Keys.Tab) - assertTabs({ active: 2, orientation: 'vertical' }) + // no-op + assertTabs({ active: 0 }) + }) + ) + }) - await press(Keys.ArrowUp) - assertTabs({ active: 1, orientation: 'vertical' }) + describe('`ArrowUp` key', () => { + it( + 'should be possible to go to the previous item (activation = `auto`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - await press(Keys.ArrowUp) - assertTabs({ active: 0, orientation: 'vertical' }) - }) + + Content 1 + Content 2 + Content 3 + + - it('should be possible to go to the previous item (activation = `manual`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + + + ) - - Content 1 - Content 2 - Content 3 - - + assertActiveElement(document.body) - - - ) + await press(Keys.Tab) + assertTabs({ active: 2, orientation: 'vertical' }) - assertActiveElement(document.body) + await press(Keys.ArrowUp) + assertTabs({ active: 1, orientation: 'vertical' }) - await press(Keys.Tab) - assertTabs({ active: 2, orientation: 'vertical' }) + await press(Keys.ArrowUp) + assertTabs({ active: 0, orientation: 'vertical' }) + }) + ) - await press(Keys.ArrowUp) - assertTabs({ active: 2, orientation: 'vertical' }) - await press(Keys.Enter) - assertTabs({ active: 1, orientation: 'vertical' }) + it( + 'should be possible to go to the previous item (activation = `manual`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - await press(Keys.ArrowUp) - assertTabs({ active: 1, orientation: 'vertical' }) - await press(Keys.Enter) - assertTabs({ active: 0, orientation: 'vertical' }) - }) + + Content 1 + Content 2 + Content 3 + + - it('should wrap around at the beginning (activation = `auto`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + + + ) - - Content 1 - Content 2 - Content 3 - - + assertActiveElement(document.body) - - - ) + await press(Keys.Tab) + assertTabs({ active: 2, orientation: 'vertical' }) - assertActiveElement(document.body) + await press(Keys.ArrowUp) + assertTabs({ active: 2, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 1, orientation: 'vertical' }) - await press(Keys.Tab) - assertTabs({ active: 2, orientation: 'vertical' }) + await press(Keys.ArrowUp) + assertTabs({ active: 1, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 0, orientation: 'vertical' }) + }) + ) - await press(Keys.ArrowUp) - assertTabs({ active: 1, orientation: 'vertical' }) + it( + 'should wrap around at the beginning (activation = `auto`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - await press(Keys.ArrowUp) - assertTabs({ active: 0, orientation: 'vertical' }) + + Content 1 + Content 2 + Content 3 + + - await press(Keys.ArrowUp) - assertTabs({ active: 2, orientation: 'vertical' }) + + + ) - await press(Keys.ArrowUp) - assertTabs({ active: 1, orientation: 'vertical' }) - }) + assertActiveElement(document.body) - it('should wrap around at the beginning (activation = `manual`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + await press(Keys.Tab) + assertTabs({ active: 2, orientation: 'vertical' }) - - Content 1 - Content 2 - Content 3 - - + await press(Keys.ArrowUp) + assertTabs({ active: 1, orientation: 'vertical' }) - - - ) + await press(Keys.ArrowUp) + assertTabs({ active: 0, orientation: 'vertical' }) - assertActiveElement(document.body) + await press(Keys.ArrowUp) + assertTabs({ active: 2, orientation: 'vertical' }) - await press(Keys.Tab) - assertTabs({ active: 2, orientation: 'vertical' }) - - await press(Keys.ArrowUp) - assertTabs({ active: 2, orientation: 'vertical' }) - await press(Keys.Enter) - assertTabs({ active: 1, orientation: 'vertical' }) - - await press(Keys.ArrowUp) - assertTabs({ active: 1, orientation: 'vertical' }) - await press(Keys.Enter) - assertTabs({ active: 0, orientation: 'vertical' }) - - await press(Keys.ArrowUp) - assertTabs({ active: 0, orientation: 'vertical' }) - await press(Keys.Enter) - assertTabs({ active: 2, orientation: 'vertical' }) - - await press(Keys.ArrowUp) - assertTabs({ active: 2, orientation: 'vertical' }) - await press(Keys.Enter) - assertTabs({ active: 1, orientation: 'vertical' }) - }) + await press(Keys.ArrowUp) + assertTabs({ active: 1, orientation: 'vertical' }) + }) + ) - it('should not be possible to go left when in vertical mode (activation = `auto`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should wrap around at the beginning (activation = `manual`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 0 }) + await press(Keys.Tab) + assertTabs({ active: 2, orientation: 'vertical' }) - await press(Keys.ArrowUp) - // no-op - assertTabs({ active: 0 }) - }) + await press(Keys.ArrowUp) + assertTabs({ active: 2, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 1, orientation: 'vertical' }) - it('should not be possible to go left when in vertical mode (activation = `manual`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + await press(Keys.ArrowUp) + assertTabs({ active: 1, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 0, orientation: 'vertical' }) - - Content 1 - Content 2 - Content 3 - - + await press(Keys.ArrowUp) + assertTabs({ active: 0, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 2, orientation: 'vertical' }) - - - ) + await press(Keys.ArrowUp) + assertTabs({ active: 2, orientation: 'vertical' }) + await press(Keys.Enter) + assertTabs({ active: 1, orientation: 'vertical' }) + }) + ) - assertActiveElement(document.body) + it( + 'should not be possible to go left when in vertical mode (activation = `auto`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - await press(Keys.Tab) - assertTabs({ active: 0 }) + + Content 1 + Content 2 + Content 3 + + - await press(Keys.ArrowUp) - assertTabs({ active: 0 }) - await press(Keys.Enter) + + + ) - // no-op - assertTabs({ active: 0 }) - }) + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0 }) + + await press(Keys.ArrowUp) + // no-op + assertTabs({ active: 0 }) + }) + ) + + it( + 'should not be possible to go left when in vertical mode (activation = `manual`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + + + + Content 1 + Content 2 + Content 3 + + + + + + ) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 0 }) + + await press(Keys.ArrowUp) + assertTabs({ active: 0 }) + await press(Keys.Enter) + + // no-op + assertTabs({ active: 0 }) + }) + ) }) describe('`Home` key', () => { - it('should be possible to go to the first focusable item (activation = `auto`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should be possible to go to the first focusable item (activation = `auto`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 1 }) + await press(Keys.Tab) + assertTabs({ active: 1 }) - await press(Keys.Home) - assertTabs({ active: 0 }) - }) + await press(Keys.Home) + assertTabs({ active: 0 }) + }) + ) - it('should be possible to go to the first focusable item (activation = `manual`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should be possible to go to the first focusable item (activation = `manual`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 1 }) + await press(Keys.Tab) + assertTabs({ active: 1 }) - await press(Keys.Home) - assertTabs({ active: 1 }) - await press(Keys.Enter) - assertTabs({ active: 0 }) - }) + await press(Keys.Home) + assertTabs({ active: 1 }) + await press(Keys.Enter) + assertTabs({ active: 0 }) + }) + ) }) describe('`PageUp` key', () => { - it('should be possible to go to the first focusable item (activation = `auto`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should be possible to go to the first focusable item (activation = `auto`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 1 }) + await press(Keys.Tab) + assertTabs({ active: 1 }) - await press(Keys.PageUp) - assertTabs({ active: 0 }) - }) + await press(Keys.PageUp) + assertTabs({ active: 0 }) + }) + ) - it('should be possible to go to the first focusable item (activation = `manual`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should be possible to go to the first focusable item (activation = `manual`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + + + + Content 1 + Content 2 + Content 3 + + + + + + ) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 1 }) + + await press(Keys.PageUp) + assertTabs({ active: 1 }) + await press(Keys.Enter) + assertTabs({ active: 0 }) + }) + ) + }) + + describe('`End` key', () => { + it( + 'should be possible to go to the first focusable item (activation = `auto`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + + + + Content 1 + Content 2 + Content 3 + + + + + + ) + + assertActiveElement(document.body) + + await press(Keys.Tab) + assertTabs({ active: 1 }) + + await press(Keys.End) + assertTabs({ active: 2 }) + }) + ) + + it( + 'should be possible to go to the first focusable item (activation = `manual`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 1 }) + await press(Keys.Tab) + assertTabs({ active: 1 }) - await press(Keys.PageUp) - assertTabs({ active: 1 }) - await press(Keys.Enter) - assertTabs({ active: 0 }) - }) + await press(Keys.End) + assertTabs({ active: 1 }) + await press(Keys.Enter) + assertTabs({ active: 2 }) + }) + ) }) - describe('`End` key', () => { - it('should be possible to go to the first focusable item (activation = `auto`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + describe('`PageDown` key', () => { + it( + 'should be possible to go to the first focusable item (activation = `auto`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 1 }) + await press(Keys.Tab) + assertTabs({ active: 1 }) - await press(Keys.End) - assertTabs({ active: 2 }) - }) + await press(Keys.PageDown) + assertTabs({ active: 2 }) + }) + ) - it('should be possible to go to the first focusable item (activation = `manual`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + it( + 'should be possible to go to the first focusable item (activation = `manual`)', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 1 }) + await press(Keys.Tab) + assertTabs({ active: 1 }) - await press(Keys.End) - assertTabs({ active: 1 }) - await press(Keys.Enter) - assertTabs({ active: 2 }) - }) + await press(Keys.PageDown) + assertTabs({ active: 1 }) + await press(Keys.Enter) + assertTabs({ active: 2 }) + }) + ) }) - describe('`PageDown` key', () => { - it('should be possible to go to the first focusable item (activation = `auto`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + describe('`Enter` key', () => { + it( + 'should be possible to activate the focused tab', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - Content 1 - Content 2 - Content 3 - - + + Content 1 + Content 2 + Content 3 + + - - - ) + + + ) - assertActiveElement(document.body) + assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 1 }) + getByText('Tab 3')?.focus() - await press(Keys.PageDown) - assertTabs({ active: 2 }) - }) + assertActiveElement(getByText('Tab 3')) + assertTabs({ active: 0 }) - it('should be possible to go to the first focusable item (activation = `manual`)', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - + await press(Keys.Enter) + assertTabs({ active: 2 }) + }) + ) + }) - - Content 1 - Content 2 - Content 3 - - + describe('`Space` key', () => { + it( + 'should be possible to activate the focused tab', + suppressConsoleLogs(async () => { + render( + <> + + + Tab 1 + Tab 2 + Tab 3 + - - - ) + + Content 1 + Content 2 + Content 3 + + - assertActiveElement(document.body) + + + ) - await press(Keys.Tab) - assertTabs({ active: 1 }) + assertActiveElement(document.body) - await press(Keys.PageDown) - assertTabs({ active: 1 }) - await press(Keys.Enter) - assertTabs({ active: 2 }) - }) + getByText('Tab 3')?.focus() + + assertActiveElement(getByText('Tab 3')) + assertTabs({ active: 0 }) + + await press(Keys.Space) + assertTabs({ active: 2 }) + }) + ) }) +}) - describe('`Enter` key', () => { - it('should be possible to activate the focused tab', async () => { +describe('Mouse interactions', () => { + it( + 'should be possible to click on a tab to focus it', + suppressConsoleLogs(async () => { render( <> - + Tab 1 Tab 2 @@ -2033,24 +2284,28 @@ describe('Keyboard interactions', () => { ) assertActiveElement(document.body) + await press(Keys.Tab) + assertTabs({ active: 1 }) - getByText('Tab 3')?.focus() - - assertActiveElement(getByText('Tab 3')) + await click(getByText('Tab 1')) assertTabs({ active: 0 }) - await press(Keys.Enter) + await click(getByText('Tab 3')) assertTabs({ active: 2 }) + + await click(getByText('Tab 2')) + assertTabs({ active: 1 }) }) - }) + ) - describe('`Space` key', () => { - it('should be possible to activate the focused tab', async () => { + it( + 'should be a no-op when clicking on a disabled tab', + suppressConsoleLogs(async () => { render( <> - + - Tab 1 + Tab 1 Tab 2 Tab 3 @@ -2067,23 +2322,24 @@ describe('Keyboard interactions', () => { ) assertActiveElement(document.body) + await press(Keys.Tab) + assertTabs({ active: 1 }) - getByText('Tab 3')?.focus() - - assertActiveElement(getByText('Tab 3')) - assertTabs({ active: 0 }) - - await press(Keys.Space) - assertTabs({ active: 2 }) + await click(getByText('Tab 1')) + // No-op, Tab 2 is still active + assertTabs({ active: 1 }) }) - }) + ) }) -describe('Mouse interactions', () => { - it('should be possible to click on a tab to focus it', async () => { +it( + 'should trigger the `onChange` when the tab changes', + suppressConsoleLogs(async () => { + let changes = jest.fn() + render( <> - + Tab 1 Tab 2 @@ -2101,83 +2357,16 @@ describe('Mouse interactions', () => { ) - assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 1 }) - - await click(getByText('Tab 1')) - assertTabs({ active: 0 }) - + await click(getByText('Tab 2')) await click(getByText('Tab 3')) - assertTabs({ active: 2 }) - await click(getByText('Tab 2')) - assertTabs({ active: 1 }) - }) - - it('should be a no-op when clicking on a disabled tab', async () => { - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - - - - Content 1 - Content 2 - Content 3 - - - - - - ) - - assertActiveElement(document.body) - await press(Keys.Tab) - assertTabs({ active: 1 }) - await click(getByText('Tab 1')) - // No-op, Tab 2 is still active - assertTabs({ active: 1 }) - }) -}) - -it('should trigger the `onChange` when the tab changes', async () => { - let changes = jest.fn() - - render( - <> - - - Tab 1 - Tab 2 - Tab 3 - - - - Content 1 - Content 2 - Content 3 - - - - - - ) - - await click(getByText('Tab 2')) - await click(getByText('Tab 3')) - await click(getByText('Tab 2')) - await click(getByText('Tab 1')) - expect(changes).toHaveBeenCalledTimes(4) + expect(changes).toHaveBeenCalledTimes(4) - expect(changes).toHaveBeenNthCalledWith(1, 1) - expect(changes).toHaveBeenNthCalledWith(2, 2) - expect(changes).toHaveBeenNthCalledWith(3, 1) - expect(changes).toHaveBeenNthCalledWith(4, 0) -}) + expect(changes).toHaveBeenNthCalledWith(1, 1) + expect(changes).toHaveBeenNthCalledWith(2, 2) + expect(changes).toHaveBeenNthCalledWith(3, 1) + expect(changes).toHaveBeenNthCalledWith(4, 0) + }) +) diff --git a/packages/@headlessui-react/src/components/tabs/tabs.tsx b/packages/@headlessui-react/src/components/tabs/tabs.tsx index c5595325f5..10b249517d 100644 --- a/packages/@headlessui-react/src/components/tabs/tabs.tsx +++ b/packages/@headlessui-react/src/components/tabs/tabs.tsx @@ -28,6 +28,7 @@ import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect' import { useSyncRefs } from '../../hooks/use-sync-refs' import { useResolveButtonType } from '../../hooks/use-resolve-button-type' import { useLatestValue } from '../../hooks/use-latest-value' +import { FocusSentinel } from '../../internal/focus-sentinel' interface StateDefinition { selectedIndex: number | null @@ -160,6 +161,7 @@ let Tabs = forwardRefWithAs(function Tabs ({ selectedIndex: state.selectedIndex }), [state.selectedIndex]) let onChangeRef = useLatestValue(onChange || (() => {})) + let stableTabsRef = useLatestValue(state.tabs) useEffect(() => { dispatch({ type: ActionTypes.SetOrientation, orientation }) @@ -169,7 +171,7 @@ let Tabs = forwardRefWithAs(function Tabs { + useIsoMorphicEffect(() => { if (state.tabs.length <= 0) return if (selectedIndex === null && state.selectedIndex !== null) return @@ -229,6 +231,18 @@ let Tabs = forwardRefWithAs(function Tabs + { + for (let tab of stableTabsRef.current) { + if (tab.current?.tabIndex === 0) { + tab.current?.focus() + return true + } + } + + return false + }} + /> {render({ props: { ref: tabsRef, ...passThroughProps }, slot, diff --git a/packages/@headlessui-react/src/internal/focus-sentinel.tsx b/packages/@headlessui-react/src/internal/focus-sentinel.tsx new file mode 100644 index 0000000000..06659cbb2a --- /dev/null +++ b/packages/@headlessui-react/src/internal/focus-sentinel.tsx @@ -0,0 +1,46 @@ +import React, { useState, FocusEvent as ReactFocusEvent } from 'react' + +import { VisuallyHidden } from './visually-hidden' + +interface FocusSentinelProps { + onFocus(): boolean +} + +export function FocusSentinel({ onFocus }: FocusSentinelProps) { + let [enabled, setEnabled] = useState(true) + + if (!enabled) return null + + return ( + { + event.preventDefault() + let frame: ReturnType + + let tries = 50 + function forwardFocus() { + // Prevent infinite loops + if (tries-- <= 0) { + if (frame) cancelAnimationFrame(frame) + return + } + + // Try to move focus to the correct element. This depends on the implementation + // of `onFocus` of course since it would be different for each place we use it in. + if (onFocus()) { + setEnabled(false) + cancelAnimationFrame(frame) + return + } + + // Retry + frame = requestAnimationFrame(forwardFocus) + } + + frame = requestAnimationFrame(forwardFocus) + }} + /> + ) +} diff --git a/packages/@headlessui-vue/src/components/tabs/tabs.ts b/packages/@headlessui-vue/src/components/tabs/tabs.ts index 1ce70cc16a..7d940a8c60 100644 --- a/packages/@headlessui-vue/src/components/tabs/tabs.ts +++ b/packages/@headlessui-vue/src/components/tabs/tabs.ts @@ -1,14 +1,18 @@ import { + Fragment, + computed, defineComponent, - ref, - provide, + h, inject, onMounted, onUnmounted, - computed, + provide, + ref, + watchEffect, + + // Types InjectionKey, Ref, - watchEffect, } from 'vue' import { Features, render, omit } from '../../utils/render' @@ -18,6 +22,7 @@ import { dom } from '../../utils/dom' import { match } from '../../utils/match' import { focusIn, Focus } from '../../utils/focus-management' import { useResolveButtonType } from '../../hooks/use-resolve-button-type' +import { FocusSentinel } from '../../internal/focus-sentinel' type StateDefinition = { // State @@ -132,13 +137,28 @@ export let TabGroup = defineComponent({ return () => { let slot = { selectedIndex: selectedIndex.value } - return render({ - props: omit(props, ['selectedIndex', 'defaultIndex', 'manual', 'vertical', 'onChange']), - slot, - slots, - attrs, - name: 'TabGroup', - }) + return h(Fragment, [ + h(FocusSentinel, { + onFocus: () => { + for (let tab of tabs.value) { + let el = dom(tab) + if (el?.tabIndex === 0) { + el.focus() + return true + } + } + + return false + }, + }), + render({ + props: omit(props, ['selectedIndex', 'defaultIndex', 'manual', 'vertical', 'onChange']), + slot, + slots, + attrs, + name: 'TabGroup', + }), + ]) } }, }) diff --git a/packages/@headlessui-vue/src/internal/focus-sentinel.ts b/packages/@headlessui-vue/src/internal/focus-sentinel.ts new file mode 100644 index 0000000000..c574861958 --- /dev/null +++ b/packages/@headlessui-vue/src/internal/focus-sentinel.ts @@ -0,0 +1,50 @@ +import { h, ref, defineComponent } from 'vue' + +import { VisuallyHidden } from './visually-hidden' + +export let FocusSentinel = defineComponent({ + props: { + onFocus: { + type: Function, + required: true, + }, + }, + setup(props) { + let enabled = ref(true) + + return () => { + if (!enabled.value) return null + + return h(VisuallyHidden, { + as: 'button', + type: 'button', + onFocus(event: FocusEvent) { + event.preventDefault() + let frame: ReturnType + + let tries = 50 + function forwardFocus() { + // Prevent infinite loops + if (tries-- <= 0) { + if (frame) cancelAnimationFrame(frame) + return + } + + // Try to move focus to the correct element. This depends on the implementation + // of `onFocus` of course since it would be different for each place we use it in. + if (props.onFocus()) { + enabled.value = false + cancelAnimationFrame(frame) + return + } + + // Retry + frame = requestAnimationFrame(forwardFocus) + } + + frame = requestAnimationFrame(forwardFocus) + }, + }) + } + }, +}) diff --git a/packages/playground-react/pages/combinations/tabs-in-dialog.tsx b/packages/playground-react/pages/combinations/tabs-in-dialog.tsx new file mode 100644 index 0000000000..bca77d568c --- /dev/null +++ b/packages/playground-react/pages/combinations/tabs-in-dialog.tsx @@ -0,0 +1,31 @@ +import { useState } from 'react' +import { Dialog, Tab } from '@headlessui/react' + +export default function App() { + let [open, setOpen] = useState(false) + + return ( + <> + + + +
+
+ + + Tab 1 + Tab 2 + Tab 3 + + + Panel 1 + Panel 2 + Panel 3 + + +
+
+
+ + ) +} diff --git a/packages/playground-vue/src/components/combinations/tabs-in-dialog.vue b/packages/playground-vue/src/components/combinations/tabs-in-dialog.vue new file mode 100644 index 0000000000..ccab3dbf80 --- /dev/null +++ b/packages/playground-vue/src/components/combinations/tabs-in-dialog.vue @@ -0,0 +1,31 @@ + + + diff --git a/packages/playground-vue/src/routes.json b/packages/playground-vue/src/routes.json index e7c841e1fb..6beebe9bf8 100644 --- a/packages/playground-vue/src/routes.json +++ b/packages/playground-vue/src/routes.json @@ -3,6 +3,17 @@ "path": "/", "component": "./components/Home.vue" }, + { + "name": "Combinations", + "path": "/combinations", + "children": [ + { + "name": "Tabs in Dialog", + "path": "/combinations/tabs-in-dialog", + "component": "./components/combinations/tabs-in-dialog.vue" + } + ] + }, { "name": "Combobox", "path": "/combobox",