import React, { createElement, useState } from 'react' import { render } from '@testing-library/react' import { Tab } from './tabs' import { Dialog } from '../dialog/dialog' import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs' import { assertTabs, assertActiveElement, getByText, getTabs, } from '../../test-utils/accessibility-assertions' import { press, Keys, shift, click } from '../../test-utils/interactions' jest.mock('../../hooks/use-id') beforeAll(() => { jest.spyOn(window, 'requestAnimationFrame').mockImplementation(setImmediate as any) jest.spyOn(window, 'cancelAnimationFrame').mockImplementation(clearImmediate as any) }) describe('safeguards', () => { it.each([ ['Tab.List', Tab.List], ['Tab', Tab], ['Tab.Panels', Tab.Panels], ['Tab.Panel', Tab.Panel], ])( 'should error when we are using a <%s /> without a parent component', suppressConsoleLogs((name, Component) => { expect(() => render(createElement(Component))).toThrowError( `<${name} /> is missing a parent component.` ) }) ) it( 'should be possible to render Tab.Group without crashing', suppressConsoleLogs(async () => { render( Tab 1 Tab 2 Tab 3 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', suppressConsoleLogs(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', suppressConsoleLogs(async () => { function Example() { let [hide, setHide] = useState(false) return ( <> Tab 1 {!hide && Tab 2} Tab 3 Content 1 {!hide && Content 2} Content 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 }) }) ) it( 'should guarantee the order when injecting new tabs dynamically', suppressConsoleLogs(async () => { function Example() { let [tabs, setTabs] = useState([]) return ( {tabs.map((t, i) => ( Tab {i + 1} ))} Insert new {tabs.map((t) => ( {t} ))} ) } render() assertTabs({ active: 0, tabContents: 'Insert new', panelContents: 'Insert' }) // Add some new tabs await click(getByText('Insert')) // We should still be on the tab we were on assertTabs({ active: 1, tabContents: 'Insert new', panelContents: 'Insert' }) }) ) it( 'should guarantee the order of DOM nodes when reversing the tabs and panels themselves, then performing actions (controlled component)', suppressConsoleLogs(async () => { function Example() { let [selectedIndex, setSelectedIndex] = useState(1) let [tabs, setTabs] = useState([0, 1, 2]) return ( <> {tabs.map((tab) => ( Tab {tab} ))} {tabs.map((tab) => ( Content {tab} ))}

{selectedIndex}

) } render() let selectedIndexElement = document.getElementById('selectedIndex') assertTabs({ active: 1 }) await click(getByText('Tab 0')) assertTabs({ active: 0 }) expect(selectedIndexElement).toHaveTextContent('0') await click(getByText('Tab 1')) assertTabs({ active: 1 }) expect(selectedIndexElement).toHaveTextContent('1') await click(getByText('Tab 2')) assertTabs({ active: 2 }) expect(selectedIndexElement).toHaveTextContent('2') await click(getByText('reverse')) // Note: the indices are reversed now await click(getByText('Tab 0')) assertTabs({ active: 2 }) expect(selectedIndexElement).toHaveTextContent('2') await click(getByText('Tab 1')) assertTabs({ active: 1 }) expect(selectedIndexElement).toHaveTextContent('1') await click(getByText('Tab 2')) assertTabs({ active: 0 }) expect(selectedIndexElement).toHaveTextContent('0') await click(getByText('reverse')) // Note: the indices are reversed again now (back to normal) await click(getByText('Tab 0')) assertTabs({ active: 0 }) expect(selectedIndexElement).toHaveTextContent('0') await click(getByText('Tab 1')) assertTabs({ active: 1 }) expect(selectedIndexElement).toHaveTextContent('1') await click(getByText('Tab 2')) assertTabs({ active: 2 }) expect(selectedIndexElement).toHaveTextContent('2') }) ) it( 'should guarantee the order of DOM nodes when reversing the tabs and panels themselves, then performing actions (uncontrolled component)', suppressConsoleLogs(async () => { function Example() { let [tabs, setTabs] = useState([0, 1, 2]) return ( <> {({ selectedIndex }) => ( <> {tabs.map((tab) => ( Tab {tab} ))} {tabs.map((tab) => ( Content {tab} ))}

{selectedIndex}

)}
) } render() let selectedIndexElement = document.getElementById('selectedIndex') await click(getByText('Tab 0')) assertTabs({ active: 0 }) expect(selectedIndexElement).toHaveTextContent('0') await click(getByText('Tab 1')) assertTabs({ active: 1 }) expect(selectedIndexElement).toHaveTextContent('1') await click(getByText('Tab 2')) assertTabs({ active: 2 }) expect(selectedIndexElement).toHaveTextContent('2') await click(getByText('reverse')) // Note: the indices are reversed now await click(getByText('Tab 0')) assertTabs({ active: 2 }) expect(selectedIndexElement).toHaveTextContent('2') await click(getByText('Tab 1')) assertTabs({ active: 1 }) expect(selectedIndexElement).toHaveTextContent('1') await click(getByText('Tab 2')) assertTabs({ active: 0 }) expect(selectedIndexElement).toHaveTextContent('0') await click(getByText('reverse')) // Note: the indices are reversed again now (back to normal) await click(getByText('Tab 0')) assertTabs({ active: 0 }) expect(selectedIndexElement).toHaveTextContent('0') await click(getByText('Tab 1')) assertTabs({ active: 1 }) expect(selectedIndexElement).toHaveTextContent('1') await click(getByText('Tab 2')) assertTabs({ active: 2 }) expect(selectedIndexElement).toHaveTextContent('2') }) ) describe('`renderProps`', () => { it( 'should be possible to render using as={Fragment}', suppressConsoleLogs(async () => { render( Tab 2 Tab 3 Content 1 Content 2 Content 3 ) assertTabs({ active: 0, tabContents: 'Tab 1', panelContents: 'Content 1' }) }) ) it( 'should be possible to render using multiple as={Fragment}', suppressConsoleLogs(async () => { render( Content 1 Content 2 ) assertTabs({ active: 0, tabContents: 'Tab 1', panelContents: 'Content 1' }) }) ) it( 'should expose the `selectedIndex` on the `Tab.Group` component', suppressConsoleLogs(async () => { render( {(data) => ( <>
{JSON.stringify(data)}
Tab 1 Tab 2 Tab 3 Content 1 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', suppressConsoleLogs(async () => { render( {(data) => ( <>
{JSON.stringify(data)}
Tab 1 Tab 2 Tab 3 )}
Content 1 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.Panels` component', suppressConsoleLogs(async () => { render( Tab 1 Tab 2 Tab 3 {(data) => ( <>
{JSON.stringify(data)}
Content 1 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 `selected` state on the `Tab` components', suppressConsoleLogs(async () => { render( {(data) => ( <>
{JSON.stringify(data)}
Tab 1 )}
{(data) => ( <>
{JSON.stringify(data)}
Tab 2 )}
{(data) => ( <>
{JSON.stringify(data)}
Tab 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 }) ) await click(getTabs()[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 expose the `selected` state on the `Tab.Panel` components', 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 )}
) 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 }) ) 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 }) ) }) ) }) 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) assertTabs({ active: 0 }) assertActiveElement(getByText('Tab 1')) }) ) it( 'should jump to the nearest tab when the defaultIndex is out of bounds (+5)', suppressConsoleLogs(async () => { render( <> Tab 1 Tab 2 Tab 3 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', 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(getByText('Tab 2')) }) ) it( 'should jump to the next available tab when the defaultIndex is a disabled tab and wrap around', 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 }) assertActiveElement(getByText('Tab 1')) }) ) it( 'should not change the Tab if the defaultIndex changes', suppressConsoleLogs(async () => { function Example() { let [defaultIndex, setDefaultIndex] = useState(1) return ( <> Tab 1 Tab 2 Tab 3 Content 1 Content 2 Content 3 ) } render() assertActiveElement(document.body) await press(Keys.Tab) assertTabs({ active: 1 }) assertActiveElement(getByText('Tab 2')) await click(getByText('Tab 3')) assertTabs({ active: 2 }) assertActiveElement(getByText('Tab 3')) // Change default index await click(getByText('change')) // Nothing should change... assertTabs({ active: 2 }) }) ) it( 'should select first tab if no tabs were provided originally', suppressConsoleLogs(async () => { function Example({ defaultIndex = undefined }: { defaultIndex?: number } = {}) { let [tabs, setTabs] = useState([]) return ( <> {tabs.map((tab, index) => ( {tab} ))} {tabs.map((tab, index) => ( content: {tab} ))} ) } render() assertActiveElement(document.body) // There are no tab initially assertTabs({ active: -1 }) // There are not tabs so this should not change anything await press(Keys.Tab) assertTabs({ active: -1 }) // Add some tabs await click(getByText('change')) // When going from no tabs to some tabs, the tab based on defaultIndex should be selected assertTabs({ active: 0 }) }) ) it( 'should select first tab if no tabs were provided originally (with a defaultIndex of 1)', suppressConsoleLogs(async () => { function Example({ defaultIndex = undefined }: { defaultIndex?: number } = {}) { let [tabs, setTabs] = useState([]) return ( <> {tabs.map((tab, index) => ( {tab} ))} {tabs.map((tab, index) => ( content: {tab} ))} ) } render() assertActiveElement(document.body) // There are no tab initially assertTabs({ active: -1 }) // There are not tabs so this should not change anything await press(Keys.Tab) assertTabs({ active: -1 }) // Add some tabs await click(getByText('change')) // When going from no tabs to some tabs, the tab based on defaultIndex should be selected assertTabs({ active: 1 }) }) ) it( 'should select first tab if no tabs were provided originally (with a defaultIndex of 1)', suppressConsoleLogs(async () => { function Example({ defaultIndex = undefined }: { defaultIndex?: number } = {}) { let [tabs, setTabs] = useState([]) return ( <> {tabs.map((tab, index) => ( {tab} ))} {tabs.map((tab, index) => ( content: {tab} ))} ) } render() assertActiveElement(document.body) // There are no tab initially assertTabs({ active: -1 }) // There are not tabs so this should not change anything await press(Keys.Tab) assertTabs({ active: -1 }) // Add some tabs await click(getByText('change 1')) await click(getByText('change 2')) await click(getByText('change 3')) // When going from no tabs to some tabs, the tab based on defaultIndex should be selected assertTabs({ active: 1 }) }) ) }) describe('`selectedIndex`', () => { it( 'should not change the tab in a controlled component if you do not respond to the onChange', suppressConsoleLogs(async () => { let handleChange = jest.fn() function ControlledTabs() { let [selectedIndex, setSelectedIndex] = useState(0) return ( <> { handleChange(value) }} > Tab 1 Tab 2 Tab 3 Content 1 Content 2 Content 3 ) } render() assertActiveElement(document.body) // test controlled behaviour await click(getByText('setSelectedIndex')) assertTabs({ active: 1 }) await click(getByText('setSelectedIndex')) assertTabs({ active: 2 }) // test uncontrolled behaviour again await click(getByText('Tab 1')) assertTabs({ active: 2 }) // Should still be Tab 3 because `selectedIndex` didn't update await click(getByText('Tab 2')) assertTabs({ active: 2 }) // Should still be Tab 3 because `selectedIndex` didn't update await click(getByText('Tab 3')) assertTabs({ active: 2 }) // Should still be Tab 3 because `selectedIndex` didn't update await click(getByText('Tab 1')) expect(handleChange).toHaveBeenCalledTimes(3) // We did see the 'onChange' calls, but only 3 because clicking Tab 3 is already the active one which means that this doesn't trigger the onChange assertTabs({ active: 2 }) // Should still be Tab 3 because `selectedIndex` didn't update }) ) it( 'should be possible to change active tab controlled and uncontrolled', suppressConsoleLogs(async () => { let handleChange = jest.fn() function ControlledTabs() { let [selectedIndex, setSelectedIndex] = useState(0) return ( <> { setSelectedIndex(value) handleChange(value) }} > Tab 1 Tab 2 Tab 3 Content 1 Content 2 Content 3 ) } render() assertActiveElement(document.body) // 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 }) // test uncontrolled behaviour again await click(getByText('Tab 2')) expect(handleChange).toHaveBeenCalledTimes(2) expect(handleChange).toHaveBeenNthCalledWith(2, 1) assertTabs({ active: 1 }) }) ) it( 'should jump to the nearest tab when the selectedIndex 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) assertTabs({ active: 0 }) assertActiveElement(getByText('Tab 1')) }) ) it( 'should jump to the nearest tab when the selectedIndex is out of bounds (+5)', suppressConsoleLogs(async () => { render( <> Tab 1 Tab 2 Tab 3 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 selectedIndex is a disabled 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(getByText('Tab 2')) }) ) it( 'should jump to the next available tab when the selectedIndex is a disabled tab and wrap around', 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 }) assertActiveElement(getByText('Tab 1')) }) ) it( 'should prefer selectedIndex over defaultIndex', 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 }) assertActiveElement(getByText('Tab 1')) }) ) it( 'should wrap around when overflowing the index when using a controlled component', suppressConsoleLogs(async () => { function Example() { let [selectedIndex, setSelectedIndex] = useState(0) return ( {({ selectedIndex }) => ( <> Tab 1 Tab 2 Tab 3 Content 1 Content 2 Content 3 )} ) } render() assertActiveElement(document.body) await click(getByText('Next')) assertTabs({ active: 1 }) await click(getByText('Next')) assertTabs({ active: 2 }) await click(getByText('Next')) assertTabs({ active: 0 }) await click(getByText('Next')) assertTabs({ active: 1 }) }) ) it( 'should wrap around when underflowing the index when using a controlled component', suppressConsoleLogs(async () => { function Example() { let [selectedIndex, setSelectedIndex] = useState(0) return ( {({ selectedIndex }) => ( <> Tab 1 Tab 2 Tab 3 Content 1 Content 2 Content 3 )} ) } render() assertActiveElement(document.body) await click(getByText('Previous')) assertTabs({ active: 2 }) await click(getByText('Previous')) assertTabs({ active: 1 }) await click(getByText('Previous')) assertTabs({ active: 0 }) await click(getByText('Previous')) assertTabs({ active: 2 }) }) ) }) describe(`'Tab'`, () => { describe('`type` attribute', () => { it( 'should set the `type` to "button" by default', suppressConsoleLogs(async () => { render( Trigger ) expect(getTabs()[0]).toHaveAttribute('type', 'button') }) ) 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) await press(Keys.Tab) assertTabs({ active: 0 }) assertActiveElement(getByText('Tab 1')) await press(Keys.Tab) assertActiveElement(getByText('Content 1')) await press(Keys.Tab) assertActiveElement(getByText('after')) await press(shift(Keys.Tab)) assertActiveElement(getByText('Content 1')) await press(shift(Keys.Tab)) assertActiveElement(getByText('Tab 1')) }) ) 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 ) assertActiveElement(document.body) await press(Keys.Tab) assertTabs({ active: 1 }) assertActiveElement(getByText('Tab 2')) await press(Keys.Tab) assertActiveElement(getByText('Content 2')) await press(Keys.Tab) assertActiveElement(getByText('after')) await press(shift(Keys.Tab)) assertActiveElement(getByText('Content 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`)', 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.ArrowRight) assertTabs({ active: 1 }) await press(Keys.ArrowRight) assertTabs({ active: 2 }) }) ) 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 }) 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 }) }) ) 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 }) await press(Keys.ArrowRight) assertTabs({ active: 1 }) await press(Keys.ArrowRight) assertTabs({ active: 2 }) await press(Keys.ArrowRight) assertTabs({ active: 0 }) await press(Keys.ArrowRight) assertTabs({ active: 1 }) }) ) 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 }) 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: 2 }) await press(Keys.Enter) assertTabs({ active: 0 }) 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`)', 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.ArrowRight) // no-op assertTabs({ active: 0, orientation: 'vertical' }) }) ) 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 ) assertActiveElement(document.body) 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' }) }) ) }) describe('`ArrowLeft` key', () => { 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 ) assertActiveElement(document.body) await press(Keys.Tab) assertTabs({ active: 2 }) await press(Keys.ArrowLeft) assertTabs({ active: 1 }) await press(Keys.ArrowLeft) assertTabs({ active: 0 }) }) ) 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 ) assertActiveElement(document.body) 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: 1 }) await press(Keys.Enter) assertTabs({ active: 0 }) }) ) it( 'should wrap around at the beginning (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: 2 }) await press(Keys.ArrowLeft) assertTabs({ active: 1 }) await press(Keys.ArrowLeft) assertTabs({ active: 0 }) await press(Keys.ArrowLeft) assertTabs({ active: 2 }) await press(Keys.ArrowLeft) assertTabs({ active: 1 }) }) ) it( 'should wrap around at the beginning (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: 2 }) 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: 0 }) await press(Keys.Enter) assertTabs({ active: 2 }) 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`)', 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.ArrowLeft) // no-op assertTabs({ active: 0, orientation: 'vertical' }) }) ) 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, orientation: 'vertical' }) await press(Keys.ArrowLeft) assertTabs({ active: 0, orientation: 'vertical' }) await press(Keys.Enter) // no-op assertTabs({ active: 0, orientation: 'vertical' }) }) ) }) describe('`ArrowDown` key', () => { 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' }) await press(Keys.ArrowDown) assertTabs({ active: 1, orientation: 'vertical' }) await press(Keys.ArrowDown) assertTabs({ active: 2, orientation: 'vertical' }) }) ) 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' }) 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' }) }) ) 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' }) await press(Keys.ArrowDown) assertTabs({ active: 1, orientation: 'vertical' }) await press(Keys.ArrowDown) assertTabs({ active: 2, orientation: 'vertical' }) await press(Keys.ArrowDown) assertTabs({ active: 0, orientation: 'vertical' }) await press(Keys.ArrowDown) assertTabs({ active: 1, orientation: 'vertical' }) }) ) 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' }) }) ) it( 'should not be possible to go down when in horizontal mode (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 }) await press(Keys.ArrowDown) // no-op assertTabs({ active: 0 }) }) ) it( 'should not be possible to go down when in horizontal 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.ArrowDown) assertTabs({ active: 0 }) await press(Keys.Enter) // no-op assertTabs({ active: 0 }) }) ) }) describe('`ArrowUp` key', () => { 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 ) assertActiveElement(document.body) await press(Keys.Tab) assertTabs({ active: 2, orientation: 'vertical' }) await press(Keys.ArrowUp) assertTabs({ active: 1, orientation: 'vertical' }) await press(Keys.ArrowUp) assertTabs({ active: 0, orientation: 'vertical' }) }) ) 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 ) assertActiveElement(document.body) 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' }) }) ) it( 'should wrap around at the beginning (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: 2, orientation: 'vertical' }) await press(Keys.ArrowUp) assertTabs({ active: 1, orientation: 'vertical' }) await press(Keys.ArrowUp) assertTabs({ active: 0, orientation: 'vertical' }) await press(Keys.ArrowUp) assertTabs({ active: 2, orientation: 'vertical' }) await press(Keys.ArrowUp) assertTabs({ active: 1, orientation: 'vertical' }) }) ) it( 'should wrap around at the beginning (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: 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' }) }) ) 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 ) 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`)', 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.Home) assertTabs({ active: 0 }) }) ) 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.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`)', 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: 0 }) }) ) 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 ) assertActiveElement(document.body) await press(Keys.Tab) assertTabs({ active: 1 }) await press(Keys.End) 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`)', 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.PageDown) 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 ) assertActiveElement(document.body) await press(Keys.Tab) assertTabs({ active: 1 }) await press(Keys.PageDown) assertTabs({ active: 1 }) await press(Keys.Enter) assertTabs({ active: 2 }) }) ) }) 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 ) assertActiveElement(document.body) getByText('Tab 3')?.focus() assertActiveElement(getByText('Tab 3')) assertTabs({ active: 0 }) await press(Keys.Enter) assertTabs({ active: 2 }) }) ) }) 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) getByText('Tab 3')?.focus() assertActiveElement(getByText('Tab 3')) assertTabs({ active: 0 }) await press(Keys.Space) assertTabs({ active: 2 }) }) ) }) }) describe('Mouse interactions', () => { it( 'should be possible to click on a tab to focus it', 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 click(getByText('Tab 1')) assertTabs({ active: 0 }) 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', 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 click(getByText('Tab 1')) // No-op, Tab 2 is still active assertTabs({ active: 1 }) }) ) }) describe('Composition', () => { it( 'should be possible to go to the next item containing a Dialog component', suppressConsoleLogs(async () => { render( <> Tab 1 Tab 2 Tab 3 Content 1 <> Content 3 ) assertActiveElement(document.body) await press(Keys.Tab) assertTabs({ active: 0 }) // Navigate to Dialog tab await press(Keys.ArrowRight) assertTabs({ active: 1 }) // Focus on to the Dialog panel await press(Keys.Tab) assertActiveElement(document.querySelector('[data-panel="1"]')) // Focus on to the Dialog trigger button await press(Keys.Tab) assertActiveElement(getByText('open')) // Focus back to the panel await press(shift(Keys.Tab)) assertActiveElement(document.querySelector('[data-panel="1"]')) // Focus back to tabs await press(shift(Keys.Tab)) assertTabs({ active: 1 }) // Navigate to the next tab await press(Keys.ArrowRight) assertTabs({ active: 2 }) // Focus on to the content panel await press(Keys.Tab) assertActiveElement(document.querySelector('[data-panel="2"]')) }) ) }) it( 'should trigger the `onChange` when the tab changes', suppressConsoleLogs(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).toHaveBeenNthCalledWith(1, 1) expect(changes).toHaveBeenNthCalledWith(2, 2) expect(changes).toHaveBeenNthCalledWith(3, 1) expect(changes).toHaveBeenNthCalledWith(4, 0) }) )