From 30bff9d094faaad5a3af8738722aed428b6b9963 Mon Sep 17 00:00:00 2001 From: Cameron Joyner Date: Wed, 22 Apr 2026 10:54:22 -0400 Subject: [PATCH 1/5] scaffolding --- .../BreadcrumbsNext.stories.tsx | 119 ++++++++++++++++++ .../breadcrumbs-next/breadcrumbsNext.test.tsx | 32 +++++ .../breadcrumbs-next/breadcrumbsNext.tsx | 21 ++++ packages/core/src/components/index.ts | 1 + 4 files changed, 173 insertions(+) create mode 100644 packages/core/src/components/breadcrumbs-next/BreadcrumbsNext.stories.tsx create mode 100644 packages/core/src/components/breadcrumbs-next/breadcrumbsNext.test.tsx create mode 100644 packages/core/src/components/breadcrumbs-next/breadcrumbsNext.tsx diff --git a/packages/core/src/components/breadcrumbs-next/BreadcrumbsNext.stories.tsx b/packages/core/src/components/breadcrumbs-next/BreadcrumbsNext.stories.tsx new file mode 100644 index 00000000000..789ac459983 --- /dev/null +++ b/packages/core/src/components/breadcrumbs-next/BreadcrumbsNext.stories.tsx @@ -0,0 +1,119 @@ +/* + * (c) Copyright 2026 Palantir Technologies Inc. All rights reserved. + */ + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { storybookLayoutDecorator, StoryLabel } from "@storybook-common"; +import React from "react"; + +import { Boundary } from "../../common/boundary"; +import type { BreadcrumbProps } from "../breadcrumbs/breadcrumb"; + +import { BreadcrumbsNext } from "./breadcrumbsNext"; + +const SAMPLE_ITEMS: BreadcrumbProps[] = [ + { text: "Home", href: "#", icon: "home" }, + { text: "Projects", href: "#", icon: "projects" }, + { text: "Blueprint", href: "#" }, + { text: "Components", href: "#" }, + { text: "Breadcrumbs" }, +]; + +type StoryArgs = React.ComponentProps & { width?: number }; + +const meta: Meta = { + title: "Core/BreadcrumbsNext", + component: BreadcrumbsNext, + decorators: [storybookLayoutDecorator], + tags: ["autodocs"], + args: { + items: SAMPLE_ITEMS, + collapseFrom: Boundary.START, + width: 400, + }, + argTypes: { + collapseFrom: { + control: "select", + options: Object.values(Boundary), + }, + minVisibleItems: { + control: "number", + }, + width: { control: { type: "range", min: 100, max: 800, step: 10 } }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** + * A basic breadcrumbs component with default styling. Adjust the width slider to see overflow behavior. + */ +export const Default: Story = { + render: ({ width, ...args }) => ( +
+ +
+ ), +}; + +/** + * Use the `collapseFrom` prop to control which end of the breadcrumb trail is collapsed when items overflow. + */ +export const CollapseFromExample: Story = { + name: "Collapse From", + argTypes: { + collapseFrom: { table: { disable: true } }, + }, + render: ({ width, ...args }) => ( +
+
+ + +
+
+ + +
+
+ ), +}; + +/** + * When the breadcrumb trail contains many items, overflow is handled by collapsing items into a dropdown. + */ +export const OverflowExample: Story = { + name: "Overflow", + render: args => ( +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ ), +}; + +/** + * Interactive playground with all props toggleable via Storybook controls. + */ +export const Playground: Story = { + args: { + items: SAMPLE_ITEMS, + collapseFrom: Boundary.START, + minVisibleItems: 0, + }, + render: ({ width, ...args }) => ( +
+ +
+ ), +}; diff --git a/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.test.tsx b/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.test.tsx new file mode 100644 index 00000000000..a2708afc6e4 --- /dev/null +++ b/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.test.tsx @@ -0,0 +1,32 @@ +/* + * (c) Copyright 2026 Palantir Technologies Inc. All rights reserved. + */ + +import { render, screen } from "@testing-library/react"; + +import { describe, expect, it } from "@blueprintjs/test-commons/vitest"; + +import { Classes } from "../../common"; +import type { BreadcrumbProps } from "../breadcrumbs/breadcrumb"; + +import { BreadcrumbsNext } from "./breadcrumbsNext"; + +const ITEMS: BreadcrumbProps[] = [{ text: "1" }, { text: "2" }, { text: "3" }]; + +describe("", () => { + it("renders without crashing", () => { + render(); + expect(screen.getByRole("list")).toBeInTheDocument(); + }); + + it("has correct displayName", () => { + expect(BreadcrumbsNext.displayName).toBe("Blueprint6.BreadcrumbsNext"); + }); + + it("renders breadcrumb items with correct classes", () => { + render(); + const list = screen.getByRole("list"); + expect(list).toHaveClass(Classes.BREADCRUMBS); + expect(screen.getAllByRole("listitem")).toHaveLength(3); + }); +}); diff --git a/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.tsx b/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.tsx new file mode 100644 index 00000000000..15067b4edd0 --- /dev/null +++ b/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.tsx @@ -0,0 +1,21 @@ +/* + * (c) Copyright 2026 Palantir Technologies Inc. All rights reserved. + */ + +import { DISPLAYNAME_PREFIX } from "../../common"; +import { Breadcrumbs, type BreadcrumbsProps } from "../breadcrumbs/breadcrumbs"; + +export type BreadcrumbsNextProps = Omit; + +/** + * BreadcrumbsNext component. + * + * Thin wrapper around `Breadcrumbs` that omits the `popoverProps` prop. + * + * @see https://blueprintjs.com/docs/#core/components/breadcrumbs + */ +export const BreadcrumbsNext: React.FC = props => { + return ; +}; + +BreadcrumbsNext.displayName = `${DISPLAYNAME_PREFIX}.BreadcrumbsNext`; diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts index 448a9fda8b0..51911acf5c4 100644 --- a/packages/core/src/components/index.ts +++ b/packages/core/src/components/index.ts @@ -17,6 +17,7 @@ export { Alert, type AlertProps } from "./alert/alert"; export { Breadcrumb, type BreadcrumbProps } from "./breadcrumbs/breadcrumb"; export { Breadcrumbs, type BreadcrumbsProps } from "./breadcrumbs/breadcrumbs"; +export { BreadcrumbsNext, type BreadcrumbsNextProps } from "./breadcrumbs-next/breadcrumbsNext"; export { AnchorButton, Button } from "./button/buttons"; export type { AnchorButtonProps, From ff84dc105ae1341271272732b073780b38ac0fb5 Mon Sep 17 00:00:00 2001 From: Cameron Joyner Date: Wed, 22 Apr 2026 11:59:05 -0400 Subject: [PATCH 2/5] thin shim --- .../BreadcrumbsNext.stories.tsx | 47 +------------------ .../breadcrumbs-next/breadcrumbsNext.tsx | 14 +++++- 2 files changed, 14 insertions(+), 47 deletions(-) diff --git a/packages/core/src/components/breadcrumbs-next/BreadcrumbsNext.stories.tsx b/packages/core/src/components/breadcrumbs-next/BreadcrumbsNext.stories.tsx index 789ac459983..123bcd69ff9 100644 --- a/packages/core/src/components/breadcrumbs-next/BreadcrumbsNext.stories.tsx +++ b/packages/core/src/components/breadcrumbs-next/BreadcrumbsNext.stories.tsx @@ -3,7 +3,7 @@ */ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { storybookLayoutDecorator, StoryLabel } from "@storybook-common"; +import { storybookLayoutDecorator } from "@storybook-common"; import React from "react"; import { Boundary } from "../../common/boundary"; @@ -57,51 +57,6 @@ export const Default: Story = { ), }; -/** - * Use the `collapseFrom` prop to control which end of the breadcrumb trail is collapsed when items overflow. - */ -export const CollapseFromExample: Story = { - name: "Collapse From", - argTypes: { - collapseFrom: { table: { disable: true } }, - }, - render: ({ width, ...args }) => ( -
-
- - -
-
- - -
-
- ), -}; - -/** - * When the breadcrumb trail contains many items, overflow is handled by collapsing items into a dropdown. - */ -export const OverflowExample: Story = { - name: "Overflow", - render: args => ( -
-
- -
- -
-
-
- -
- -
-
-
- ), -}; - /** * Interactive playground with all props toggleable via Storybook controls. */ diff --git a/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.tsx b/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.tsx index 15067b4edd0..466269a52e7 100644 --- a/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.tsx +++ b/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.tsx @@ -1,5 +1,17 @@ /* - * (c) Copyright 2026 Palantir Technologies Inc. All rights reserved. + * Copyright 2026 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ import { DISPLAYNAME_PREFIX } from "../../common"; From 44f823e122d31bf02f70619dbcbf3578ef6ce9ea Mon Sep 17 00:00:00 2001 From: Cameron Joyner Date: Wed, 22 Apr 2026 12:03:25 -0400 Subject: [PATCH 3/5] larger scaffolding --- .../breadcrumbs-next/breadcrumbsNext.tsx | 158 +++++++++++++++++- 1 file changed, 151 insertions(+), 7 deletions(-) diff --git a/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.tsx b/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.tsx index 466269a52e7..ae8b5972de1 100644 --- a/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.tsx +++ b/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.tsx @@ -14,20 +14,164 @@ * limitations under the License. */ -import { DISPLAYNAME_PREFIX } from "../../common"; -import { Breadcrumbs, type BreadcrumbsProps } from "../breadcrumbs/breadcrumbs"; +import classNames from "classnames"; +import { memo, useCallback } from "react"; -export type BreadcrumbsNextProps = Omit; +import { Boundary, Classes, DISPLAYNAME_PREFIX, type Props, removeNonHTMLProps } from "../../common"; +import { Menu } from "../menu/menu"; +import { MenuItem } from "../menu/menuItem"; +import { OverflowList, type OverflowListProps } from "../overflow-list/overflowList"; +import { PopoverNext } from "../popover-next/popoverNext"; +import { Breadcrumb, type BreadcrumbProps } from "../breadcrumbs/breadcrumb"; + +const EMPTY_ITEMS: readonly BreadcrumbProps[] = []; + +export interface BreadcrumbsNextProps extends Props { + /** + * Callback invoked to render visible breadcrumbs. Best practice is to + * render a `` element. If `currentBreadcrumbRenderer` is also + * supplied, that callback will be used for the current breadcrumb instead. + * + * @default Breadcrumb + */ + breadcrumbRenderer?: (props: BreadcrumbProps) => React.JSX.Element; + + /** + * Which direction the breadcrumbs should collapse from: start or end. + * + * @default Boundary.START + */ + collapseFrom?: Boundary; + + /** + * Callback invoked to render the current breadcrumb, which is the last + * element in the `items` array. + * + * If this prop is omitted, `breadcrumbRenderer` will be invoked for the + * current breadcrumb instead. + */ + currentBreadcrumbRenderer?: (props: BreadcrumbProps) => React.JSX.Element; + + /** + * All breadcrumbs to display. Breadcrumbs that do not fit in the container + * will be rendered in an overflow menu instead. + * + * @default [] + */ + items?: readonly BreadcrumbProps[]; + + /** + * The minimum number of visible breadcrumbs that should never collapse into + * the overflow menu, regardless of DOM dimensions. + * + * @default 0 + */ + minVisibleItems?: number; + + /** + * Props to spread to the `OverflowList` popover target. + */ + overflowButtonProps?: React.HTMLProps; + + /** + * Props to spread to `OverflowList`. Note that `items`, + * `overflowRenderer`, and `visibleItemRenderer` cannot be changed. + */ + overflowListProps?: Partial< + Omit, "items" | "overflowRenderer" | "visibleItemRenderer"> + >; +} /** * BreadcrumbsNext component. * - * Thin wrapper around `Breadcrumbs` that omits the `popoverProps` prop. + * Uses PopoverNext (Floating UI) for the overflow menu instead of Popover (Popper.js). + * Does not expose popover configuration props, making it forward-compatible with + * future popover implementation changes. * * @see https://blueprintjs.com/docs/#core/components/breadcrumbs */ -export const BreadcrumbsNext: React.FC = props => { - return ; -}; +export const BreadcrumbsNext: React.FC = memo(props => { + const { + breadcrumbRenderer, + className, + collapseFrom = Boundary.START, + currentBreadcrumbRenderer, + items = EMPTY_ITEMS, + minVisibleItems = 0, + overflowButtonProps, + overflowListProps = {}, + } = props; + + const renderBreadcrumb = useCallback( + (breadcrumbProps: BreadcrumbProps, isCurrent: boolean) => { + if (isCurrent && currentBreadcrumbRenderer != null) { + return currentBreadcrumbRenderer(breadcrumbProps); + } else if (breadcrumbRenderer != null) { + return breadcrumbRenderer(breadcrumbProps); + } else { + return ; + } + }, + [breadcrumbRenderer, currentBreadcrumbRenderer], + ); + + const renderBreadcrumbWrapper = useCallback( + (breadcrumbProps: BreadcrumbProps, index: number) => { + const isCurrent = items[items.length - 1] === breadcrumbProps; + return
  • {renderBreadcrumb(breadcrumbProps, isCurrent)}
  • ; + }, + [items, renderBreadcrumb], + ); + + const renderOverflowBreadcrumb = useCallback((breadcrumbProps: BreadcrumbProps, index: number) => { + const isClickable = breadcrumbProps.href != null || breadcrumbProps.onClick != null; + const htmlProps = removeNonHTMLProps(breadcrumbProps); + return ; + }, []); + + const renderOverflow = useCallback( + (overflowItems: readonly BreadcrumbProps[]) => { + let orderedItems = overflowItems; + if (collapseFrom === Boundary.START) { + orderedItems = overflowItems.slice().reverse(); + } + + return ( +
  • + {orderedItems.map(renderOverflowBreadcrumb)}} + disabled={orderedItems.length === 0} + placement={collapseFrom === Boundary.END ? "bottom-end" : "bottom-start"} + > + + +
  • + ); + }, + [collapseFrom, overflowButtonProps, renderOverflowBreadcrumb], + ); + + return ( + + ); +}); BreadcrumbsNext.displayName = `${DISPLAYNAME_PREFIX}.BreadcrumbsNext`; From 248035700d7916c4b98cc8338bf20966300db9e0 Mon Sep 17 00:00:00 2001 From: Cameron Joyner Date: Wed, 22 Apr 2026 13:00:25 -0400 Subject: [PATCH 4/5] lint --- .../core/src/components/breadcrumbs-next/breadcrumbsNext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.tsx b/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.tsx index ae8b5972de1..df89dbd6a0b 100644 --- a/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.tsx +++ b/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.tsx @@ -18,11 +18,11 @@ import classNames from "classnames"; import { memo, useCallback } from "react"; import { Boundary, Classes, DISPLAYNAME_PREFIX, type Props, removeNonHTMLProps } from "../../common"; +import { Breadcrumb, type BreadcrumbProps } from "../breadcrumbs/breadcrumb"; import { Menu } from "../menu/menu"; import { MenuItem } from "../menu/menuItem"; import { OverflowList, type OverflowListProps } from "../overflow-list/overflowList"; import { PopoverNext } from "../popover-next/popoverNext"; -import { Breadcrumb, type BreadcrumbProps } from "../breadcrumbs/breadcrumb"; const EMPTY_ITEMS: readonly BreadcrumbProps[] = []; From a5e926f5386387cd3467657efb33d3e05ddd0b56 Mon Sep 17 00:00:00 2001 From: Cameron Joyner Date: Wed, 22 Apr 2026 14:06:42 -0400 Subject: [PATCH 5/5] remove bloat --- .../breadcrumbs-next/breadcrumbsNext.test.tsx | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 packages/core/src/components/breadcrumbs-next/breadcrumbsNext.test.tsx diff --git a/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.test.tsx b/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.test.tsx deleted file mode 100644 index a2708afc6e4..00000000000 --- a/packages/core/src/components/breadcrumbs-next/breadcrumbsNext.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * (c) Copyright 2026 Palantir Technologies Inc. All rights reserved. - */ - -import { render, screen } from "@testing-library/react"; - -import { describe, expect, it } from "@blueprintjs/test-commons/vitest"; - -import { Classes } from "../../common"; -import type { BreadcrumbProps } from "../breadcrumbs/breadcrumb"; - -import { BreadcrumbsNext } from "./breadcrumbsNext"; - -const ITEMS: BreadcrumbProps[] = [{ text: "1" }, { text: "2" }, { text: "3" }]; - -describe("", () => { - it("renders without crashing", () => { - render(); - expect(screen.getByRole("list")).toBeInTheDocument(); - }); - - it("has correct displayName", () => { - expect(BreadcrumbsNext.displayName).toBe("Blueprint6.BreadcrumbsNext"); - }); - - it("renders breadcrumb items with correct classes", () => { - render(); - const list = screen.getByRole("list"); - expect(list).toHaveClass(Classes.BREADCRUMBS); - expect(screen.getAllByRole("listitem")).toHaveLength(3); - }); -});