diff --git a/package.json b/package.json index de6481eb6..98bb83b89 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "homepage": "https://github.com/buildo/bento-design-system#readme", "dependencies": { "@dessert-box/react": "^0.2.0", + "@react-aria/breadcrumbs": "^3.1.5", "@react-aria/button": "^3.3.4", "@react-aria/link": "^3.2.0", "@react-aria/separator": "^3.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 019688825..8c5abd4fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,7 @@ specifiers: '@babel/preset-react': ^7.16.0 '@babel/preset-typescript': ^7.16.0 '@dessert-box/react': ^0.2.0 + '@react-aria/breadcrumbs': ^3.1.5 '@react-aria/button': ^3.3.4 '@react-aria/link': ^3.2.0 '@react-aria/separator': ^3.1.3 @@ -58,6 +59,7 @@ specifiers: dependencies: '@dessert-box/react': 0.2.0_react@17.0.2 + '@react-aria/breadcrumbs': 3.1.5_react@17.0.2 '@react-aria/button': 3.3.4_react@17.0.2 '@react-aria/link': 3.2.0_react@17.0.2 '@react-aria/separator': 3.1.3_react@17.0.2 @@ -1779,6 +1781,40 @@ packages: - supports-color dev: true + /@formatjs/ecma402-abstract/1.11.3: + resolution: {integrity: sha512-kP/Buv5vVFMAYLHNvvUzr0lwRTU0u2WTy44Tqwku1X3C3lJ5dKqDCYVqA8wL+Y19Bq+MwHgxqd5FZJRCIsLRyQ==} + dependencies: + '@formatjs/intl-localematcher': 0.2.24 + tslib: 2.3.1 + dev: false + + /@formatjs/fast-memoize/1.2.1: + resolution: {integrity: sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==} + dependencies: + tslib: 2.3.1 + dev: false + + /@formatjs/icu-messageformat-parser/2.0.18: + resolution: {integrity: sha512-vquIzsAJJmZ5jWVH8dEgUKcbG4yu3KqtyPet+q35SW5reLOvblkfeCXTRW2TpIwNXzdVqsJBwjbTiRiSU9JxwQ==} + dependencies: + '@formatjs/ecma402-abstract': 1.11.3 + '@formatjs/icu-skeleton-parser': 1.3.5 + tslib: 2.3.1 + dev: false + + /@formatjs/icu-skeleton-parser/1.3.5: + resolution: {integrity: sha512-Nhyo2/6kG7ZfgeEfo02sxviOuBcvtzH6SYUharj3DLCDJH3A/4OxkKcmx/2PWGX4bc6iSieh+FA94CsKDxnZBQ==} + dependencies: + '@formatjs/ecma402-abstract': 1.11.3 + tslib: 2.3.1 + dev: false + + /@formatjs/intl-localematcher/0.2.24: + resolution: {integrity: sha512-K/HRGo6EMnCbhpth/y3u4rW4aXkmQNqRe1L2G+Y5jNr3v0gYhvaucV8WixNju/INAMbPBlbsRBRo/nfjnoOnxQ==} + dependencies: + tslib: 2.3.1 + dev: false + /@gar/promisify/1.1.2: resolution: {integrity: sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw==} dev: true @@ -1798,6 +1834,25 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true + /@internationalized/date/3.0.0-alpha.1: + resolution: {integrity: sha512-fxciU4AQ/4XBYfse/mT9h1nsyNkmQkxwQtTmQVu6b4Tp2u95Y3m5BNgWgV2m3vLiiKZ82NtHJXAIGoqiK53w4g==} + dependencies: + '@babel/runtime': 7.16.3 + dev: false + + /@internationalized/message/3.0.3: + resolution: {integrity: sha512-TpNLP6FgzD9kukdNOhcxYhULf1mcE7Du+eMZe5voSP/yWlAl9GsJqPVY2knsR5Ld1oQELhxU61griqn6uhGsnA==} + dependencies: + '@babel/runtime': 7.16.3 + intl-messageformat: 9.11.4 + dev: false + + /@internationalized/number/3.0.3: + resolution: {integrity: sha512-ewFoVvsxSyd9QZnknvOWPjirYqdMQhXTeDhJg3hM6C/FeZt0banpGH1nZ0SGMZXHz8NK9uAa2KVIq+jqAIOg4w==} + dependencies: + '@babel/runtime': 7.16.3 + dev: false + /@istanbuljs/load-nyc-config/1.1.0: resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -2186,6 +2241,21 @@ packages: resolution: {integrity: sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ==} dev: true + /@react-aria/breadcrumbs/3.1.5_react@17.0.2: + resolution: {integrity: sha512-0ruIP6gP4hkGyX/b3g8MeuaP7ZX9M4mvauPHvuqGHNpUAZdESMj4jHo5ERImaTUJTObC2Vid2674OyzYFITSUA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 + dependencies: + '@babel/runtime': 7.16.3 + '@react-aria/i18n': 3.3.4_react@17.0.2 + '@react-aria/interactions': 3.7.0_react@17.0.2 + '@react-aria/link': 3.2.0_react@17.0.2 + '@react-aria/utils': 3.11.0_react@17.0.2 + '@react-types/breadcrumbs': 3.2.1_react@17.0.2 + '@react-types/shared': 3.10.1_react@17.0.2 + react: 17.0.2 + dev: false + /@react-aria/button/3.3.4_react@17.0.2: resolution: {integrity: sha512-vebTcf9YpwaKCvsca2VWhn6eYPa15OJtMENwaGop72UrL35Oa7xDgU0RG22RAjRjt8HRVlAfLpHkJQW6GBGU3g==} peerDependencies: @@ -2213,6 +2283,21 @@ packages: react: 17.0.2 dev: false + /@react-aria/i18n/3.3.4_react@17.0.2: + resolution: {integrity: sha512-1DV3I82UfL2dT8WBI/88TwtokO80B7ISSyuz6rO/6n7q76A/nC2AtVINbrGYrcKsCcxCEoEMxW5RVJ39fcLijA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 + dependencies: + '@babel/runtime': 7.16.3 + '@internationalized/date': 3.0.0-alpha.1 + '@internationalized/message': 3.0.3 + '@internationalized/number': 3.0.3 + '@react-aria/ssr': 3.1.0_react@17.0.2 + '@react-aria/utils': 3.11.0_react@17.0.2 + '@react-types/shared': 3.10.1_react@17.0.2 + react: 17.0.2 + dev: false + /@react-aria/interactions/3.7.0_react@17.0.2: resolution: {integrity: sha512-Xomchjb9bqvh3ocil+QCEYFSxsTy8PHEz43mNP6z2yuu3UqTpl2FsWfyKgF/Yy0WKVkyV2dO2uz758KJTCLZhw==} peerDependencies: @@ -2318,6 +2403,15 @@ packages: react: 17.0.2 dev: false + /@react-types/breadcrumbs/3.2.1_react@17.0.2: + resolution: {integrity: sha512-njXfiYTlACKAz5xVp34tXb7gtm6avzgzrkYT70r3HHk8g7cBUS7iJPiSIgCRxUGwIpesIYeZY3a1Nvqzvohgmg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 + dependencies: + '@react-types/shared': 3.10.1_react@17.0.2 + react: 17.0.2 + dev: false + /@react-types/button/3.4.1_react@17.0.2: resolution: {integrity: sha512-B54M84LxdEppwjXNlkBEJyMfe9fd+bvFV7R6+NJvupGrZm/LuFNYjFcHk7yjMKWTdWm6DbpIuQz54n5qTW7Vlg==} peerDependencies: @@ -9977,6 +10071,15 @@ packages: resolution: {integrity: sha512-2Vkz8z46Dv401zTWudDGwO7KiGHNDkMv417T5ItcNYfmvHR/1qCTVBO9vwH8zZmQ0WkA/1ARwpysR9bsnop4NQ==} dev: true + /intl-messageformat/9.11.4: + resolution: {integrity: sha512-77TSkNubIy/hsapz6LQpyR6OADcxhWdhSaboPb5flMaALCVkPvAIxr48AlPqaMl4r1anNcvR9rpLWVdwUY1IKg==} + dependencies: + '@formatjs/ecma402-abstract': 1.11.3 + '@formatjs/fast-memoize': 1.2.1 + '@formatjs/icu-messageformat-parser': 2.0.18 + tslib: 2.3.1 + dev: false + /invariant/2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} dependencies: @@ -15503,7 +15606,6 @@ packages: /tslib/2.3.1: resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} - dev: true /tsup/5.11.11_typescript@4.5.2: resolution: {integrity: sha512-rgbTu+KhAI9PdGUS07rKohDXbRLTstBGJaxl75q7RZYRGF+n+kN8L4RlXY5pqYb9Hsq0gEB6nS39v7nSvVBS+g==} diff --git a/src/Breadcrumb/createBreadcrumb.tsx b/src/Breadcrumb/createBreadcrumb.tsx new file mode 100644 index 000000000..0ca97e71f --- /dev/null +++ b/src/Breadcrumb/createBreadcrumb.tsx @@ -0,0 +1,92 @@ +import { useBreadcrumbItem, useBreadcrumbs } from "@react-aria/breadcrumbs"; +import { useRef, Fragment, FunctionComponent } from "react"; +import { IconProps } from "src/Icons/IconProps"; +import { IconChevronRight, Label, LinkProps, LocalizedString } from "../"; +import { Box, Inline, BentoSprinkles } from "../internal"; + +type LastItem = { + label: LocalizedString; +}; + +type Item = LastItem & { + href: string; +}; + +export type BreadcrumbProps = { + items: [...Item[], LastItem]; +}; + +type BreadcrumbItemProps = LastItem & Partial & { isCurrent: boolean }; + +type BreadcrumbConfig = { + separator: FunctionComponent; + separatorSize: IconProps["size"]; + space: BentoSprinkles["gap"]; +}; + +export function createBreadcrumb( + Link: FunctionComponent, + config: BreadcrumbConfig = { + separator: IconChevronRight, + separatorSize: "8", + space: "16", + } +) { + const BreadcrumbItem = createBreadcrumbItem(Link); + const Separator = config.separator; + return function Breadcrumb(props: BreadcrumbProps) { + const children = ( + + + {props.items.map((item, idx) => { + const isCurrent = idx === props.items.length - 1; + return ( + + + {!isCurrent && ( + + )} + + ); + })} + + + ); + const { navProps } = useBreadcrumbs({ children }); + return ( + + {children} + + ); + }; +} + +function createBreadcrumbItem(Link: FunctionComponent) { + return function BreadcrumbItem({ isCurrent, label, href = "" }: BreadcrumbItemProps) { + const ref = useRef(null); + const { itemProps } = useBreadcrumbItem( + { children: label, isCurrent, elementType: "div" }, + ref + ); + + return ( + + {isCurrent ? ( + + ) : ( + + )} + + ); + }; +} diff --git a/src/Icons/IconChevronRight.tsx b/src/Icons/IconChevronRight.tsx new file mode 100644 index 000000000..c77bd3db4 --- /dev/null +++ b/src/Icons/IconChevronRight.tsx @@ -0,0 +1,10 @@ +import { IconProps } from "./IconProps"; +import { svgIconProps } from "./svgIconProps"; + +export function IconChevronRight(props: IconProps) { + return ( + + + + ); +} diff --git a/src/Icons/index.ts b/src/Icons/index.ts index b706720cb..fce2b9446 100644 --- a/src/Icons/index.ts +++ b/src/Icons/index.ts @@ -1,4 +1,5 @@ export * from "./IconCheckCircleSolid"; +export * from "./IconChevronRight"; export * from "./IconClose"; export * from "./IconInformative"; export * from "./IconNegative"; diff --git a/src/index.ts b/src/index.ts index 83ccf8f5e..0ce5d7b70 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ export * from "./Banner/createBanner"; export * from "./Box/createBentoBox"; +export * from "./Breadcrumb/createBreadcrumb"; export * from "./Button/createButton"; export * from "./Divider/Divider"; export * from "./Field/createFormFields"; diff --git a/stories/Components/Breadcrumb.stories.tsx b/stories/Components/Breadcrumb.stories.tsx new file mode 100644 index 000000000..b71d2cf16 --- /dev/null +++ b/stories/Components/Breadcrumb.stories.tsx @@ -0,0 +1,37 @@ +import { Breadcrumb, unsafeLocalizedString } from "../"; +import { createComponentStories } from "../util"; + +const { defaultExport, createStory } = createComponentStories({ + component: Breadcrumb, + args: {}, +}); + +export default defaultExport; + +export const breadcrumb = createStory({ + items: [ + { + label: unsafeLocalizedString("Root"), + href: "https://www.example.com", + }, + { + label: unsafeLocalizedString("1st Level"), + href: "https://www.example.com", + }, + { + label: unsafeLocalizedString("2nd Level"), + href: "https://www.example.com", + }, + { + label: unsafeLocalizedString("3rd Level"), + href: "https://www.example.com", + }, + { + label: unsafeLocalizedString("4th Level"), + href: "https://www.example.com", + }, + { + label: unsafeLocalizedString("5th Level"), + }, + ], +}); diff --git a/stories/index.tsx b/stories/index.tsx index 86e42cc51..ebdc4f692 100644 --- a/stories/index.tsx +++ b/stories/index.tsx @@ -1,12 +1,13 @@ import "../src/reset.css"; import { + createBanner, createBentoBox, - createLayoutComponents, - createFormFields, + createBreadcrumb, createButton, - createBanner, - createToast, + createFormFields, + createLayoutComponents, createLink, + createToast, } from "../src"; import { sprinkles } from "./sprinkles.css"; @@ -18,3 +19,4 @@ export const Button = createButton({}); export const Banner = createBanner({}); export const { Toast, ToastProvider } = createToast(Button, {}); export const Link = createLink(); +export const Breadcrumb = createBreadcrumb(Link);