From 2b890cb8ddc3a822efc612dc0652703a911034ca Mon Sep 17 00:00:00 2001 From: Josh Wooding <12938082+joshwooding@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:26:56 +0100 Subject: [PATCH] Add TabBar --- package.json | 2 +- .../__e2e__/tabs-next/TabsNext.cy.tsx | 44 ++-- packages/lab/src/tabs-next/TabBar.css | 21 ++ packages/lab/src/tabs-next/TabBar.tsx | 44 ++++ packages/lab/src/tabs-next/TabListNext.css | 15 -- packages/lab/src/tabs-next/TabListNext.tsx | 38 +-- packages/lab/src/tabs-next/TabNext.css | 6 +- packages/lab/src/tabs-next/TabNext.tsx | 12 +- .../lab/src/tabs-next/TabOverflowList.tsx | 6 +- packages/lab/src/tabs-next/TabsNext.tsx | 9 +- .../lab/src/tabs-next/hooks/useOverflow.ts | 23 +- packages/lab/src/tabs-next/index.tsx | 1 + .../stories/tabs-next/tabs-next.stories.tsx | 238 ++++++++++-------- site/docs/components/tabs/examples.mdx | 29 +-- .../tabs/{Variants.tsx => ActiveColor.tsx} | 19 +- site/src/examples/tabs/AddANewTab.tsx | 39 +-- site/src/examples/tabs/Appearance.tsx | 34 +++ site/src/examples/tabs/ClosableTabs.tsx | 27 +- site/src/examples/tabs/DisabledTabs.tsx | 22 +- site/src/examples/tabs/Inline.tsx | 18 -- site/src/examples/tabs/Main.tsx | 18 -- site/src/examples/tabs/Overflow.tsx | 18 +- .../src/examples/tabs/SeparatorAndPadding.tsx | 49 ++++ site/src/examples/tabs/WithBadge.tsx | 24 +- site/src/examples/tabs/WithIcon.tsx | 24 +- site/src/examples/tabs/index.ts | 6 +- yarn.lock | 11 +- 27 files changed, 463 insertions(+), 334 deletions(-) create mode 100644 packages/lab/src/tabs-next/TabBar.css create mode 100644 packages/lab/src/tabs-next/TabBar.tsx rename site/src/examples/tabs/{Variants.tsx => ActiveColor.tsx} (81%) create mode 100644 site/src/examples/tabs/Appearance.tsx delete mode 100644 site/src/examples/tabs/Inline.tsx delete mode 100644 site/src/examples/tabs/Main.tsx create mode 100644 site/src/examples/tabs/SeparatorAndPadding.tsx diff --git a/package.json b/package.json index a13bce4aef..e3127fca15 100644 --- a/package.json +++ b/package.json @@ -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": [ 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 e2a193e21f..4acf177242 100644 --- a/packages/lab/src/__tests__/__e2e__/tabs-next/TabsNext.cy.tsx +++ b/packages/lab/src/__tests__/__e2e__/tabs-next/TabsNext.cy.tsx @@ -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, @@ -13,13 +13,13 @@ const { describe("Given a Tabstrip", () => { it("should render with tablist and tab roles", () => { - cy.mount(
); + cy.mount(); cy.findByRole("tablist").should("be.visible"); cy.findAllByRole("tab").should("have.length", 5); }); it("should support keyboard navigation and wrap", () => { - cy.mount(
); + cy.mount(); cy.realPress("Tab"); cy.findByRole("tab", { name: "Home" }).should("have.focus"); cy.realPress("ArrowRight"); @@ -36,7 +36,7 @@ describe("Given a Tabstrip", () => { it("should support selection with a mouse", () => { const changeSpy = cy.stub().as("changeSpy"); - cy.mount(
); + cy.mount(); cy.findByRole("tab", { name: "Home" }).should( "have.attr", "aria-selected", @@ -58,7 +58,7 @@ describe("Given a Tabstrip", () => { it("should support selection with the keyboard", () => { const changeSpy = cy.stub().as("changeSpy"); - cy.mount(
); + cy.mount(); cy.realPress("Tab"); cy.findByRole("tab", { name: "Home" }).should("have.focus"); cy.findByRole("tab", { name: "Home" }).should( @@ -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(); 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(); 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"); @@ -155,7 +155,7 @@ 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", @@ -163,7 +163,7 @@ describe("Given a Tabstrip", () => { "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", () => { @@ -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(); @@ -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", () => { @@ -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); @@ -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(); - 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", diff --git a/packages/lab/src/tabs-next/TabBar.css b/packages/lab/src/tabs-next/TabBar.css new file mode 100644 index 0000000000..2f5081e730 --- /dev/null +++ b/packages/lab/src/tabs-next/TabBar.css @@ -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); +} diff --git a/packages/lab/src/tabs-next/TabBar.tsx b/packages/lab/src/tabs-next/TabBar.tsx new file mode 100644 index 0000000000..6d6dd8714e --- /dev/null +++ b/packages/lab/src/tabs-next/TabBar.tsx @@ -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( + function TabBar(props, ref) { + const { className, children, separator, padding, ...rest } = props; + + const targetWindow = useWindow(); + useComponentCssInjection({ + testId: "salt-tab-bar", + css: tabBarCss, + window: targetWindow, + }); + + return ( +
+ {children} +
+ ); + }, +); diff --git a/packages/lab/src/tabs-next/TabListNext.css b/packages/lab/src/tabs-next/TabListNext.css index dba1199f6c..817097dc5d 100644 --- a/packages/lab/src/tabs-next/TabListNext.css +++ b/packages/lab/src/tabs-next/TabListNext.css @@ -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); } @@ -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); } diff --git a/packages/lab/src/tabs-next/TabListNext.tsx b/packages/lab/src/tabs-next/TabListNext.tsx index b67e8864c6..fe23e32a55 100644 --- a/packages/lab/src/tabs-next/TabListNext.tsx +++ b/packages/lab/src/tabs-next/TabListNext.tsx @@ -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"; @@ -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. */ @@ -45,13 +40,12 @@ export interface TabListNextProps export const TabListNext = forwardRef( function TabstripNext(props, ref) { const { + appearance = "bordered", activeColor = "primary", children, className, - onAdd, onClose, onKeyDown, - variant = "main", ...rest } = props; const targetWindow = useWindow(); @@ -68,12 +62,12 @@ export const TabListNext = forwardRef( getPrevious, getFirst, getLast, + item, items, } = useTabsNext(); const tabstripRef = useRef(null); const handleRef = useForkRef(tabstripRef, ref); - const addButtonRef = useRef(null); const overflowButtonRef = useRef(null); const [menuOpen, setMenuOpen] = useState(false); @@ -82,7 +76,6 @@ export const TabListNext = forwardRef( tabs: items, children, selected, - addButton: addButtonRef, overflowButton: overflowButtonRef, }); @@ -111,13 +104,17 @@ export const TabListNext = forwardRef( 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 }); } @@ -138,7 +135,7 @@ export const TabListNext = forwardRef( role="tablist" className={clsx( withBaseName(), - withBaseName(variant), + withBaseName(appearance), withBaseName("horizontal"), withBaseName(`activeColor${capitalize(activeColor)}`), className, @@ -157,17 +154,6 @@ export const TabListNext = forwardRef( > {hidden} - {onAdd && ( - - )} ); diff --git a/packages/lab/src/tabs-next/TabNext.css b/packages/lab/src/tabs-next/TabNext.css index 5dfdc57d88..2ca6cdbec0 100644 --- a/packages/lab/src/tabs-next/TabNext.css +++ b/packages/lab/src/tabs-next/TabNext.css @@ -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; } @@ -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); diff --git a/packages/lab/src/tabs-next/TabNext.tsx b/packages/lab/src/tabs-next/TabNext.tsx index 5c3fb4a308..c3e71197d0 100644 --- a/packages/lab/src/tabs-next/TabNext.tsx +++ b/packages/lab/src/tabs-next/TabNext.tsx @@ -73,14 +73,16 @@ export const TabNext = forwardRef( const handleClick = (event: MouseEvent) => { onClick?.(event); - setSelected(event, value); + + if (!id) return; + setSelected(event, id); }; const handleKeyDown = (event: KeyboardEvent) => { onKeyDown?.(event); - if (event.key === "Enter" || event.key === " ") { - setSelected(event, value); + if (id && (event.key === "Enter" || event.key === " ")) { + setSelected(event, id); } }; @@ -120,7 +122,9 @@ export const TabNext = forwardRef( }; const handleCloseButtonClick = (event: MouseEvent) => { - handleClose(event, value); + if (!id) return; + + handleClose(event, id); event.stopPropagation(); }; diff --git a/packages/lab/src/tabs-next/TabOverflowList.tsx b/packages/lab/src/tabs-next/TabOverflowList.tsx index cd966a9edf..495da2af92 100644 --- a/packages/lab/src/tabs-next/TabOverflowList.tsx +++ b/packages/lab/src/tabs-next/TabOverflowList.tsx @@ -100,12 +100,11 @@ export const TabOverflowList = forwardRef( useDismissWithEscape(handleDismiss, open); const handleClick = () => { - setOpen((old) => !old); - setTimeout(() => { + if (!open) { listRef.current ?.querySelectorAll('[role="tab"]')[0] ?.focus({ preventScroll: true }); - }); + } }; const handleFocus = () => { @@ -113,6 +112,7 @@ export const TabOverflowList = forwardRef( }; const handleBlur = (event: FocusEvent) => { + console.log(event.currentTarget, event.relatedTarget); if (!event.currentTarget.contains(event.relatedTarget)) { setOpen(false); } diff --git a/packages/lab/src/tabs-next/TabsNext.tsx b/packages/lab/src/tabs-next/TabsNext.tsx index da9deb78e9..0c7a96f000 100644 --- a/packages/lab/src/tabs-next/TabsNext.tsx +++ b/packages/lab/src/tabs-next/TabsNext.tsx @@ -57,14 +57,17 @@ export const TabsNext = forwardRef( const setSelected = useCallback( (event: SyntheticEvent, action: string) => { - setSelectedState(action); + const newItem = item(action); + + if (!newItem) return; + + setSelectedState(newItem.value); + onChange?.(event, newItem.value); setTimeout(() => { const itemElement = item(action)?.element; itemElement?.focus({ preventScroll: true }); }, 0); - - onChange?.(event, action); }, [onChange, item], ); diff --git a/packages/lab/src/tabs-next/hooks/useOverflow.ts b/packages/lab/src/tabs-next/hooks/useOverflow.ts index 7ff0aed016..bef152a063 100644 --- a/packages/lab/src/tabs-next/hooks/useOverflow.ts +++ b/packages/lab/src/tabs-next/hooks/useOverflow.ts @@ -18,37 +18,30 @@ interface UseOverflowProps { selected?: string; children: ReactNode; tabs: Item[]; - addButton: RefObject; overflowButton: RefObject; } function getAvailableWidth({ container, targetWindow, - addButton, -}: Pick & { +}: { + container: UseOverflowProps["container"]; targetWindow: Window; }) { if (!container.current || !targetWindow) return 0; const containerStyles = targetWindow.getComputedStyle(container.current); - const gap = Number.parseInt(containerStyles.gap || "0"); const containerPadding = Number.parseInt(containerStyles.paddingLeft || "0") + Number.parseInt(containerStyles.paddingRight || "0"); - const addButtonWidth = addButton.current - ? addButton.current.offsetWidth + gap - : 0; - - return container.current.clientWidth - containerPadding - addButtonWidth; + return container.current.clientWidth - containerPadding; } export function useOverflow({ tabs, container, - addButton, overflowButton, children, selected, @@ -74,7 +67,6 @@ export function useOverflow({ let maxWidth = getAvailableWidth({ container, targetWindow, - addButton, }); const containerStyles = targetWindow.getComputedStyle( @@ -152,14 +144,7 @@ export function useOverflow({ }; } }); - }, [ - setVisibleItems, - targetWindow, - container, - addButton, - overflowButton, - tabs.length, - ]); + }, [setVisibleItems, targetWindow, container, overflowButton, tabs.length]); // biome-ignore lint/correctness/useExhaustiveDependencies: we want to update when selected changes. useIsomorphicLayoutEffect(() => { diff --git a/packages/lab/src/tabs-next/index.tsx b/packages/lab/src/tabs-next/index.tsx index 1b37b05e69..74231c2b4b 100644 --- a/packages/lab/src/tabs-next/index.tsx +++ b/packages/lab/src/tabs-next/index.tsx @@ -2,3 +2,4 @@ export * from "./TabNext"; export * from "./TabListNext"; export * from "./TabsNext"; export * from "./TabNextPanel"; +export * from "./TabBar"; diff --git a/packages/lab/stories/tabs-next/tabs-next.stories.tsx b/packages/lab/stories/tabs-next/tabs-next.stories.tsx index 1f332e9650..9d4d6628e7 100644 --- a/packages/lab/stories/tabs-next/tabs-next.stories.tsx +++ b/packages/lab/stories/tabs-next/tabs-next.stories.tsx @@ -8,12 +8,12 @@ import { FormField, FormFieldLabel, Input, - Panel, RadioButton, RadioButtonGroup, StackLayout, } from "@salt-ds/core"; import { + AddIcon, BankCheckIcon, CreditCardIcon, HomeIcon, @@ -21,6 +21,7 @@ import { ReceiptIcon, } from "@salt-ds/icons"; import { + TabBar, TabListNext, type TabListNextProps, TabNext, @@ -68,17 +69,19 @@ const lotsOfTabs = [ "Screens", ]; -export const Main: StoryFn = (args) => { +export const Bordered: StoryFn = (args) => { return (
- - {tabs.map((label) => ( - - {label} - - ))} - + + + {tabs.map((label) => ( + + {label} + + ))} + + {tabs.map((label) => ( {label} @@ -89,7 +92,7 @@ export const Main: StoryFn = (args) => { ); }; -Main.args = { +Bordered.args = { defaultValue: tabs[0], }; @@ -97,13 +100,14 @@ export const Inline: StoryFn = (args) => { return (
- + {tabs.map((label) => ( {label} ))} + {tabs.map((label) => ( {label} @@ -128,22 +132,24 @@ const tabToIcon: Record = { export const WithIcon: StoryFn = (args) => { return ( -
+
- - {tabs.map((label) => { - const Icon = tabToIcon[label]; - return ( - - {label} - - ); - })} - + + + {tabs.map((label) => { + const Icon = tabToIcon[label]; + return ( + + {label} + + ); + })} + +
); @@ -157,14 +163,16 @@ export const WithBadge: StoryFn = (args) => { return (
- - {tabs.map((label) => ( - - {label} - {label === "Transactions" && } - - ))} - + + + {tabs.map((label) => ( + + {label} + {label === "Transactions" && } + + ))} + +
); @@ -177,13 +185,15 @@ WithBadge.args = { export const Overflow: StoryFn = (args) => { return ( - - {lotsOfTabs.map((label) => ( - - {label} - - ))} - + + + {lotsOfTabs.map((label) => ( + + {label} + + ))} + + ); }; @@ -204,17 +214,19 @@ export const Closable: StoryFn = (args) => { return (
- { - setTabs(tabs.filter((tab) => tab !== closedTab)); - }} - > - {tabs.map((label) => ( - 1}> - {label} - - ))} - + + { + setTabs(tabs.filter((tab) => tab !== closedTab)); + }} + > + {tabs.map((label) => ( + 1}> + {label} + + ))} + +
); @@ -228,13 +240,15 @@ export const DisabledTabs: StoryFn = (args) => { return (
- - {tabs.map((label) => ( - - {label} - - ))} - + + + {tabs.map((label) => ( + + {label} + + ))} + + {tabs.map((label) => ( {label} @@ -261,20 +275,27 @@ export const AddTabs: StoryFn = (args) => { value={value} onChange={(_event, newValue) => setValue(newValue)} > - { - const newTab = `New Tab${newCount.current > 0 ? ` ${newCount.current}` : ""}`; - newCount.current += 1; - - setTabs((old) => old.concat(newTab)); - }} - > - {tabs.map((label) => ( - - {label} - - ))} - + + + {tabs.map((label) => ( + + {label} + + ))} + + +
); @@ -292,7 +313,7 @@ export const Backgrounds = (): ReactElement => {
- + {tabs.map((label) => ( {label} @@ -379,30 +400,47 @@ export const AddWithDialog = () => { }; return ( -
+
- { - setConfirmationOpen(true); - }} - > - {tabs.map((label) => ( - - {label} - - ))} - + + + {tabs.map((label) => ( + + {label} + + ))} + + +
); }; -function CloseConfirmationDialog({ open, onConfirm, onCancel, valueToRemove }) { +function CloseConfirmationDialog({ + open, + onConfirm, + onCancel, + valueToRemove, +}: { + open?: boolean; + onConfirm: () => void; + onCancel: () => void; + valueToRemove?: string; +}) { return ( @@ -434,7 +472,7 @@ export const CloseWithConfirmation = () => { }; return ( -
+
{ onCancel={handleCancel} /> - { - setValueToRemove(closedTab); - }} - > - {tabs.map((label) => ( - 1}> - {label} - - ))} - + + { + setValueToRemove(closedTab); + }} + > + {tabs.map((label) => ( + 1}> + {label} + + ))} + +
); diff --git a/site/docs/components/tabs/examples.mdx b/site/docs/components/tabs/examples.mdx index 9b817f6caf..7500b09ac4 100644 --- a/site/docs/components/tabs/examples.mdx +++ b/site/docs/components/tabs/examples.mdx @@ -9,29 +9,22 @@ data: --- - + -## Main tabstrip - -The main tabstrip uses a border to separate the tabs from the rest of the page. You can align tabs inside the tabstrip can be aligned to the left, center or right. Use it to organize content across the main region of an interface under the app header. - -### Best practices - -The main tabstrip should span the width of the page. +## Appearance +Two different appearances are available for tabs: Bordered and Transparent. + - + -## Inline tabstrip - -The inline tabstrip has no separator between the tabs and the content below. Use it within an area of a page to switch between related content. Like the main tabstrip, you can align the tabs to the left, center or right. - -### Best practices +## Separator and padding -Don’t use inline tabs standalone outside of the content region it is nested within. +Tabs can be shown with a separator or padding to help create visual alignment in your UI, + @@ -97,9 +90,11 @@ Tabs can be set as disabled using the `disabled` prop. - + -## Variants +## Active color + +Tabs supports three active colors: "primary", "secondary" and "tertiary". diff --git a/site/src/examples/tabs/Variants.tsx b/site/src/examples/tabs/ActiveColor.tsx similarity index 81% rename from site/src/examples/tabs/Variants.tsx rename to site/src/examples/tabs/ActiveColor.tsx index a289c57298..d77ee8ac49 100644 --- a/site/src/examples/tabs/Variants.tsx +++ b/site/src/examples/tabs/ActiveColor.tsx @@ -7,6 +7,7 @@ import { StackLayout, } from "@salt-ds/core"; import { + TabBar, TabListNext, type TabListNextProps, TabNext, @@ -17,7 +18,7 @@ import { type ChangeEvent, type ReactElement, useState } from "react"; const tabs = ["Home", "Transactions", "Loans"]; -export const Variants = (): ReactElement => { +export const ActiveColor = (): ReactElement => { const [variant, setVariant] = useState("primary"); @@ -29,13 +30,15 @@ export const Variants = (): ReactElement => {
- - {tabs.map((label) => ( - - {label} - - ))} - + + + {tabs.map((label) => ( + + {label} + + ))} + + {tabs.map((label) => ( {label} diff --git a/site/src/examples/tabs/AddANewTab.tsx b/site/src/examples/tabs/AddANewTab.tsx index 4efca4cd1e..8fc185b1ad 100644 --- a/site/src/examples/tabs/AddANewTab.tsx +++ b/site/src/examples/tabs/AddANewTab.tsx @@ -1,4 +1,6 @@ -import { TabListNext, TabNext, TabsNext } from "@salt-ds/lab"; +import { Button } from "@salt-ds/core"; +import { AddIcon } from "@salt-ds/icons"; +import { TabBar, TabListNext, TabNext, TabsNext } from "@salt-ds/lab"; import React, { type ReactElement, useRef, useState } from "react"; export const AddANewTab = (): ReactElement => { @@ -8,22 +10,27 @@ export const AddANewTab = (): ReactElement => { return ( setValue(newValue)}> - { - const newTab = `New Tab${newCount.current > 0 ? ` ${newCount.current}` : ""}`; - newCount.current += 1; + + + {tabs.map((label) => ( + + {label} + + ))} + + + ); }; diff --git a/site/src/examples/tabs/Appearance.tsx b/site/src/examples/tabs/Appearance.tsx new file mode 100644 index 0000000000..b1a8e2c8da --- /dev/null +++ b/site/src/examples/tabs/Appearance.tsx @@ -0,0 +1,34 @@ +import { StackLayout } from "@salt-ds/core"; +import { TabBar, TabListNext, TabNext, TabsNext } from "@salt-ds/lab"; +import type { ReactElement } from "react"; + +const tabs = ["Home", "Transactions", "Loans", "Checks", "Liquidity"]; + +export const Appearance = (): ReactElement => { + return ( + + + + + {tabs.map((label) => ( + + {label} + + ))} + + + + + + + {tabs.map((label) => ( + + {label} + + ))} + + + + + ); +}; diff --git a/site/src/examples/tabs/ClosableTabs.tsx b/site/src/examples/tabs/ClosableTabs.tsx index 702a0a04c9..289719cefb 100644 --- a/site/src/examples/tabs/ClosableTabs.tsx +++ b/site/src/examples/tabs/ClosableTabs.tsx @@ -1,5 +1,4 @@ -import { Badge } from "@salt-ds/core"; -import { TabListNext, TabNext, TabsNext } from "@salt-ds/lab"; +import { TabBar, TabListNext, TabNext, TabsNext } from "@salt-ds/lab"; import { type ReactElement, useState } from "react"; export const ClosableTabs = (): ReactElement => { @@ -13,17 +12,19 @@ export const ClosableTabs = (): ReactElement => { return ( - { - setTabs(tabs.filter((tab) => tab !== closedTab)); - }} - > - {tabs.map((label) => ( - 1}> - {label} - - ))} - + + { + setTabs(tabs.filter((tab) => tab !== closedTab)); + }} + > + {tabs.map((label) => ( + 1}> + {label} + + ))} + + ); }; diff --git a/site/src/examples/tabs/DisabledTabs.tsx b/site/src/examples/tabs/DisabledTabs.tsx index 6dc99c74e7..78b3516696 100644 --- a/site/src/examples/tabs/DisabledTabs.tsx +++ b/site/src/examples/tabs/DisabledTabs.tsx @@ -1,4 +1,4 @@ -import { TabListNext, TabNext, TabsNext } from "@salt-ds/lab"; +import { TabBar, TabListNext, TabNext, TabsNext } from "@salt-ds/lab"; import type { ReactElement } from "react"; const tabs = ["Home", "Transactions", "Loans", "Checks", "Liquidity"]; @@ -6,15 +6,17 @@ const tabs = ["Home", "Transactions", "Loans", "Checks", "Liquidity"]; export const DisabledTabs = (): ReactElement => { return ( - - {tabs.map((label) => { - return ( - - {label} - - ); - })} - + + + {tabs.map((label) => { + return ( + + {label} + + ); + })} + + ); }; diff --git a/site/src/examples/tabs/Inline.tsx b/site/src/examples/tabs/Inline.tsx deleted file mode 100644 index 565e4810b5..0000000000 --- a/site/src/examples/tabs/Inline.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { TabListNext, TabNext, TabsNext } from "@salt-ds/lab"; -import type { ReactElement } from "react"; - -const tabs = ["Home", "Transactions", "Loans", "Checks", "Liquidity"]; - -export const Inline = (): ReactElement => { - return ( - - - {tabs.map((label) => ( - - {label} - - ))} - - - ); -}; diff --git a/site/src/examples/tabs/Main.tsx b/site/src/examples/tabs/Main.tsx deleted file mode 100644 index 9ce5b16646..0000000000 --- a/site/src/examples/tabs/Main.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { TabListNext, TabNext, TabsNext } from "@salt-ds/lab"; -import type { ReactElement } from "react"; - -const tabs = ["Home", "Transactions", "Loans", "Checks", "Liquidity"]; - -export const Main = (): ReactElement => { - return ( - - - {tabs.map((label) => ( - - {label} - - ))} - - - ); -}; diff --git a/site/src/examples/tabs/Overflow.tsx b/site/src/examples/tabs/Overflow.tsx index aebac6bfe0..c472860dcf 100644 --- a/site/src/examples/tabs/Overflow.tsx +++ b/site/src/examples/tabs/Overflow.tsx @@ -1,4 +1,4 @@ -import { TabListNext, TabNext, TabsNext } from "@salt-ds/lab"; +import { TabBar, TabListNext, TabNext, TabsNext } from "@salt-ds/lab"; import type { ReactElement } from "react"; const tabs = ["Home", "Transactions", "Loans", "Checks", "Liquidity"]; @@ -6,13 +6,15 @@ const tabs = ["Home", "Transactions", "Loans", "Checks", "Liquidity"]; export const Overflow = (): ReactElement => { return ( - - {tabs.map((label) => ( - - {label} - - ))} - + + + {tabs.map((label) => ( + + {label} + + ))} + + ); }; diff --git a/site/src/examples/tabs/SeparatorAndPadding.tsx b/site/src/examples/tabs/SeparatorAndPadding.tsx new file mode 100644 index 0000000000..d5cf7addc8 --- /dev/null +++ b/site/src/examples/tabs/SeparatorAndPadding.tsx @@ -0,0 +1,49 @@ +import { StackLayout, Switch } from "@salt-ds/core"; +import { TabBar, TabListNext, TabNext, TabsNext } from "@salt-ds/lab"; +import { type ReactElement, useState } from "react"; + +const tabs = ["Home", "Transactions", "Loans", "Checks", "Liquidity"]; + +export const SeparatorAndPadding = (): ReactElement => { + const [separator, setSeparator] = useState(true); + const [padding, setPadding] = useState(true); + + return ( + + + + + {tabs.map((label) => ( + + {label} + + ))} + + + + + + + {tabs.map((label) => ( + + {label} + + ))} + + + + + setSeparator(event.target.checked)} + /> + setPadding(event.target.checked)} + /> + + + ); +}; diff --git a/site/src/examples/tabs/WithBadge.tsx b/site/src/examples/tabs/WithBadge.tsx index bdda23d847..68aa468fbf 100644 --- a/site/src/examples/tabs/WithBadge.tsx +++ b/site/src/examples/tabs/WithBadge.tsx @@ -1,5 +1,5 @@ import { Badge } from "@salt-ds/core"; -import { TabListNext, TabNext, TabsNext } from "@salt-ds/lab"; +import { TabBar, TabListNext, TabNext, TabsNext } from "@salt-ds/lab"; import type { ReactElement } from "react"; const tabs = ["Home", "Transactions", "Loans", "Checks", "Liquidity"]; @@ -12,16 +12,18 @@ const notifications: Record<(typeof tabs)[number], number> = { export const WithBadge = (): ReactElement => { return ( - - {tabs.map((label) => ( - - {label} - {notifications[label] > 0 ? ( - - ) : undefined} - - ))} - + + + {tabs.map((label) => ( + + {label} + {notifications[label] > 0 ? ( + + ) : undefined} + + ))} + + ); }; diff --git a/site/src/examples/tabs/WithIcon.tsx b/site/src/examples/tabs/WithIcon.tsx index e29a95e233..c3337e1c93 100644 --- a/site/src/examples/tabs/WithIcon.tsx +++ b/site/src/examples/tabs/WithIcon.tsx @@ -5,7 +5,7 @@ import { LineChartIcon, ReceiptIcon, } from "@salt-ds/icons"; -import { TabListNext, TabNext, TabsNext } from "@salt-ds/lab"; +import { TabBar, TabListNext, TabNext, TabsNext } from "@salt-ds/lab"; import type { ComponentType, ReactElement } from "react"; const tabs = ["Home", "Transactions", "Loans", "Checks", "Liquidity"]; @@ -21,16 +21,18 @@ const tabToIcon: Record = { export const WithIcon = (): ReactElement => { return ( - - {tabs.map((label) => { - const Icon = tabToIcon[label]; - return ( - - {label} - - ); - })} - + + + {tabs.map((label) => { + const Icon = tabToIcon[label]; + return ( + + {label} + + ); + })} + + ); }; diff --git a/site/src/examples/tabs/index.ts b/site/src/examples/tabs/index.ts index 760321cee0..0108716f74 100644 --- a/site/src/examples/tabs/index.ts +++ b/site/src/examples/tabs/index.ts @@ -1,9 +1,9 @@ -export * from "./Main"; -export * from "./Inline"; +export * from "./Appearance"; export * from "./WithIcon"; export * from "./WithBadge"; export * from "./AddANewTab"; export * from "./ClosableTabs"; export * from "./DisabledTabs"; export * from "./Overflow"; -export * from "./Variants"; +export * from "./ActiveColor"; +export * from "./SeparatorAndPadding"; diff --git a/yarn.lock b/yarn.lock index 6e57b4b462..401a66da4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4107,11 +4107,10 @@ __metadata: languageName: node linkType: hard -"@joshwooding/vite-plugin-react-docgen-typescript@npm:0.4.0": - version: 0.4.0 - resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.4.0" +"@joshwooding/vite-plugin-react-docgen-typescript@npm:0.4.1": + version: 0.4.1 + resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.4.1" dependencies: - glob: "npm:^10.0.0" magic-string: "npm:^0.27.0" react-docgen-typescript: "npm:^2.2.2" peerDependencies: @@ -4120,7 +4119,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/96ab538a3b01a5dd81869ea52df2fd7cd989a6648f79e8c8cf9fb30a2614620c7e986bdce62724bdf9c277be22dd52b006481207c1ee134c8f9e19135f5b56ef + checksum: 10/f4ac95967a221b37fc0e93dafeb0c5b2496bdb27c1822581889ba102734fd69b185d7da02df6fd5e7c7670f805137e0cf8440cf8f3cb6b84c3c134833ada7fae languageName: node linkType: hard @@ -15273,7 +15272,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.0.0, glob@npm:^10.3.10, glob@npm:^10.3.7": +"glob@npm:^10.3.10, glob@npm:^10.3.7": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: