Skip to content

Commit

Permalink
Add TabBar
Browse files Browse the repository at this point in the history
  • Loading branch information
joshwooding committed Oct 4, 2024
1 parent 4d535c5 commit 2b890cb
Show file tree
Hide file tree
Showing 27 changed files with 463 additions and 334 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@
"webpack": "5.94.0",
"recursive-readdir": "2.2.3",
"@storybook/test@npm:8.2.4": "patch:@storybook/test@npm%3A8.2.4#~/.yarn/patches/@storybook-test-npm-8.2.4-0a53c854b7.patch",
"@joshwooding/vite-plugin-react-docgen-typescript": "0.4.0"
"@joshwooding/vite-plugin-react-docgen-typescript": "0.4.1"
},
"browserslist": {
"production": [
Expand Down
44 changes: 22 additions & 22 deletions packages/lab/src/__tests__/__e2e__/tabs-next/TabsNext.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as tabsStories from "@stories/tabs-next/tabs-next.stories";
import { composeStories } from "@storybook/react";

const {
Main,
Bordered,
DisabledTabs,
Overflow,
AddTabs,
Expand All @@ -13,13 +13,13 @@ const {

describe("Given a Tabstrip", () => {
it("should render with tablist and tab roles", () => {
cy.mount(<Main />);
cy.mount(<Bordered />);
cy.findByRole("tablist").should("be.visible");
cy.findAllByRole("tab").should("have.length", 5);
});

it("should support keyboard navigation and wrap", () => {
cy.mount(<Main />);
cy.mount(<Bordered />);
cy.realPress("Tab");
cy.findByRole("tab", { name: "Home" }).should("have.focus");
cy.realPress("ArrowRight");
Expand All @@ -36,7 +36,7 @@ describe("Given a Tabstrip", () => {

it("should support selection with a mouse", () => {
const changeSpy = cy.stub().as("changeSpy");
cy.mount(<Main onChange={changeSpy} />);
cy.mount(<Bordered onChange={changeSpy} />);
cy.findByRole("tab", { name: "Home" }).should(
"have.attr",
"aria-selected",
Expand All @@ -58,7 +58,7 @@ describe("Given a Tabstrip", () => {

it("should support selection with the keyboard", () => {
const changeSpy = cy.stub().as("changeSpy");
cy.mount(<Main onChange={changeSpy} />);
cy.mount(<Bordered onChange={changeSpy} />);
cy.realPress("Tab");
cy.findByRole("tab", { name: "Home" }).should("have.focus");
cy.findByRole("tab", { name: "Home" }).should(
Expand Down Expand Up @@ -116,17 +116,17 @@ describe("Given a Tabstrip", () => {
it("should overflow into a menu when there is not enough space to show all tabs", () => {
cy.mount(<Overflow />);
cy.findAllByRole("tab").should("have.length", 17);
cy.findAllByRole("tab").filter(":visible").should("have.length", 3);
cy.findAllByRole("tab").filter(":not(:visible)").should("have.length", 14);
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");
});

it("should allow keyboard navigation in the menu", () => {
cy.mount(<Overflow />);
cy.get("[data-overflowbutton]").realClick();
cy.findByRole("tab", { name: "Checks" }).should("be.focused");
cy.realPress("ArrowDown");
cy.findByRole("tab", { name: "Liquidity" }).should("be.focused");
cy.realPress("ArrowDown");
cy.findByRole("tab", { name: "Reports" }).should("be.focused");
cy.realPress("End");
cy.findByRole("tab", { name: "Screens" }).should("be.focused");
cy.realPress("Escape");
Expand Down Expand Up @@ -155,15 +155,15 @@ describe("Given a Tabstrip", () => {
"aria-selected",
"true",
);
cy.findByRole("button", { name: "Add Tab" }).realClick();
cy.findByRole("button", { name: "Add tab" }).realClick();
cy.findAllByRole("tab").should("have.length", 4);
cy.findByRole("tab", { name: "Home" }).should(
"have.attr",
"aria-selected",
"true",
);
cy.findByRole("tab", { name: "New tab" }).should("be.visible");
cy.findByRole("button", { name: "Add Tab" }).should("be.focused");
cy.findByRole("button", { name: "Add tab" }).should("be.focused");
});

it("should support adding tabs with confirmation", () => {
Expand All @@ -174,10 +174,10 @@ describe("Given a Tabstrip", () => {
"aria-selected",
"true",
);
cy.findByRole("button", { name: "Add Tab" }).realClick();
cy.findByRole("button", { name: "Add tab" }).realClick();

cy.findByRole("dialog").should("be.visible");
cy.findByLabelText("New Tab name").realClick();
cy.findByLabelText("New tab name").realClick();
cy.realType("New tab");
cy.findByRole("button", { name: "Confirm" }).realClick();

Expand All @@ -188,7 +188,7 @@ describe("Given a Tabstrip", () => {
"aria-selected",
"true",
);
cy.findByRole("button", { name: "Add Tab" }).should("be.focused");
cy.findByRole("button", { name: "Add tab" }).should("be.focused");
});

it("should support closing tabs with a mouse", () => {
Expand Down Expand Up @@ -217,7 +217,7 @@ describe("Given a Tabstrip", () => {
"aria-selected",
"true",
);
cy.findByRole("tab", { name: "Checks" }).should("be.focused");
cy.findByRole("tab", { name: "Transactions" }).should("be.focused");

cy.findByRole("button", { name: "Close tab Home" }).realClick();
cy.findAllByRole("tab").should("have.length", 2);
Expand Down Expand Up @@ -264,23 +264,23 @@ describe("Given a Tabstrip", () => {
cy.findByRole("tab", { name: "Transactions" }).should("be.focused");
});

it("should support close with confirmation", () => {
it("should support closing with confirmation", () => {
cy.mount(<CloseWithConfirmation />);
cy.findAllByRole("tab").should("have.length", 5);
cy.findAllByRole("tab").should("have.length", 3);

cy.findAllByRole("button", { name: "Close tab Home" }).realClick();
cy.findByRole("dialog").should("be.visible");

cy.findByRole("button", { name: "Cancel" }).realClick();
cy.findByRole("dialog").should("not.be.visible");
cy.findByRole("button", { name: "No" }).realClick();
cy.findByRole("dialog").should("not.to.exist");
cy.findByRole("button", { name: "Close tab Home" }).should("be.focused");

cy.findAllByRole("button", { name: "Close tab Home" }).realClick();
cy.findByRole("dialog").should("be.visible");

cy.findByRole("button", { name: "Confirm" }).realClick();
cy.findByRole("dialog").should("not.be.visible");
cy.findAllByRole("tab").should("have.length", 4);
cy.findByRole("button", { name: "Yes" }).realClick();
cy.findByRole("dialog").should("not.to.exist");
cy.findAllByRole("tab").should("have.length", 2);
cy.findByRole("tab", { name: "Transactions" }).should(
"have.attr",
"aria-selected",
Expand Down
21 changes: 21 additions & 0 deletions packages/lab/src/tabs-next/TabBar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.saltTabBar {
display: flex;
align-items: center;
flex-direction: row;
gap: var(--salt-spacing-100);
position: relative;
box-sizing: border-box;
}

.saltTabBar-separator::before {
content: "";
position: absolute;
inset: auto 0 0 0;
height: var(--salt-size-border);
border-bottom: var(--salt-size-border) var(--salt-separable-borderStyle) var(--salt-separable-secondary-borderColor);
}

.saltTabBar-padding {
padding-left: var(--salt-spacing-300);
padding-right: var(--salt-spacing-300);
}
44 changes: 44 additions & 0 deletions packages/lab/src/tabs-next/TabBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import { type ComponentPropsWithRef, forwardRef } from "react";

import { makePrefixer } from "@salt-ds/core";
import { clsx } from "clsx";
import tabBarCss from "./TabBar.css";

export interface TabBarProps extends ComponentPropsWithRef<"div"> {
separator?: boolean;
padding?: boolean;
}

const withBaseName = makePrefixer("saltTabBar");

export const TabBar = forwardRef<HTMLDivElement, TabBarProps>(
function TabBar(props, ref) {
const { className, children, separator, padding, ...rest } = props;

const targetWindow = useWindow();
useComponentCssInjection({
testId: "salt-tab-bar",
css: tabBarCss,
window: targetWindow,
});

return (
<div
className={clsx(
withBaseName(),
{
[withBaseName("separator")]: separator,
[withBaseName("padding")]: padding,
},
className,
)}
{...rest}
ref={ref}
>
{children}
</div>
);
},
);
15 changes: 0 additions & 15 deletions packages/lab/src/tabs-next/TabListNext.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
align-items: center;
position: relative;
background: transparent;
width: 100%;
min-height: calc(var(--salt-size-base) + var(--salt-spacing-100));
gap: var(--salt-spacing-100);
}
Expand All @@ -19,20 +18,6 @@
justify-content: flex-end;
}

.saltTabListNext-main {
padding-left: var(--salt-spacing-300);
padding-right: var(--salt-spacing-300);
box-sizing: border-box;
}

.saltTabListNext-main::before {
content: "";
position: absolute;
inset: auto 0 0 0;
height: var(--salt-size-border);
border-bottom: var(--salt-size-border) var(--salt-separable-borderStyle) var(--salt-separable-secondary-borderColor);
}

.saltTabListNext-activeColorPrimary {
--saltTabListNext-activeColor: var(--salt-container-primary-background);
}
Expand Down
38 changes: 12 additions & 26 deletions packages/lab/src/tabs-next/TabListNext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Button, capitalize, makePrefixer, useForkRef } from "@salt-ds/core";
import { AddIcon } from "@salt-ds/icons";
import { capitalize, makePrefixer, useForkRef } from "@salt-ds/core";
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import clsx from "clsx";
Expand Down Expand Up @@ -29,13 +28,9 @@ export interface TabListNextProps
*/
activeColor?: "primary" | "secondary" | "tertiary";
/**
* The tab variant, "main" should be shown at the top of the page under the app header. "inline" should be used everywhere else. Defaults to "main".
* The appearance of the tabs. Defaults to "bordered".
*/
variant?: "main" | "inline";
/**
* Callback fired when add button is triggered.
*/
onAdd?: () => void;
appearance?: "bordered" | "transparent";
/**
* Callback fired when a tab is closed.
*/
Expand All @@ -45,13 +40,12 @@ export interface TabListNextProps
export const TabListNext = forwardRef<HTMLDivElement, TabListNextProps>(
function TabstripNext(props, ref) {
const {
appearance = "bordered",
activeColor = "primary",
children,
className,
onAdd,
onClose,
onKeyDown,
variant = "main",
...rest
} = props;
const targetWindow = useWindow();
Expand All @@ -68,12 +62,12 @@ export const TabListNext = forwardRef<HTMLDivElement, TabListNextProps>(
getPrevious,
getFirst,
getLast,
item,
items,
} = useTabsNext();

const tabstripRef = useRef<HTMLDivElement>(null);
const handleRef = useForkRef(tabstripRef, ref);
const addButtonRef = useRef<HTMLButtonElement>(null);
const overflowButtonRef = useRef<HTMLButtonElement>(null);
const [menuOpen, setMenuOpen] = useState(false);

Expand All @@ -82,7 +76,6 @@ export const TabListNext = forwardRef<HTMLDivElement, TabListNextProps>(
tabs: items,
children,
selected,
addButton: addButtonRef,
overflowButton: overflowButtonRef,
});

Expand Down Expand Up @@ -111,13 +104,17 @@ export const TabListNext = forwardRef<HTMLDivElement, TabListNextProps>(

const handleClose = useCallback(
(event: SyntheticEvent, id: string) => {
const currentItem = item(id);
const firstItem = getFirst();
const newActive = id === firstItem?.id ? getNext(id) : getPrevious(id);
onClose?.(event, id);

if (currentItem == null) return;

onClose?.(event, currentItem.value);

if (!newActive) return;
if (id === selected) {
setSelected(event, newActive.value);
setSelected(event, newActive.id);
} else {
newActive?.element?.focus({ preventScroll: true });
}
Expand All @@ -138,7 +135,7 @@ export const TabListNext = forwardRef<HTMLDivElement, TabListNextProps>(
role="tablist"
className={clsx(
withBaseName(),
withBaseName(variant),
withBaseName(appearance),
withBaseName("horizontal"),
withBaseName(`activeColor${capitalize(activeColor)}`),
className,
Expand All @@ -157,17 +154,6 @@ export const TabListNext = forwardRef<HTMLDivElement, TabListNextProps>(
>
{hidden}
</TabOverflowList>
{onAdd && (
<Button
ref={addButtonRef}
aria-label="Add Tab"
appearance="transparent"
sentiment="neutral"
onClick={onAdd}
>
<AddIcon aria-hidden />
</Button>
)}
</div>
</TabListNextContext.Provider>
);
Expand Down
6 changes: 3 additions & 3 deletions packages/lab/src/tabs-next/TabNext.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@
height: var(--salt-size-indicator);
}

.saltTabListNext-main .saltTabNext::after {
.saltTabListNext-bordered .saltTabNext::after {
top: 0;
}

.saltTabListNext-inline .saltTabNext::after {
.saltTabListNext-transparent .saltTabNext::after {
bottom: 0;
}

Expand All @@ -76,7 +76,7 @@
outline: var(--salt-focused-outline);
}

.saltTabListNext-main .saltTabNext.saltTabNext-selected {
.saltTabListNext-bordered .saltTabNext.saltTabNext-selected {
background: var(--saltTabListNext-activeColor);
border-left: var(--salt-size-border) var(--salt-separable-borderStyle) var(--salt-separable-secondary-borderColor);
border-right: var(--salt-size-border) var(--salt-separable-borderStyle) var(--salt-separable-secondary-borderColor);
Expand Down
Loading

0 comments on commit 2b890cb

Please sign in to comment.