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(