diff --git a/packages/lab/src/__tests__/__e2e__/tabs-next/TabsNext.cy.tsx b/packages/lab/src/__tests__/__e2e__/tabs-next/TabsNext.cy.tsx
index 54ed483d7be..6053eb39f56 100644
--- a/packages/lab/src/__tests__/__e2e__/tabs-next/TabsNext.cy.tsx
+++ b/packages/lab/src/__tests__/__e2e__/tabs-next/TabsNext.cy.tsx
@@ -1,27 +1,473 @@
+import {
+ TabBar,
+ TabListNext,
+ TabNext,
+ TabNextPanel,
+ TabNextTrigger,
+ TabsNext,
+} from "@salt-ds/lab";
import * as tabsStories from "@stories/tabs-next/tabs-next.stories";
import { composeStories } from "@storybook/react-vite";
+import { type ReactElement, useEffect, useState } from "react";
const {
Bordered,
DisabledTabs,
Overflow,
AddTabs,
- Closable,
+ Dismissible,
AddWithDialog,
- CloseWithConfirmation,
+ DismissWithConfirmation,
WithInteractiveElementInPanel,
Controlled,
+ AsyncDismissibleTabs,
} = composeStories(tabsStories);
+const selectorSafeTabs = [
+ "Home",
+ "Transactions",
+ 'Loan "A"',
+ "Checks",
+ "Liquidity",
+];
+
+const dynamicOverflowTabs = [
+ "Home",
+ "Transactions",
+ "Loans",
+ "Checks",
+ "Liquidity",
+ "With",
+ "Lots",
+ "More",
+ "Additional",
+ "Tabs",
+ "Added",
+ "In order to",
+ "Showcase overflow",
+ "Menu",
+ "On",
+ "Larger",
+ "Screens",
+];
+
+type ResponsiveOverflowWindow = Cypress.AUTWindow & {
+ __setResponsiveOverflowWidth?: (width: number) => void;
+ __setDynamicOverflowWidth?: (width: number) => void;
+};
+
+type OverflowOrderWindow = Cypress.AUTWindow & {
+ __overflowOrderObserver?: MutationObserver;
+ __overflowOrderSnapshots?: string[][];
+};
+
+type PortalContractWindow = Cypress.AUTWindow & {
+ __setPortalContractWidth?: (width: number) => void;
+};
+
+let nextTrackedTabInstanceId = 0;
+
+function TrackedTabContent({ label }: { label: string }) {
+ const [instanceId] = useState(() => {
+ nextTrackedTabInstanceId += 1;
+ return nextTrackedTabInstanceId;
+ });
+
+ return (
+ {`${label} instance ${instanceId}`}
+ );
+}
+
+function OverflowWithSelectorSafeValues() {
+ return (
+
+
+
+
+ {selectorSafeTabs.map((label) => (
+
+ {label}
+
+ ))}
+
+
+
+
+ );
+}
+
+function OverflowAfterWidthOnlyContentChange() {
+ const [expanded, setExpanded] = useState(false);
+ const tabs = [
+ { value: "Home", label: "Home" },
+ {
+ value: "Transactions",
+ label: expanded ? "Transactions with a much longer label" : "Tx",
+ },
+ { value: "Loans", label: "Loans" },
+ ];
+
+ return (
+ <>
+
+
+
+
+ {tabs.map(({ value, label }) => (
+
+ {label}
+
+ ))}
+
+
+
+
+
+ >
+ );
+}
+
+function OverflowAfterContainerWidthChange() {
+ const [width, setWidth] = useState(150);
+
+ useEffect(() => {
+ Object.assign(window, {
+ __setResponsiveOverflowWidth: setWidth,
+ });
+
+ return () => {
+ delete (
+ window as Window & {
+ __setResponsiveOverflowWidth?: typeof setWidth;
+ }
+ ).__setResponsiveOverflowWidth;
+ };
+ }, []);
+
+ return (
+
+
+
+
+ {selectorSafeTabs.map((label) => (
+
+ {label}
+
+ ))}
+
+
+
+
+ );
+}
+
+function OverflowWithoutInitialSelection() {
+ return (
+
+
+
+
+ {selectorSafeTabs.map((label) => (
+
+ {label}
+
+ ))}
+
+
+
+
+ );
+}
+
+function OverflowWithControlledSelection() {
+ const [selected, setSelected] = useState(selectorSafeTabs[0]);
+
+ return (
+
+ setSelected(nextValue)}
+ >
+
+
+ {selectorSafeTabs.map((label) => (
+
+ {label}
+
+ ))}
+
+
+
+
+ );
+}
+
+function OverflowWithIgnoredOverflowSelection() {
+ const [selected, setSelected] = useState(selectorSafeTabs[0]);
+
+ return (
+ <>
+
+ undefined}>
+
+
+ {selectorSafeTabs.map((label) => (
+
+ {label}
+
+ ))}
+
+
+
+
+
+ >
+ );
+}
+
+function TabsNextWithEmptyStringValue() {
+ const tabs = [
+ { label: "Empty", value: "" },
+ { label: "Transactions", value: "transactions" },
+ { label: "Liquidity", value: "liquidity" },
+ ];
+
+ return (
+
+
+
+ {tabs.map(({ label, value }) => (
+
+ {label}
+
+ ))}
+
+
+ {tabs.map(({ label, value }) => (
+
+ {label}
+
+ ))}
+
+ );
+}
+
+function OverflowWithEmptyStringValue() {
+ const tabs = [
+ { label: "Home", value: "home" },
+ { label: "Transactions", value: "transactions" },
+ { label: "Empty", value: "" },
+ { label: "Liquidity", value: "liquidity" },
+ { label: "Checks", value: "checks" },
+ ];
+
+ return (
+
+
+
+
+ {tabs.map(({ label, value }) => (
+
+ {label}
+
+ ))}
+
+
+
+
+ );
+}
+
+function OverflowWithDisabledHiddenTab() {
+ return (
+
+
+
+
+ {dynamicOverflowTabs.map((label) => (
+
+ {label}
+
+ ))}
+
+
+
+
+ );
+}
+
+function OverflowAfterClassBasedWidthChange() {
+ const [wide, setWide] = useState(false);
+
+ return (
+ <>
+
+
+
+
+
+ {selectorSafeTabs.map((label) => (
+
+ {label}
+
+ ))}
+
+
+
+
+
+ >
+ );
+}
+
+function OverflowWithinContainer({ width }: { width: number }) {
+ return (
+
+
+
+ );
+}
+
+function DynamicOverflowBoundary() {
+ const [width, setWidth] = useState(408);
+
+ useEffect(() => {
+ Object.assign(window, {
+ __setDynamicOverflowWidth: setWidth,
+ });
+
+ return () => {
+ delete (
+ window as Window & {
+ __setDynamicOverflowWidth?: typeof setWidth;
+ }
+ ).__setDynamicOverflowWidth;
+ };
+ }, []);
+
+ return (
+
+
+
+
+ {dynamicOverflowTabs.map((label) => (
+
+ {label}
+
+ ))}
+
+
+
+
+ );
+}
+
+function OverflowWithTrackedTabContent() {
+ const [width, setWidth] = useState(198);
+
+ useEffect(() => {
+ Object.assign(window, {
+ __setPortalContractWidth: setWidth,
+ });
+
+ return () => {
+ delete (
+ window as Window & {
+ __setPortalContractWidth?: typeof setWidth;
+ }
+ ).__setPortalContractWidth;
+ };
+ }, []);
+
+ return (
+
+
+
+
+ {selectorSafeTabs.map((label) => (
+
+
+
+
+
+ ))}
+
+
+
+
+ );
+}
+
+function clickOverflowTab(name: string | RegExp) {
+ cy.findByRole("tablist", { name: "Overflow tab options" })
+ .should("be.visible")
+ .within(() => {
+ cy.findByRole("tab", { name }).click();
+ });
+
+ cy.findByRole("tab", { name: "Overflow" }).should(
+ "have.attr",
+ "aria-expanded",
+ "false",
+ );
+}
+
+function assertSelectedMainTab(name: string) {
+ cy.findByRole("tablist").within(() => {
+ cy.findByRole("tab", { name, selected: true }).should("be.visible");
+ });
+}
+
+function mountTabsNext(
+ element: ReactElement,
+ options?: { width?: number | string },
+) {
+ cy.mount(
+ {element}
,
+ );
+}
+
describe("Given a Tabstrip", () => {
it("should render with tablist and tab roles", () => {
- cy.mount();
+ mountTabsNext();
cy.findByRole("tablist").should("be.visible");
cy.findAllByRole("tab").should("have.length", 5);
});
it("should support keyboard navigation and wrap", () => {
- cy.mount();
+ mountTabsNext();
cy.realPress("Tab");
cy.findByRole("tab", { name: "Home" }).should("be.focused");
cy.realPress("ArrowRight");
@@ -38,13 +484,13 @@ describe("Given a Tabstrip", () => {
it("should support selection with a mouse", () => {
const changeSpy = cy.stub().as("changeSpy");
- cy.mount();
+ mountTabsNext();
cy.findByRole("tab", { name: "Home" }).should(
"have.attr",
"aria-selected",
"true",
);
- cy.findByRole("tab", { name: "Transactions" }).realClick();
+ cy.findByRole("tab", { name: "Transactions" }).click();
cy.findByRole("tab", { name: "Transactions" }).should(
"have.attr",
"aria-selected",
@@ -60,7 +506,7 @@ describe("Given a Tabstrip", () => {
it("should support selection with the keyboard", () => {
const changeSpy = cy.stub().as("changeSpy");
- cy.mount();
+ mountTabsNext();
cy.realPress("Tab");
cy.findByRole("tab", { name: "Home" }).should("be.focused");
cy.findByRole("tab", { name: "Home" }).should(
@@ -97,37 +543,38 @@ describe("Given a Tabstrip", () => {
cy.findByRole("tab", { name: "Loans" }).should("be.focused");
});
- it("should allow keyboard navigation into the overflow menu", () => {
- cy.mount();
+ it("should allow keyboard access into and out of the overflow menu", () => {
+ mountTabsNext();
- cy.findAllByRole("tab").filter(":visible").should("have.length", 4);
+ cy.findAllByRole("tab").should("have.length", 5);
cy.realPress("Tab");
cy.findByRole("tab", { name: "Home" }).should("be.focused");
cy.realPress("ArrowLeft");
- cy.findAllByRole("tab").filter(":visible").should("have.length", 17);
- cy.findByRole("tab", { name: "Screens" }).should("be.focused");
+ cy.findByRole("tab", { name: "Overflow" }).should("be.focused");
- cy.realPress("ArrowUp");
- cy.findByRole("tab", { name: "Larger" }).should("be.focused");
+ cy.realPress("Enter");
- cy.realPress("ArrowLeft");
- cy.findByRole("tab", { name: "On" }).should("be.focused");
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
- cy.realPress("ArrowDown");
- cy.findByRole("tab", { name: "Larger" }).should("be.focused");
+ cy.findByRole("tab", { name: "Liquidity" }).should("be.focused");
+
+ cy.realPress("Escape");
+ cy.findByRole("tab", { name: "Overflow" }).should("be.focused");
});
it("should allow tabs to be disabled", () => {
const changeSpy = cy.stub().as("changeSpy");
- cy.mount();
+ mountTabsNext();
cy.findByRole("tab", { name: "Loans" }).should(
"have.attr",
"aria-disabled",
"true",
);
- cy.findByRole("tab", { name: "Loans" }).realClick();
+ cy.findByRole("tab", { name: "Loans" }).click();
cy.findByRole("tab", { name: "Loans" }).should(
"have.attr",
"aria-selected",
@@ -138,140 +585,567 @@ describe("Given a Tabstrip", () => {
});
it("should overflow into a menu when there is not enough space to show all tabs", () => {
- cy.mount();
- cy.findAllByRole("tab").should("have.length", 17);
- cy.findAllByRole("tab").filter(":visible").should("have.length", 4);
- cy.findAllByRole("tab").filter(":not(:visible)").should("have.length", 13);
- cy.get("[data-overflowbutton]").should("be.visible");
+ mountTabsNext();
+ cy.findAllByRole("tab").should("have.length", 5);
+ cy.findByRole("tab", { name: "Overflow" }).should("be.visible");
});
it("should allow keyboard navigation in the menu", () => {
- cy.mount(
+ mountTabsNext(
<>
>,
);
- cy.get("[data-overflowbutton]").realClick();
+
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
+
cy.findByRole("tab", { name: "Liquidity" }).should("be.focused");
cy.realPress("ArrowDown");
cy.findByRole("tab", { name: "With" }).should("be.focused");
cy.realPress("End");
cy.findByRole("tab", { name: "Screens" }).should("be.focused");
+ cy.realPress("Home");
+ cy.findByRole("tab", { name: "Liquidity" }).should("be.focused");
cy.realPress("Escape");
- cy.findByRole("tab", { name: "Checks" }).should("be.focused");
+ cy.findByRole("tab", { name: "Overflow" }).should("be.focused");
cy.realPress("Tab");
cy.findByRole("button", { name: "end" }).should("be.focused");
});
- it("should close the overflow menu when a click is detected outside", () => {
- cy.mount();
+ it("should navigate past a disabled tab in the overflow menu", () => {
+ mountTabsNext();
- cy.get("[data-overflowbutton]").realClick();
- cy.findAllByRole("tab").filter(":visible").should("have.length", 17);
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
- cy.wait(500);
+ cy.findByRole("tab", { name: "Transactions" })
+ .should("be.focused")
+ .and("have.attr", "aria-disabled", "true");
+
+ cy.realPress("ArrowDown");
+ cy.findByRole("tab", { name: "Loans" }).should("be.focused");
+ });
+
+ it("should restore focus correctly after opening the menu with a mouse", () => {
+ mountTabsNext(
+ <>
+
+
+ >,
+ );
+
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
+
+ cy.findByRole("tab", { name: "Liquidity" }).should("be.focused");
+ cy.realPress("Escape");
+ cy.findByRole("tab", { name: "Overflow" }).should("be.focused");
+ cy.realPress("Tab");
+ cy.findByRole("button", { name: "end" }).should("be.focused");
+ });
+
+ it("should dismiss the overflow menu when a click is detected outside", () => {
+ mountTabsNext();
+
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
+
+ cy.findAllByRole("tab").should("have.length", 18);
cy.get("body").click(0, 0);
- cy.findAllByRole("tab").filter(":visible").should("have.length", 4);
+ cy.findAllByRole("tab").should("have.length", 5);
});
it("should allow selection in the menu", () => {
- cy.mount();
+ mountTabsNext();
+
+ cy.findAllByRole("tab").should("have.length", 5);
- cy.findAllByRole("tab").filter(":visible").should("have.length", 4);
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
- cy.get("[data-overflowbutton]").realClick();
- cy.findByRole("tab", { name: "Liquidity" }).realClick();
+ cy.findByRole("tab", { name: "Liquidity" }).should("be.focused");
- cy.findByRole("tab", { name: "Liquidity" })
+ clickOverflowTab("Liquidity");
+ cy.findByRole("tab", { name: "Liquidity", selected: true })
.should("have.attr", "aria-selected", "true")
- .should("be.focused");
+ .and("be.focused");
- cy.findAllByRole("tab").filter(":visible").should("have.length", 4);
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
+
+ cy.findByRole("tab", { name: "Checks" }).should("be.focused");
- cy.get("[data-overflowbutton]").realClick();
cy.realPress("Enter");
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "not.exist",
+ );
cy.findByRole("tab", { name: "Checks" })
.should("have.attr", "aria-selected", "true")
.should("be.focused");
});
+ it("should not temporarily remove an extra main tab when selecting from overflow", () => {
+ mountTabsNext();
+
+ cy.window().then((win) => {
+ const overflowOrderWindow = win as OverflowOrderWindow;
+ const tablist = win.document.querySelector('[role="tablist"]');
+
+ expect(tablist).to.exist;
+ if (!tablist) {
+ throw new Error("Expected tablist to exist");
+ }
+
+ const getMainTabOrder = () =>
+ Array.from(
+ tablist.querySelectorAll(':scope > [data-tabslot] [role="tab"]'),
+ ).map((tab) => tab.textContent?.trim() ?? "");
+
+ overflowOrderWindow.__overflowOrderSnapshots = [getMainTabOrder()];
+ overflowOrderWindow.__overflowOrderObserver = new win.MutationObserver(
+ () => {
+ overflowOrderWindow.__overflowOrderSnapshots?.push(getMainTabOrder());
+ },
+ );
+ overflowOrderWindow.__overflowOrderObserver.observe(tablist, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: ["aria-selected"],
+ });
+ });
+
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
+
+ clickOverflowTab("Liquidity");
+
+ cy.window().then(
+ (win) =>
+ new Cypress.Promise((resolve) => {
+ const overflowOrderWindow = win as OverflowOrderWindow;
+ win.requestAnimationFrame(() => {
+ win.requestAnimationFrame(() => {
+ overflowOrderWindow.__overflowOrderObserver?.disconnect();
+ resolve();
+ });
+ });
+ }),
+ );
+
+ cy.window().then((win) => {
+ const overflowOrderWindow = win as OverflowOrderWindow;
+ const snapshots = overflowOrderWindow.__overflowOrderSnapshots ?? [];
+
+ expect(snapshots).to.deep.include([
+ "Home",
+ "Transactions",
+ "Loans",
+ "Liquidity",
+ ]);
+ expect(snapshots).not.to.deep.include([
+ "Home",
+ "Transactions",
+ "Liquidity",
+ ]);
+ });
+ });
+
+ it("should announce when a selected overflow tab moves to the main list", () => {
+ mountTabsNext();
+
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
+
+ clickOverflowTab("Liquidity");
+ assertSelectedMainTab("Liquidity");
+ cy.get("[aria-live]", { timeout: 8000 }).should(
+ "contain.text",
+ "Liquidity moved to main tab list",
+ );
+ });
+
+ it("should announce when the first selection comes from overflow", () => {
+ mountTabsNext();
+
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
+
+ clickOverflowTab("Liquidity");
+ assertSelectedMainTab("Liquidity");
+ cy.get("[aria-live]", { timeout: 8000 }).should(
+ "contain.text",
+ "Liquidity moved to main tab list",
+ );
+ });
+
+ it("should make the first visible tab tabbable when there is no initial selection", () => {
+ mountTabsNext(
+ <>
+
+
+ >,
+ );
+
+ cy.realPress("Tab");
+ cy.findByRole("tab", { name: "Home" }).should("be.focused");
+
+ cy.realPress("Tab");
+ cy.findByRole("button", { name: "After" }).should("be.focused");
+ });
+
+ it("should announce when a controlled parent immediately applies an overflow selection", () => {
+ mountTabsNext();
+
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
+
+ clickOverflowTab("Liquidity");
+
+ assertSelectedMainTab("Liquidity");
+ cy.get("[aria-live]", { timeout: 8000 }).should(
+ "contain.text",
+ "Liquidity moved to main tab list",
+ );
+ });
+
+ it("should not announce when the same overflow value is later set externally after being ignored", () => {
+ mountTabsNext();
+
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
+
+ clickOverflowTab("Liquidity");
+ cy.findByRole("tab", { name: "Home", selected: true }).should("be.visible");
+
+ cy.findByRole("button", { name: "Select Liquidity externally" }).click();
+
+ assertSelectedMainTab("Liquidity");
+ cy.get("[aria-live]").should(
+ "not.contain.text",
+ "Liquidity moved to main tab list",
+ );
+ });
+
+ it("should preserve custom tab props and content instances while moving through overflow", () => {
+ let homeInstance = "";
+ let liquidityInstance = "";
+
+ mountTabsNext();
+
+ cy.findByRole("tablist", { name: "Portal contract tablist" }).within(() => {
+ cy.get('[data-slotid="main:Home"] [data-tab-host="Home"]').should(
+ "exist",
+ );
+ });
+
+ cy.get('[data-instance-label="Home"]')
+ .invoke("text")
+ .then((text) => {
+ homeInstance = text;
+ });
+ cy.get('[data-instance-label="Liquidity"]')
+ .invoke("text")
+ .then((text) => {
+ liquidityInstance = text;
+ });
+
+ cy.get('[data-root-marker="Home"][data-root-state="preserved"]').should(
+ "exist",
+ );
+
+ cy.window().then((win) => {
+ const portalContractWindow = win as PortalContractWindow;
+ portalContractWindow.__setPortalContractWidth?.(1000);
+ });
+
+ cy.findByRole("tab", { name: "Overflow" }).should("not.exist");
+
+ cy.then(() => {
+ cy.get('[data-instance-label="Home"]').should("have.text", homeInstance);
+ });
+ cy.then(() => {
+ cy.get('[data-instance-label="Liquidity"]').should(
+ "have.text",
+ liquidityInstance,
+ );
+ });
+
+ cy.window().then((win) => {
+ const portalContractWindow = win as PortalContractWindow;
+ portalContractWindow.__setPortalContractWidth?.(198);
+ });
+
+ cy.findByRole("tab", { name: "Overflow" }).should("be.visible");
+ cy.findByRole("tab", { name: "Overflow" }).click();
+
+ cy.findByRole("tablist", { name: "Overflow tab options" })
+ .should("be.visible")
+ .within(() => {
+ cy.get(
+ '[data-root-marker="Liquidity"][data-root-state="preserved"]',
+ ).should("exist");
+ });
+
+ cy.then(() => {
+ cy.get('[data-instance-label="Liquidity"]').should(
+ "have.text",
+ liquidityInstance,
+ );
+ });
+
+ clickOverflowTab(/^Liquidity instance /);
+
+ cy.findByRole("tablist", { name: "Portal contract tablist" }).within(() => {
+ cy.get(
+ '[data-root-marker="Liquidity"][data-root-state="preserved"]',
+ ).should("exist");
+ });
+ cy.then(() => {
+ cy.get('[data-instance-label="Liquidity"]').should(
+ "have.text",
+ liquidityInstance,
+ );
+ });
+ });
+
it("should allow selection in the menu when only having enough space for the newly selected tab", () => {
- cy.mount();
+ mountTabsNext();
- cy.findByRole("tablist").invoke("css", "max-width", 140);
- cy.wait(500);
+ cy.findByRole("tab", { name: "Home" }).should(
+ "have.attr",
+ "aria-selected",
+ "true",
+ );
- cy.findAllByRole("tab").filter(":visible").should("have.length", 1);
+ cy.findAllByRole("tab").should("have.length", 2);
- cy.get("[data-overflowbutton]").realClick();
- cy.findAllByRole("tab").filter(":visible").should("have.length", 14); // overflow menu shown
- cy.findByRole("tab", { name: "Liquidity" }).realClick();
- cy.findAllByRole("tab").filter(":visible").should("have.length", 1); // overflow menu hidden
+ cy.findByRole("tab", { name: "Overflow" }).click();
- cy.findByRole("tab", { name: "Liquidity" })
- .should("have.attr", "aria-selected", "true")
- .should("be.focused");
+ cy.findAllByRole("tab").should("have.length", 18); // overflow menu shown
+
+ cy.findByRole("tab", { name: "Transactions" }).should("be.focused");
+
+ clickOverflowTab("Liquidity");
+
+ cy.findAllByRole("tab").should("have.length", 2); // overflow menu hidden
+
+ cy.findByRole("tab", { name: "Liquidity", selected: true }).should(
+ "be.focused",
+ );
+ });
+
+ it("should allow overflow selection when values contain selector characters", () => {
+ mountTabsNext();
+
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
+
+ clickOverflowTab('Loan "A"');
+
+ cy.findByRole("tab", { name: 'Loan "A"', selected: true }).should(
+ "be.focused",
+ );
+ });
+
+ it("should keep the overflow menu closed when overflow returns after being removed by resize", () => {
+ mountTabsNext();
+
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
+
+ cy.window().then((win) => {
+ const responsiveWindow = win as ResponsiveOverflowWindow;
+ responsiveWindow.__setResponsiveOverflowWidth?.(1000);
+ });
+ cy.findByRole("tab", { name: "Overflow" }).should("not.exist");
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "not.exist",
+ );
+
+ cy.window().then((win) => {
+ const responsiveWindow = win as ResponsiveOverflowWindow;
+ responsiveWindow.__setResponsiveOverflowWidth?.(150);
+ });
+ cy.findByRole("tab", { name: "Overflow" })
+ .should("be.visible")
+ .and("have.attr", "aria-expanded", "false");
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "not.exist",
+ );
+ });
+
+ it("should recompute overflow when sizing is driven by CSS classes", () => {
+ mountTabsNext();
+
+ cy.findByRole("tablist", { name: "Class sized overflow tablist" }).within(
+ () => {
+ cy.findByRole("tab", { name: "Overflow" }).should("be.visible");
+ },
+ );
+
+ cy.findByRole("button", { name: "Toggle class width" }).click();
+
+ cy.findByRole("tablist", { name: "Class sized overflow tablist" }).within(
+ () => {
+ cy.findByRole("tab", { name: "Overflow" }).should("not.exist");
+ },
+ );
+
+ cy.findByRole("button", { name: "Toggle class width" }).click();
+
+ cy.findByRole("tablist", { name: "Class sized overflow tablist" }).within(
+ () => {
+ cy.findByRole("tab", { name: "Overflow" })
+ .should("be.visible")
+ .and("have.attr", "aria-expanded", "false");
+ },
+ );
+ });
+
+ it("should recompute overflow when tab content changes width without resizing the container", () => {
+ mountTabsNext();
+
+ cy.findByRole("tablist", { name: "Width change tablist" }).within(() => {
+ cy.findByRole("tab", { name: "Overflow" }).should("not.exist");
+ });
+
+ cy.findByRole("button", { name: "Expand label" }).click();
+
+ cy.findByRole("tablist", { name: "Width change tablist" }).within(() => {
+ cy.findByRole("tab", { name: "Overflow" }).should("be.visible");
+ });
+ });
+
+ it("should keep a pinned overflow tab visible when selection moves to an already visible tab", () => {
+ mountTabsNext();
+
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
+
+ clickOverflowTab("Liquidity");
+ assertSelectedMainTab("Liquidity");
+
+ cy.findByRole("tab", { name: "Transactions" }).click();
+ assertSelectedMainTab("Transactions");
+ cy.findByRole("tab", { name: "Liquidity" }).should("be.visible");
});
it("should support adding tabs", () => {
- cy.mount();
+ mountTabsNext();
cy.findAllByRole("tab").should("have.length", 3);
cy.findByRole("tab", { name: "Home" }).should(
"have.attr",
"aria-selected",
"true",
);
- cy.findByRole("button", { name: "Add tab" }).realClick();
+ cy.findByRole("button", { name: "Add tab" }).click();
cy.findAllByRole("tab").should("have.length", 4);
cy.findByRole("tab", { name: "Home" }).should(
"have.attr",
"aria-selected",
- "true",
+ "false",
);
- cy.findByRole("tab", { name: "New tab" }).should("be.visible");
+ cy.findByRole("tab", { name: "New tab" })
+ .should("be.visible")
+ .and("have.attr", "aria-selected", "true");
cy.findByRole("button", { name: "Add tab" }).should("be.focused");
});
+ it("should reserve space for the add button when tabs overflow", () => {
+ mountTabsNext(
+ ,
+ );
+
+ cy.findByRole("button", { name: "Add tab" }).click();
+ cy.findByRole("button", { name: "Add tab" }).click();
+ cy.findByRole("button", { name: "Add tab" }).click();
+
+ cy.findByRole("tab", { name: "Overflow" }).should("be.visible");
+
+ cy.findByRole("tablist").then(($tablist) => {
+ cy.findByRole("button", { name: "Add tab" }).then(($button) => {
+ const tablistRect = $tablist[0].getBoundingClientRect();
+ const buttonRect = $button[0].getBoundingClientRect();
+
+ expect(tablistRect.right).to.be.at.most(buttonRect.left);
+ });
+ });
+ });
+
it("should support adding tabs with confirmation", () => {
- cy.mount();
+ mountTabsNext();
cy.findAllByRole("tab").should("have.length", 3);
cy.findByRole("tab", { name: "Home" }).should(
"have.attr",
"aria-selected",
"true",
);
- cy.findByRole("button", { name: "Add tab" }).realClick();
+ cy.findByRole("button", { name: "Add tab" }).click();
cy.findByRole("dialog").should("be.visible");
- cy.findByLabelText("New tab name").realClick();
+ cy.findByLabelText("New tab name").click();
cy.realType("New tab");
- cy.findByRole("button", { name: "Confirm" }).realClick();
+ cy.findByRole("button", { name: "Confirm" }).click();
cy.findAllByRole("tab").should("have.length", 4);
- cy.findByRole("tab", { name: "New tab" }).should("be.visible");
cy.findByRole("tab", { name: "Home" }).should(
"have.attr",
"aria-selected",
"true",
);
cy.findByRole("button", { name: "Add tab" }).should("be.focused");
+
+ cy.get("body").then(($body) => {
+ if ($body.find("[data-overflowbutton]").length > 0) {
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).within(
+ () => {
+ cy.findByRole("tab", { name: "New tab" }).should("be.visible");
+ },
+ );
+ return;
+ }
+
+ cy.findByRole("tab", { name: "New tab" }).should("be.visible");
+ });
});
it("should add the correct aria when tab actions are used", () => {
- cy.mount();
-
- // TODO: enable when aria-actions is supported in browsers.
- // cy.findByRole("tab", { name: "Home" })
- // .invoke("attr", "aria-actions")
- // .then((actionId) => {
- // cy.findByRole("button", { name: "Home Close tab" }).should(
- // "have.attr",
- // "id",
- // actionId,
- // );
- // });
+ mountTabsNext();
cy.findByRole("tab", { name: "Home" }).should(
"have.accessibleDescription",
@@ -280,7 +1154,7 @@ describe("Given a Tabstrip", () => {
});
it("should support closing tabs with a mouse", () => {
- cy.mount();
+ mountTabsNext();
cy.findByRole("tab", { name: "Home" }).should(
"have.attr",
@@ -289,7 +1163,7 @@ describe("Given a Tabstrip", () => {
);
cy.findAllByRole("tab").should("have.length", 5);
- cy.findByRole("button", { name: "Liquidity Close tab" }).realClick();
+ cy.findByRole("button", { name: "Liquidity Dismiss tab" }).click();
cy.findAllByRole("tab").should("have.length", 4);
cy.findByRole("tab", { name: "Home" }).should(
"have.attr",
@@ -298,7 +1172,7 @@ describe("Given a Tabstrip", () => {
);
cy.findByRole("tab", { name: "Checks" }).should("be.focused");
- cy.findByRole("button", { name: "Loans Close tab" }).realClick();
+ cy.findByRole("button", { name: "Loans Dismiss tab" }).click();
cy.findAllByRole("tab").should("have.length", 3);
cy.findByRole("tab", { name: "Home" }).should(
"have.attr",
@@ -307,7 +1181,7 @@ describe("Given a Tabstrip", () => {
);
cy.findByRole("tab", { name: "Checks" }).should("be.focused");
- cy.findByRole("button", { name: "Home Close tab" }).realClick();
+ cy.findByRole("button", { name: "Home Dismiss tab" }).click();
cy.findAllByRole("tab").should("have.length", 2);
cy.findByRole("tab", { name: "Transactions" }).should(
"have.attr",
@@ -317,21 +1191,43 @@ describe("Given a Tabstrip", () => {
cy.findByRole("tab", { name: "Transactions" }).should("be.focused");
});
+ it("should restore focus when selected tab removal is async", () => {
+ mountTabsNext();
+
+ cy.findByRole("button", { name: "Home Dismiss tab" }).click();
+ cy.findByRole("tab", { name: "Transactions" })
+ .should("have.attr", "aria-selected", "true")
+ .and("be.focused");
+ });
+
+ it("should call onChange with null when selection moves automatically after removal", () => {
+ const changeSpy = cy.stub().as("changeSpy");
+ mountTabsNext();
+
+ cy.findByRole("button", { name: "Home Dismiss tab" }).click();
+
+ cy.findByRole("tab", { name: "Transactions" })
+ .should("have.attr", "aria-selected", "true")
+ .and("be.focused");
+
+ cy.get("@changeSpy").should("have.been.calledWith", null, "Transactions");
+ });
+
it("should support closing with a keyboard", () => {
- cy.mount();
+ mountTabsNext();
cy.findAllByRole("tab").should("have.length", 5);
cy.realPress("Tab");
cy.findByRole("tab", { name: "Home" }).should("be.focused");
cy.realPress("Tab");
- cy.findByRole("button", { name: "Home Close tab" }).should("be.focused");
+ cy.findByRole("button", { name: "Home Dismiss tab" }).should("be.focused");
cy.realPress("ArrowRight");
cy.findByRole("tab", { name: "Transactions" }).should("be.focused");
cy.realPress("Tab");
- cy.findByRole("button", { name: "Transactions Close tab" }).should(
+ cy.findByRole("button", { name: "Transactions Dismiss tab" }).should(
"be.focused",
);
@@ -339,7 +1235,7 @@ describe("Given a Tabstrip", () => {
cy.findByRole("tab", { name: "Transactions" }).should("be.focused");
cy.realPress(["Shift", "Tab"]);
- cy.findByRole("button", { name: "Home Close tab" }).should("be.focused");
+ cy.findByRole("button", { name: "Home Dismiss tab" }).should("be.focused");
cy.realPress("Enter");
@@ -353,20 +1249,20 @@ describe("Given a Tabstrip", () => {
});
it("should support closing with confirmation", () => {
- cy.mount();
+ mountTabsNext();
cy.findAllByRole("tab").should("have.length", 3);
- cy.findAllByRole("button", { name: "Home Close tab" }).realClick();
+ cy.findAllByRole("button", { name: "Home Dismiss tab" }).click();
cy.findByRole("dialog").should("be.visible");
- cy.findByRole("button", { name: "No" }).realClick();
+ cy.findByRole("button", { name: "No" }).click();
cy.findByRole("dialog").should("not.to.exist");
- cy.findByRole("button", { name: "Home Close tab" }).should("be.focused");
+ cy.findByRole("button", { name: "Home Dismiss tab" }).should("be.focused");
- cy.findAllByRole("button", { name: "Home Close tab" }).realClick();
+ cy.findAllByRole("button", { name: "Home Dismiss tab" }).click();
cy.findByRole("dialog").should("be.visible");
- cy.findByRole("button", { name: "Yes" }).realClick();
+ cy.findByRole("button", { name: "Yes" }).click();
cy.findByRole("dialog").should("not.to.exist");
cy.findAllByRole("tab").should("have.length", 2);
cy.findByRole("tab", { name: "Transactions" }).should(
@@ -378,81 +1274,197 @@ describe("Given a Tabstrip", () => {
});
it("should set tab-index 0 on the panel when it contains non-tabbable elements", () => {
- cy.mount();
+ mountTabsNext();
cy.findByRole("tabpanel").should("have.attr", "tabIndex", "0");
});
it("should not set tab-index 0 on the panel when it contains tabbable elements", () => {
- cy.mount();
+ mountTabsNext();
cy.findByRole("tabpanel").should("not.have.attr", "tabIndex");
});
it("should associate panels with tabs", () => {
- cy.mount();
+ mountTabsNext();
cy.findByRole("tabpanel", { name: "Home" }).should("be.visible");
});
it("should dynamically overflow tabs", () => {
- cy.mount();
- cy.findAllByRole("tab").filter(":visible").should("have.length", 4);
+ mountTabsNext();
+ cy.findAllByRole("tab").should("have.length", 5);
- cy.findByRole("tablist").invoke("css", "max-width", 500);
- cy.wait(500);
- cy.findAllByRole("tab").filter(":visible").should("have.length", 6);
+ cy.window().then((win) => {
+ const dynamicOverflowWindow = win as ResponsiveOverflowWindow;
+ dynamicOverflowWindow.__setDynamicOverflowWidth?.(548);
+ });
+ cy.findAllByRole("tab").should("have.length", 7);
- cy.findByRole("tablist").invoke("css", "max-width", 200);
- cy.wait(500);
- cy.findAllByRole("tab").filter(":visible").should("have.length", 2);
+ cy.window().then((win) => {
+ const dynamicOverflowWindow = win as ResponsiveOverflowWindow;
+ dynamicOverflowWindow.__setDynamicOverflowWidth?.(248);
+ });
+ cy.findAllByRole("tab").should("have.length", 3);
});
- it("should support a controlled API", () => {
- cy.mount();
+ it("should support empty-string tab values", () => {
+ mountTabsNext();
- cy.findByRole("tab", { name: "Home" }).should(
+ cy.findByRole("tab", { name: "Empty" }).should(
"have.attr",
"aria-selected",
"true",
);
+ cy.findByRole("tabpanel", { name: "Empty" }).should("be.visible");
+
+ cy.findByRole("tab", { name: "Transactions" }).click();
- cy.findByRole("tab", { name: "Transactions" }).realClick();
cy.findByRole("tab", { name: "Transactions" }).should(
"have.attr",
"aria-selected",
"true",
);
+ cy.findByRole("tabpanel", { name: "Transactions" }).should("be.visible");
+ });
+
+ it("should keep an empty-string selection visible after selecting it from overflow", () => {
+ mountTabsNext();
+
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
+
+ clickOverflowTab("Empty");
- cy.get("[data-overflowbutton]").realClick();
- cy.findByRole("tab", { name: "Lots" }).realClick();
- cy.findByRole("tab", { name: "Lots" }).should(
+ cy.findByRole("tab", { name: "Empty", selected: true }).should(
+ "be.focused",
+ );
+ cy.findAllByRole("tab").should("have.length", 2);
+ });
+
+ it("should support a controlled API", () => {
+ mountTabsNext(
+
+
+
,
+ );
+
+ cy.findByRole("tab", { name: "Home" }).should(
+ "have.attr",
+ "aria-selected",
+ "true",
+ );
+
+ cy.findByRole("tab", { name: "Transactions" }).click();
+ cy.findByRole("tab", { name: "Transactions" }).should(
"have.attr",
"aria-selected",
"true",
);
- cy.findByRole("button", { name: "Lots Close tab" }).realClick();
- cy.findByRole("tab", { name: "More" })
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
+
+ cy.findByRole("tab", { name: "Loans" }).should("be.focused");
+
+ clickOverflowTab("Lots");
+ cy.findByRole("tab", { name: "Lots", selected: true })
+ .should("be.focused")
+ .and("have.attr", "aria-selected", "true");
+
+ cy.findByRole("button", { name: "Lots Dismiss tab" }).click();
+ cy.findByRole("tab", { name: "Transactions" })
.should("have.attr", "aria-selected", "true")
.and("be.focused");
});
- it(
- "should not cause page overflow when overflow menu is not visible",
- { viewportWidth: 280, viewportHeight: 280 },
- () => {
- cy.get("body").invoke("css", "display", "block");
+ it("should follow visible tab order when navigating with arrow keys after selecting from overflow", () => {
+ mountTabsNext();
- cy.mount();
- cy.findAllByRole("tab").filter(":visible").should("have.length", 2);
+ // Wait for overflow to be ready
+ cy.findAllByRole("tab").should("have.length", 5);
- // no horizontal overflow
- cy.get("html").then((body) => {
- console.log(body[0]);
- const { clientWidth, scrollWidth } = body[0];
- expect(clientWidth).to.equal(scrollWidth);
- });
- },
- );
+ // Open overflow menu and select "With"
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
+
+ clickOverflowTab("With");
+ cy.findByRole("tab", { name: "With", selected: true })
+ .should("be.visible")
+ .and("be.focused");
+
+ // Go to the first tab
+ cy.realPress("Home");
+ cy.findByRole("tab", { name: "Home" }).should("be.focused");
+
+ // Navigate right through all visible tabs
+ cy.realPress("ArrowRight");
+ cy.findByRole("tab", { name: "Transactions" }).should("be.focused");
+
+ cy.realPress("ArrowRight");
+ cy.findByRole("tab", { name: "Loans" }).should("be.focused");
+
+ // "With" should come before Overflow in the navigation order
+ cy.realPress("ArrowRight");
+ cy.findByRole("tab", { name: "With" }).should("be.focused");
+
+ cy.realPress("ArrowRight");
+ cy.findByRole("tab", { name: "Overflow" }).should("be.focused");
+
+ // Wrapping back to Home
+ cy.realPress("ArrowRight");
+ cy.findByRole("tab", { name: "Home" }).should("be.focused");
+ });
+
+ it("should close the overflow menu and move focus past the tablist when Tab is pressed from the menu", () => {
+ mountTabsNext(
+ <>
+
+
+ >,
+ );
+
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
+
+ cy.findByRole("tab", { name: "Liquidity" }).should("be.focused");
+
+ cy.realPress("Tab");
+
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "not.exist",
+ );
+ cy.findByRole("button", { name: "after" }).should("be.focused");
+ });
+
+ it("should close the overflow menu and move focus to the overflow trigger when Shift+Tab is pressed from the menu", () => {
+ mountTabsNext(
+ <>
+
+
+ >,
+ );
+
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
+
+ cy.findByRole("tab", { name: "Liquidity" }).should("be.focused");
+
+ cy.realPress(["Shift", "Tab"]);
+
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "not.exist",
+ );
+ cy.findByRole("tab", { name: "Overflow" }).should("be.focused");
+ });
it(
"should flip overflow menu placement if there is enough space",
@@ -460,15 +1472,16 @@ describe("Given a Tabstrip", () => {
() => {
cy.get("body").invoke("css", "display", "block");
- cy.mount();
- cy.findAllByRole("tab").filter(":visible").should("have.length", 4);
+ mountTabsNext(, { width: 408 });
+ cy.findAllByRole("tab").should("have.length", 5);
- cy.get("[data-overflowbutton]").realClick();
- cy.wait(500);
+ cy.findByRole("tab", { name: "Overflow" }).click();
+ cy.findByRole("tablist", { name: "Overflow tab options" }).should(
+ "be.visible",
+ );
// no horizontal overflow, menu should flip in horizontally
cy.get("html").then((body) => {
- console.log(body[0]);
const { clientWidth, scrollWidth } = body[0];
expect(clientWidth).to.equal(scrollWidth);
});
diff --git a/packages/lab/src/tabs-next/TabBar.css b/packages/lab/src/tabs-next/TabBar.css
index 4ae1feb3335..886b31eef93 100644
--- a/packages/lab/src/tabs-next/TabBar.css
+++ b/packages/lab/src/tabs-next/TabBar.css
@@ -2,9 +2,21 @@
display: flex;
align-items: center;
flex-direction: row;
- gap: var(--salt-spacing-100);
position: relative;
box-sizing: border-box;
+ min-width: 0;
+ max-width: 100%;
+}
+
+.saltTabBar-strip {
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ flex: 1 1 auto;
+ gap: var(--salt-spacing-100);
+ box-sizing: border-box;
+ min-width: 0;
+ max-width: 100%;
}
.saltTabBar-divider::before {
diff --git a/packages/lab/src/tabs-next/TabBar.tsx b/packages/lab/src/tabs-next/TabBar.tsx
index 229a66283a1..fa2e66088fe 100644
--- a/packages/lab/src/tabs-next/TabBar.tsx
+++ b/packages/lab/src/tabs-next/TabBar.tsx
@@ -42,7 +42,7 @@ export const TabBar = forwardRef(
{...rest}
ref={ref}
>
- {children}
+ {children}
);
},
diff --git a/packages/lab/src/tabs-next/TabListLayoutContext.tsx b/packages/lab/src/tabs-next/TabListLayoutContext.tsx
new file mode 100644
index 00000000000..3c52822c0f3
--- /dev/null
+++ b/packages/lab/src/tabs-next/TabListLayoutContext.tsx
@@ -0,0 +1,21 @@
+import { createContext } from "@salt-ds/core";
+import { useContext } from "react";
+
+export type TabSlotLocation = "hidden" | "main" | "overflow";
+
+export interface TabListLayoutContextValue {
+ getLocation: (value: string) => TabSlotLocation;
+ overflowActiveValue: string | null;
+ setOverflowActiveValue: (value: string | null) => void;
+ moveOverflowFocus: (
+ key: "ArrowDown" | "ArrowUp" | "Home" | "End",
+ value: string,
+ ) => boolean;
+}
+
+export const TabListLayoutContext =
+ createContext("TabListLayoutContext", null);
+
+export function useTabListLayout() {
+ return useContext(TabListLayoutContext);
+}
diff --git a/packages/lab/src/tabs-next/TabListNext.css b/packages/lab/src/tabs-next/TabListNext.css
index 4a67dccc4d6..4ad3557d0ed 100644
--- a/packages/lab/src/tabs-next/TabListNext.css
+++ b/packages/lab/src/tabs-next/TabListNext.css
@@ -9,8 +9,8 @@
min-height: calc(var(--salt-size-base) + var(--salt-spacing-100));
gap: var(--salt-spacing-100);
max-width: 100%;
- width: 100%;
min-width: 0;
+ flex: 0 1 auto;
}
.saltTabListNext-center {
@@ -33,6 +33,13 @@
--saltTabListNext-activeColor: var(--salt-container-tertiary-background);
}
-.saltTabListNext-overflowWarning {
- display: none;
+.saltTabListNext-measureContainer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 0;
+ overflow: hidden;
+ pointer-events: none;
+ visibility: hidden;
+ white-space: nowrap;
}
diff --git a/packages/lab/src/tabs-next/TabListNext.tsx b/packages/lab/src/tabs-next/TabListNext.tsx
index b9198ebe48b..f23674769c4 100644
--- a/packages/lab/src/tabs-next/TabListNext.tsx
+++ b/packages/lab/src/tabs-next/TabListNext.tsx
@@ -1,4 +1,10 @@
-import { capitalize, makePrefixer, useForkRef, useId } from "@salt-ds/core";
+import {
+ capitalize,
+ makePrefixer,
+ useAriaAnnouncer,
+ useForkRef,
+ useIsomorphicLayoutEffect,
+} from "@salt-ds/core";
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import { clsx } from "clsx";
@@ -6,15 +12,27 @@ import {
type ComponentPropsWithoutRef,
forwardRef,
type KeyboardEvent,
+ useCallback,
+ useEffect,
+ useMemo,
useRef,
+ useState,
} from "react";
+import { useFocusWithRetry } from "./hooks/useFocusWithRetry";
import { useOverflow } from "./hooks/useOverflow";
-import { useRestoreActiveTab } from "./hooks/useRestoreActiveTab";
+import { useOverflowLayoutState } from "./hooks/useOverflowLayoutState";
+import { useTabListRecovery } from "./hooks/useTabListRecovery";
+import { useTabRemovalHandler } from "./hooks/useTabRemovalHandler";
+import { useTabSelectionFocus } from "./hooks/useTabSelectionFocus";
+import { TabListLayoutContext } from "./TabListLayoutContext";
import tablistNextCss from "./TabListNext.css";
import { TabOverflowList } from "./TabOverflowList";
+import { TabSlot } from "./TabSlot";
+import { TabSlotRegistryContext } from "./TabSlotRegistryContext";
import { useTabsNext } from "./TabsNextContext";
const withBaseName = makePrefixer("saltTabListNext");
+const MAX_FOCUS_RETRY_ATTEMPTS = 120;
export interface TabListNextProps
extends Omit, "onChange"> {
@@ -29,11 +47,10 @@ export interface TabListNextProps
}
export const TabListNext = forwardRef(
- function TabstripNext(props, ref) {
+ function TabListNext(props, ref) {
const {
appearance = "bordered",
activeColor = "primary",
- "aria-describedby": ariaDescribedBy,
children,
className,
onKeyDown,
@@ -47,67 +64,209 @@ export const TabListNext = forwardRef(
});
const {
+ renderMode,
selected,
+ setSelected,
+ setBootstrapOverflowReady,
getNext,
getPrevious,
getFirst,
getLast,
- items,
+ getIndex,
+ item,
+ itemAt,
activeTab,
+ selectionFromOverflowValueRef,
menuOpen,
setMenuOpen,
- removedActiveTabRef,
+ sortItems,
+ getRemovedItems,
+ getRenderedTab,
+ renderedTabs,
+ removalVersion,
} = useTabsNext();
const tabstripRef = useRef(null);
+ const overflowListRef = useRef(null);
+ const slotMapRef = useRef