diff --git a/packages/mui-base/src/Tabs/Root/TabsRoot.test.tsx b/packages/mui-base/src/Tabs/Root/TabsRoot.test.tsx index b18f83af0c..019cb9acb4 100644 --- a/packages/mui-base/src/Tabs/Root/TabsRoot.test.tsx +++ b/packages/mui-base/src/Tabs/Root/TabsRoot.test.tsx @@ -5,6 +5,8 @@ import { act, describeSkipIf, flushMicrotasks, fireEvent, screen } from '@mui/in import { Tabs } from '@base_ui/react/Tabs'; import { createRenderer, describeConformance } from '#test-utils'; +const isJSDOM = /jsdom/.test(window.navigator.userAgent); + describe('', () => { const { render } = createRenderer(); @@ -275,188 +277,156 @@ describe('', () => { ].forEach((entry) => { const [orientation, direction, previousItemKey, nextItemKey] = entry; - describe(`when focus is on a tab element in a ${orientation} ${direction ?? ''} tablist`, () => { - describe(previousItemKey ?? '', () => { - describe('with `activateOnFocus = false`', () => { - it('moves focus to the last tab without activating it if focus is on the first tab', async () => { - const handleChange = spy(); - const handleKeyDown = spy(); - const { getAllByRole } = await render( - - - - - - - , - ); - const [firstTab, , lastTab] = getAllByRole('tab'); - await act(async () => { - firstTab.focus(); + describeSkipIf(isJSDOM && direction === 'rtl')( + `when focus is on a tab element in a ${orientation} ${direction ?? ''} tablist`, + () => { + describe(previousItemKey ?? '', () => { + describe('with `activateOnFocus = false`', () => { + it('moves focus to the last tab without activating it if focus is on the first tab', async () => { + const handleChange = spy(); + const handleKeyDown = spy(); + const { getAllByRole } = await render( + + + + + + + , + ); + const [firstTab, , lastTab] = getAllByRole('tab'); + await act(async () => { + firstTab.focus(); + }); + + fireEvent.keyDown(firstTab, { key: previousItemKey }); + await flushMicrotasks(); + + expect(lastTab).toHaveFocus(); + expect(handleChange.callCount).to.equal(0); + expect(handleKeyDown.callCount).to.equal(1); + expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - fireEvent.keyDown(firstTab, { key: previousItemKey }); - await flushMicrotasks(); - - expect(lastTab).toHaveFocus(); - expect(handleChange.callCount).to.equal(0); - expect(handleKeyDown.callCount).to.equal(1); - expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); - }); - - it('moves focus to the previous tab without activating it', async () => { - const handleChange = spy(); - const handleKeyDown = spy(); - const { getAllByRole } = await render( - - - - - - - , - ); - const [firstTab, secondTab] = getAllByRole('tab'); - await act(async () => { - secondTab.focus(); + it('moves focus to the previous tab without activating it', async () => { + const handleChange = spy(); + const handleKeyDown = spy(); + const { getAllByRole } = await render( + + + + + + + , + ); + const [firstTab, secondTab] = getAllByRole('tab'); + await act(async () => { + secondTab.focus(); + }); + + fireEvent.keyDown(secondTab, { key: previousItemKey }); + await flushMicrotasks(); + + expect(firstTab).toHaveFocus(); + expect(handleChange.callCount).to.equal(0); + expect(handleKeyDown.callCount).to.equal(1); + expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - - fireEvent.keyDown(secondTab, { key: previousItemKey }); - await flushMicrotasks(); - - expect(firstTab).toHaveFocus(); - expect(handleChange.callCount).to.equal(0); - expect(handleKeyDown.callCount).to.equal(1); - expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - }); - describe('with `activateOnFocus = true`', () => { - it('moves focus to the last tab while activating it if focus is on the first tab', async () => { - const handleChange = spy(); - const handleKeyDown = spy(); - const { getAllByRole } = await render( - - - - - - - , - ); - const [firstTab, , lastTab] = getAllByRole('tab'); - await act(async () => { - firstTab.focus(); + describe('with `activateOnFocus = true`', () => { + it('moves focus to the last tab while activating it if focus is on the first tab', async () => { + const handleChange = spy(); + const handleKeyDown = spy(); + const { getAllByRole } = await render( + + + + + + + , + ); + const [firstTab, , lastTab] = getAllByRole('tab'); + await act(async () => { + firstTab.focus(); + }); + + fireEvent.keyDown(firstTab, { key: previousItemKey }); + await flushMicrotasks(); + + expect(lastTab).toHaveFocus(); + expect(handleChange.callCount).to.equal(1); + expect(handleChange.firstCall.args[0]).to.equal(2); + expect(handleKeyDown.callCount).to.equal(1); + expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - fireEvent.keyDown(firstTab, { key: previousItemKey }); - await flushMicrotasks(); - - expect(lastTab).toHaveFocus(); - expect(handleChange.callCount).to.equal(1); - expect(handleChange.firstCall.args[0]).to.equal(2); - expect(handleKeyDown.callCount).to.equal(1); - expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); - }); - - it('moves focus to the previous tab while activating it', async () => { - const handleChange = spy(); - const handleKeyDown = spy(); - const { getAllByRole } = await render( - - - - - - - , - ); - const [firstTab, secondTab] = getAllByRole('tab'); - await act(async () => { - secondTab.focus(); + it('moves focus to the previous tab while activating it', async () => { + const handleChange = spy(); + const handleKeyDown = spy(); + const { getAllByRole } = await render( + + + + + + + , + ); + const [firstTab, secondTab] = getAllByRole('tab'); + await act(async () => { + secondTab.focus(); + }); + + fireEvent.keyDown(secondTab, { key: previousItemKey }); + await flushMicrotasks(); + + expect(firstTab).toHaveFocus(); + expect(handleChange.callCount).to.equal(1); + expect(handleChange.firstCall.args[0]).to.equal(0); + expect(handleKeyDown.callCount).to.equal(1); + expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - - fireEvent.keyDown(secondTab, { key: previousItemKey }); - await flushMicrotasks(); - - expect(firstTab).toHaveFocus(); - expect(handleChange.callCount).to.equal(1); - expect(handleChange.firstCall.args[0]).to.equal(0); - expect(handleKeyDown.callCount).to.equal(1); - expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); - }); - }); - - it('skips over disabled tabs', async () => { - const handleKeyDown = spy(); - const { getAllByRole } = await render( - - - - - - - , - ); - const [firstTab, , lastTab] = getAllByRole('tab'); - await act(async () => { - lastTab.focus(); }); - fireEvent.keyDown(lastTab, { key: previousItemKey }); - await flushMicrotasks(); - - expect(firstTab).toHaveFocus(); - expect(handleKeyDown.callCount).to.equal(1); - expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); - }); - }); - - describe(nextItemKey ?? '', () => { - describe('with `activateOnFocus = false`', () => { - it('moves focus to the first tab without activating it if focus is on the last tab', async () => { - const handleChange = spy(); + it('skips over disabled tabs', async () => { const handleKeyDown = spy(); const { getAllByRole } = await render( - + - + , @@ -466,146 +436,181 @@ describe('', () => { lastTab.focus(); }); - fireEvent.keyDown(lastTab, { key: nextItemKey }); + fireEvent.keyDown(lastTab, { key: previousItemKey }); await flushMicrotasks(); expect(firstTab).toHaveFocus(); - expect(handleChange.callCount).to.equal(0); expect(handleKeyDown.callCount).to.equal(1); expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); + }); - it('moves focus to the next tab without activating it', async () => { - const handleChange = spy(); - const handleKeyDown = spy(); - const { getAllByRole } = await render( - - - - - - - , - ); - const [, secondTab, lastTab] = getAllByRole('tab'); - await act(async () => { - secondTab.focus(); + describe(nextItemKey ?? '', () => { + describe('with `activateOnFocus = false`', () => { + it('moves focus to the first tab without activating it if focus is on the last tab', async () => { + const handleChange = spy(); + const handleKeyDown = spy(); + const { getAllByRole } = await render( + + + + + + + , + ); + const [firstTab, , lastTab] = getAllByRole('tab'); + await act(async () => { + lastTab.focus(); + }); + + fireEvent.keyDown(lastTab, { key: nextItemKey }); + await flushMicrotasks(); + + expect(firstTab).toHaveFocus(); + expect(handleChange.callCount).to.equal(0); + expect(handleKeyDown.callCount).to.equal(1); + expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - fireEvent.keyDown(secondTab, { key: nextItemKey }); - await flushMicrotasks(); - - expect(lastTab).toHaveFocus(); - expect(handleChange.callCount).to.equal(0); - expect(handleKeyDown.callCount).to.equal(1); - expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); + it('moves focus to the next tab without activating it', async () => { + const handleChange = spy(); + const handleKeyDown = spy(); + const { getAllByRole } = await render( + + + + + + + , + ); + const [, secondTab, lastTab] = getAllByRole('tab'); + await act(async () => { + secondTab.focus(); + }); + + fireEvent.keyDown(secondTab, { key: nextItemKey }); + await flushMicrotasks(); + + expect(lastTab).toHaveFocus(); + expect(handleChange.callCount).to.equal(0); + expect(handleKeyDown.callCount).to.equal(1); + expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); + }); }); - }); - describe('with `activateOnFocus = true`', () => { - it('moves focus to the first tab while activating it if focus is on the last tab', async () => { - const handleChange = spy(); - const handleKeyDown = spy(); - const { getAllByRole } = await render( - - - - - - - , - ); - const [firstTab, , lastTab] = getAllByRole('tab'); - await act(async () => { - lastTab.focus(); + describe('with `activateOnFocus = true`', () => { + it('moves focus to the first tab while activating it if focus is on the last tab', async () => { + const handleChange = spy(); + const handleKeyDown = spy(); + const { getAllByRole } = await render( + + + + + + + , + ); + const [firstTab, , lastTab] = getAllByRole('tab'); + await act(async () => { + lastTab.focus(); + }); + + fireEvent.keyDown(lastTab, { key: nextItemKey }); + await flushMicrotasks(); + + expect(firstTab).toHaveFocus(); + expect(handleChange.callCount).to.equal(1); + expect(handleChange.firstCall.args[0]).to.equal(0); + expect(handleKeyDown.callCount).to.equal(1); + expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - fireEvent.keyDown(lastTab, { key: nextItemKey }); - await flushMicrotasks(); - - expect(firstTab).toHaveFocus(); - expect(handleChange.callCount).to.equal(1); - expect(handleChange.firstCall.args[0]).to.equal(0); - expect(handleKeyDown.callCount).to.equal(1); - expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); + it('moves focus to the next tab while activating it', async () => { + const handleChange = spy(); + const handleKeyDown = spy(); + const { getAllByRole } = await render( + + + + + + + , + ); + const [, secondTab, lastTab] = getAllByRole('tab'); + await act(async () => { + secondTab.focus(); + }); + + fireEvent.keyDown(secondTab, { key: nextItemKey }); + await flushMicrotasks(); + + expect(lastTab).toHaveFocus(); + expect(handleChange.callCount).to.equal(1); + expect(handleChange.firstCall.args[0]).to.equal(2); + expect(handleKeyDown.callCount).to.equal(1); + expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); + }); }); - it('moves focus to the next tab while activating it', async () => { - const handleChange = spy(); + it('skips over disabled tabs', async () => { const handleKeyDown = spy(); const { getAllByRole } = await render( - + , ); - const [, secondTab, lastTab] = getAllByRole('tab'); + const [firstTab, , lastTab] = getAllByRole('tab'); await act(async () => { - secondTab.focus(); + firstTab.focus(); }); - fireEvent.keyDown(secondTab, { key: nextItemKey }); + fireEvent.keyDown(firstTab, { key: nextItemKey }); await flushMicrotasks(); expect(lastTab).toHaveFocus(); - expect(handleChange.callCount).to.equal(1); - expect(handleChange.firstCall.args[0]).to.equal(2); expect(handleKeyDown.callCount).to.equal(1); expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); }); - - it('skips over disabled tabs', async () => { - const handleKeyDown = spy(); - const { getAllByRole } = await render( - - - - - - - , - ); - const [firstTab, , lastTab] = getAllByRole('tab'); - await act(async () => { - firstTab.focus(); - }); - - fireEvent.keyDown(firstTab, { key: nextItemKey }); - await flushMicrotasks(); - - expect(lastTab).toHaveFocus(); - expect(handleKeyDown.callCount).to.equal(1); - expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); - }); - }); - }); + }, + ); }); describe('when focus is on a tab regardless of orientation', () => { @@ -628,6 +633,7 @@ describe('', () => { }); fireEvent.keyDown(lastTab, { key: 'Home' }); + await flushMicrotasks(); expect(firstTab).toHaveFocus(); expect(handleChange.callCount).to.equal(0); @@ -653,6 +659,7 @@ describe('', () => { }); fireEvent.keyDown(lastTab, { key: 'Home' }); + await flushMicrotasks(); expect(firstTab).toHaveFocus(); expect(handleChange.callCount).to.equal(1); @@ -678,6 +685,7 @@ describe('', () => { }); fireEvent.keyDown(lastTab, { key: 'Home' }); + await flushMicrotasks(); expect(secondTab).toHaveFocus(); expect(handleKeyDown.callCount).to.equal(1); @@ -704,6 +712,7 @@ describe('', () => { }); fireEvent.keyDown(firstTab, { key: 'End' }); + await flushMicrotasks(); expect(lastTab).toHaveFocus(); expect(handleChange.callCount).to.equal(0); @@ -729,6 +738,7 @@ describe('', () => { }); fireEvent.keyDown(firstTab, { key: 'End' }); + await flushMicrotasks(); expect(lastTab).toHaveFocus(); expect(handleChange.callCount).to.equal(1); @@ -754,6 +764,7 @@ describe('', () => { }); fireEvent.keyDown(firstTab, { key: 'End' }); + await flushMicrotasks(); expect(secondTab).toHaveFocus(); expect(handleKeyDown.callCount).to.equal(1); @@ -779,7 +790,7 @@ describe('', () => { }); }); - describeSkipIf(/jsdom/.test(window.navigator.userAgent))('activation direction', () => { + describeSkipIf(isJSDOM)('activation direction', () => { it('should set the `data-activation-direction` attribute on the tabs root with orientation=horizontal', async () => { const { getAllByRole, getByTestId } = await render( diff --git a/packages/mui-base/src/Tabs/TabsList/TabsList.tsx b/packages/mui-base/src/Tabs/TabsList/TabsList.tsx index 9a5528ea71..5d98514c2a 100644 --- a/packages/mui-base/src/Tabs/TabsList/TabsList.tsx +++ b/packages/mui-base/src/Tabs/TabsList/TabsList.tsx @@ -97,6 +97,7 @@ const TabsList = React.forwardRef(function TabsList(