@purpurds/breadcrumbs 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/meta.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ export type MetaListItem = {
2
+ "@type": string;
3
+ position: number;
4
+ name: string;
5
+ item: string;
6
+ };
7
+ type MakeMetaListItem = (name: string, item: string, position: number) => MetaListItem;
8
+ export declare const metaListItem: MakeMetaListItem;
9
+ export declare const metaSchema: (itemListElement: MetaListItem[]) => string;
10
+ export {};
11
+ //# sourceMappingURL=meta.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.d.ts","sourceRoot":"","sources":["../src/meta.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,KAAK,gBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,YAAY,CAAC;AAEvF,eAAO,MAAM,YAAY,EAAE,gBAKzB,CAAC;AAEH,eAAO,MAAM,UAAU,oBAAqB,YAAY,EAAE,WAKtD,CAAC"}
@@ -0,0 +1 @@
1
+ ._purpur-breadcrumbs_hceki_1{font-family:var(--purpur-typography-family-default);padding:var(--purpur-spacing-150) 0}._purpur-breadcrumbs_hceki_1 a{font-size:var(--purpur-typography-scale-75);text-underline-offset:3px;padding:14px 2px;letter-spacing:.3px}._purpur-breadcrumbs_hceki_1 a:hover,._purpur-breadcrumbs_hceki_1 a:active{text-decoration-thickness:2px;text-underline-offset:2px}._purpur-breadcrumbs--default_hceki_15,._purpur-breadcrumbs--default_hceki_15 a{color:var(--purpur-color-text-interactive-primary)}._purpur-breadcrumbs--default_hceki_15 a:hover{color:var(--purpur-color-text-interactive-primary-hover);background:var(--purpur-color-background-interactive-transparent-hover)}._purpur-breadcrumbs--default_hceki_15 a:active{color:var(--purpur-color-text-interactive-primary-active);background:var(--purpur-color-background-interactive-transparent-active)}._purpur-breadcrumbs--negative_hceki_29,._purpur-breadcrumbs--negative_hceki_29 a{color:var(--purpur-color-text-interactive-primary-negative)}._purpur-breadcrumbs--negative_hceki_29 a:hover{color:var(--purpur-color-text-interactive-primary-negative-hover);background:var(--purpur-color-background-interactive-transparent-negative-hover)}._purpur-breadcrumbs--negative_hceki_29 a:active{color:var(--purpur-color-text-interactive-primary-negative-active);background:var(--purpur-color-background-interactive-transparent-negative-active)}._purpur-breadcrumbs__list_hceki_43{display:flex;flex-direction:row;align-items:center;gap:var(--purpur-spacing-50);padding:0;list-style:none}._purpur-breadcrumbs__home_hceki_51{display:none}@media screen and (min-width: 600px){._purpur-breadcrumbs__home_hceki_51{display:flex;align-items:center}}._purpur-breadcrumb-item_hceki_61{white-space:nowrap}._purpur-breadcrumb-item--current_hceki_64{text-overflow:ellipsis;overflow:hidden}._purpur-breadcrumb-item--current_hceki_64 a{color:var(--purpur-color-text-default);text-decoration:none;font-weight:var(--purpur-typography-weight-medium)}._purpur-breadcrumb-item--current_hceki_64 a:hover{background:none}._purpur-breadcrumb-item_hceki_61:not(:nth-last-child(2)):not(:last-child){display:none}@media screen and (min-width: 600px){._purpur-breadcrumb-item_hceki_61:not(:nth-last-child(2)):not(:last-child){display:initial}}._purpur-breadcrumb-item--negative_hceki_84._purpur-breadcrumb-item--current_hceki_64 a{color:var(--purpur-color-text-default-negative)}._purpur-breadcrumb-item__separator_hceki_87{font-size:var(--purpur-typography-scale-75);margin-left:var(--purpur-spacing-50)}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@purpurds/breadcrumbs",
3
+ "version": "3.0.0",
4
+ "license": "AGPL-3.0-only",
5
+ "main": "./dist/breadcrumbs.cjs.js",
6
+ "types": "./dist/breadcrumbs.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "require": "./dist/breadcrumbs.cjs.js",
10
+ "systemjs": "./dist/breadcrumbs.system.js",
11
+ "types": "./dist/breadcrumbs.d.ts",
12
+ "default": "./dist/breadcrumbs.es.js"
13
+ },
14
+ "./styles": "./dist/styles.css"
15
+ },
16
+ "source": "src/breadcrumbs.tsx",
17
+ "dependencies": {
18
+ "classnames": "~2.5.0",
19
+ "@purpurds/icon": "3.0.0",
20
+ "@purpurds/tokens": "3.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@rushstack/eslint-patch": "~1.7.0",
24
+ "@storybook/blocks": "~7.6.0",
25
+ "@storybook/react": "~7.6.0",
26
+ "@telia/base-rig": "~8.2.0",
27
+ "@telia/react-rig": "~3.2.0",
28
+ "@testing-library/dom": "~9.3.3",
29
+ "@testing-library/jest-dom": "~6.3.0",
30
+ "@testing-library/react": "~14.1.2",
31
+ "@types/node": "18",
32
+ "@types/react-dom": "~18.2.17",
33
+ "@types/react": "~18.2.42",
34
+ "eslint-plugin-testing-library": "~6.2.0",
35
+ "eslint": "~8.56.0",
36
+ "jsdom": "~22.1.0",
37
+ "lint-staged": "~10.5.3",
38
+ "prettier": "~2.8.8",
39
+ "react-dom": "~18.2.0",
40
+ "react": "~18.2.0",
41
+ "typescript": "~5.2.2",
42
+ "vite": "~5.0.6",
43
+ "vitest": "~1.2.0",
44
+ "@purpurds/component-rig": "1.0.0"
45
+ },
46
+ "scripts": {
47
+ "build:dev": "vite",
48
+ "build:watch": "vite build --watch",
49
+ "build": "rm -rf dist && vite build && vite build --mode systemjs",
50
+ "ci:build": "rushx build",
51
+ "coverage": "vitest run --coverage",
52
+ "lint:fix": "eslint . --fix",
53
+ "lint": "lint-staged --no-stash 2>&1",
54
+ "sbdev": "rush sbdev",
55
+ "test:unit": "vitest run --passWithNoTests",
56
+ "test:watch": "vitest --watch",
57
+ "test": "rushx test:unit",
58
+ "typecheck": "tsc -p ./tsconfig.json"
59
+ }
60
+ }
package/readme.mdx ADDED
@@ -0,0 +1,101 @@
1
+ import { Meta, Stories, ArgTypes, Primary, Subtitle } from "@storybook/blocks";
2
+
3
+ import * as BreadcrumbsStories from "./src/breadcrumbs.stories";
4
+ import packageInfo from "./package.json";
5
+
6
+ <Meta name="Docs" title="Components/Breadcrumbs" of={BreadcrumbsStories} />
7
+
8
+ # Breadcrumbs
9
+
10
+ <Subtitle>Version {packageInfo.version}</Subtitle>
11
+
12
+ When a website has a lot of pages, breadcrumbs can help a user find their current location within the overal hierarchy. This page shows how you can make breadcrumbs accessible to all users.
13
+
14
+ Use them when you have several levels of navigation and want to make the parent pages available as navigation.
15
+
16
+ ### Accessibility
17
+
18
+ The Purpur breadcrumbs are accessible by default according to best practices. The last BreadcrumbsItem is the one belonging to the page the user is currently on.
19
+
20
+ ### Meta Data & SEO
21
+
22
+ Another benefit of helping the user find their way, is that we also help the search engines increasing understanding of our site.
23
+
24
+ By default, the breadcrumbs will render a [`JSON+LD` script tag](?path=/story/components-breadcrumbs--breadcrumb-meta-data) with [structured meta data](https://developers.google.com/search/docs/appearance/structured-data/breadcrumb#json-ld) extracted from the breadcrumb items.
25
+
26
+ ### Custom Link Component
27
+
28
+ If you need a [custom link](#custom-link), perhaps to use a router, like the `next/link` component, you can simply [pass your custom link](#custom-link) component and skip the `href` property on Breadcrumbs.Item, only passing it directly to `<Link />`.
29
+
30
+ ### Showcase
31
+
32
+ <Primary />
33
+
34
+ ### Properties
35
+
36
+ <ArgTypes />
37
+
38
+ ### Installation
39
+
40
+ #### Via NPM
41
+
42
+ Add the dependency to your consumer app like `"@purpurds/breadcrumbs": "x.y.z"`
43
+
44
+ #### From outside the monorepo (build-time)
45
+
46
+ To install this package, you need to setup access to the artifactory. [Click here to go to the guide on how to do that](https://github.com/telia-company/jfrog-documentation/blob/main/doc/JFrog/JFrog_Onboarding.md#getting-access-to-artifactory-and-other-jfrog-applications).
47
+
48
+ ---
49
+
50
+ In MyApp.tsx
51
+
52
+ ```tsx
53
+ import "@purpurds/tokens/index.css";
54
+ ```
55
+
56
+ and
57
+
58
+ ```tsx
59
+ import "@purpurds/breadcrumbs/styles";
60
+ ```
61
+
62
+ In MyComponent.tsx
63
+
64
+ ```tsx
65
+ import { Breadcrumbs } from "@purpurds/breadcrumbs";
66
+
67
+ export const MyComponent = () => {
68
+ return (
69
+ <Breadcrumbs>
70
+ <Breadcrumbs.Item href="/ships">Ships</Breadcrumbs.Item>
71
+ <Breadcrumbs.Item href="/ships/twin-seaters/">Twin Seaters</Breadcrumbs.Item>
72
+ <Breadcrumbs.Item href="/ships/twin-seaters/naboo-n1">N1 Starfighter</Breadcrumbs.Item>
73
+ </Breadcrumbs>
74
+ );
75
+ };
76
+ ```
77
+
78
+ ### Custom Link
79
+
80
+ With a custom link component such as `next/link`:
81
+
82
+ ```tsx
83
+ import { Breadcrumbs } from "@purpurds/breadcrumbs";
84
+ import Link from "next/link";
85
+
86
+ export const MyComponent = () => {
87
+ return (
88
+ <Breadcrumbs>
89
+ <Breadcrumbs.Item>
90
+ <Link href="/ships">Ships</Link>
91
+ </Breadcrumbs.Item>
92
+ <Breadcrumbs.Item>
93
+ <Link href="/ships/twin-seaters">Twin Seaters</Link>
94
+ </Breadcrumbs.Item>
95
+ <Breadcrumbs.Item>
96
+ <Link href="/ships/twin-seaters/naboo-n1">N1 Starfighter</Link>
97
+ </Breadcrumbs.Item>
98
+ </Breadcrumbs>
99
+ );
100
+ };
101
+ ```
@@ -0,0 +1,118 @@
1
+ @import "@purpurds/tokens/breakpoint/variables";
2
+
3
+ .purpur-breadcrumbs {
4
+ font-family: var(--purpur-typography-family-default);
5
+
6
+ padding: var(--purpur-spacing-150) 0;
7
+
8
+ a {
9
+ font-size: var(--purpur-typography-scale-75);
10
+ text-underline-offset: 3px;
11
+ padding: 14px 2px;
12
+ letter-spacing: 0.3px;
13
+
14
+ &:hover,
15
+ &:active {
16
+ text-decoration-thickness: 2px;
17
+ text-underline-offset: 2px;
18
+ }
19
+ }
20
+
21
+ &--default {
22
+ color: var(--purpur-color-text-interactive-primary);
23
+
24
+ a {
25
+ color: var(--purpur-color-text-interactive-primary);
26
+
27
+ &:hover {
28
+ color: var(--purpur-color-text-interactive-primary-hover);
29
+ background: var(--purpur-color-background-interactive-transparent-hover);
30
+ }
31
+
32
+ &:active {
33
+ color: var(--purpur-color-text-interactive-primary-active);
34
+ background: var(--purpur-color-background-interactive-transparent-active);
35
+ }
36
+ }
37
+ }
38
+
39
+ &--negative {
40
+ color: var(--purpur-color-text-interactive-primary-negative);
41
+
42
+ a {
43
+ color: var(--purpur-color-text-interactive-primary-negative);
44
+
45
+ &:hover {
46
+ color: var(--purpur-color-text-interactive-primary-negative-hover);
47
+ background: var(--purpur-color-background-interactive-transparent-negative-hover);
48
+ }
49
+
50
+ &:active {
51
+ color: var(--purpur-color-text-interactive-primary-negative-active);
52
+ background: var(--purpur-color-background-interactive-transparent-negative-active);
53
+ }
54
+ }
55
+ }
56
+
57
+ &__list {
58
+ display: flex;
59
+ flex-direction: row;
60
+ align-items: center;
61
+ gap: var(--purpur-spacing-50);
62
+
63
+ padding: 0;
64
+
65
+ list-style: none;
66
+ }
67
+
68
+ &__home {
69
+ display: none;
70
+ }
71
+
72
+ @media screen and (min-width: #{$purpur-breakpoint-md}) {
73
+ &__home {
74
+ display: flex;
75
+ align-items: center;
76
+ }
77
+ }
78
+ }
79
+
80
+ .purpur-breadcrumb-item {
81
+ white-space: nowrap;
82
+
83
+ &--current {
84
+ text-overflow: ellipsis;
85
+ overflow: hidden;
86
+
87
+ a {
88
+ color: var(--purpur-color-text-default);
89
+ text-decoration: none;
90
+ font-weight: var(--purpur-typography-weight-medium);
91
+
92
+ &:hover {
93
+ background: none;
94
+ }
95
+ }
96
+ }
97
+
98
+ &:not(:nth-last-child(2)):not(:last-child) {
99
+ display: none;
100
+ }
101
+
102
+ @media screen and (min-width: #{$purpur-breakpoint-md}) {
103
+ &:not(:nth-last-child(2)):not(:last-child) {
104
+ display: initial;
105
+ }
106
+ }
107
+
108
+ &--negative.purpur-breadcrumb-item--current {
109
+ a {
110
+ color: var(--purpur-color-text-default-negative);
111
+ }
112
+ }
113
+
114
+ &__separator {
115
+ font-size: var(--purpur-typography-scale-75);
116
+ margin-left: var(--purpur-spacing-50);
117
+ }
118
+ }
@@ -0,0 +1,180 @@
1
+ import React, { ReactElement, ReactNode, useEffect, useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+
4
+ import "@purpurds/icon/styles";
5
+ import {
6
+ Breadcrumbs,
7
+ breadcrumbVariants,
8
+ BreadcrumbVariant,
9
+ BreadcrumbsItemProps,
10
+ } from "./breadcrumbs";
11
+
12
+ const meta: Meta<typeof Breadcrumbs> = {
13
+ title: "Components/Breadcrumbs",
14
+ component: Breadcrumbs,
15
+ };
16
+
17
+ export default meta;
18
+
19
+ type Story = StoryObj<typeof Breadcrumbs>;
20
+
21
+ type LinkProps = {
22
+ href?: string;
23
+ children: ReactNode;
24
+ };
25
+
26
+ type RendererProps = {
27
+ children: ReactElement<BreadcrumbsItemProps>;
28
+ variant: BreadcrumbVariant;
29
+ };
30
+
31
+ const MetaData = () => {
32
+ const [code, setCode] = useState("");
33
+
34
+ useEffect(() => {
35
+ const meta = document.querySelector<HTMLScriptElement>(
36
+ "script[type='application/ld+json']"
37
+ )?.text;
38
+
39
+ if (meta) {
40
+ const stringified = JSON.parse(meta);
41
+ const clean = JSON.stringify(stringified, null, 2);
42
+ setCode(clean);
43
+ }
44
+ }, []);
45
+
46
+ return (
47
+ <>
48
+ <p>
49
+ This is outputted as a{" "}
50
+ <a
51
+ href="https://developers.google.com/search/docs/appearance/structured-data/breadcrumb"
52
+ target="_blank"
53
+ rel="noopener noreferer"
54
+ >
55
+ BreadcrumbList
56
+ </a>{" "}
57
+ in JSON-LD schema format:
58
+ </p>
59
+ <pre
60
+ style={{
61
+ backgroundColor: "var(--purpur-color-beige-50)",
62
+ padding: "1rem",
63
+ }}
64
+ >
65
+ {`<script type="application/ld+json">\n`}
66
+ {code}
67
+ {`\n</script>`}
68
+ </pre>
69
+ </>
70
+ );
71
+ };
72
+
73
+ const Link = ({ href, children }: LinkProps) => <a href={href}>{children}</a>;
74
+
75
+ const Renderer =
76
+ (showMeta = false) =>
77
+ ({ children, variant, ...args }: RendererProps) => {
78
+ return (
79
+ <>
80
+ <div
81
+ style={{
82
+ backgroundColor:
83
+ variant && variant.endsWith("negative")
84
+ ? "var(--purpur-color-background-tone-on-tone-primary)"
85
+ : undefined,
86
+ }}
87
+ >
88
+ <Breadcrumbs variant={variant} {...args}>
89
+ {children}
90
+ </Breadcrumbs>
91
+ </div>
92
+ {showMeta ? <MetaData /> : null}
93
+ </>
94
+ );
95
+ };
96
+
97
+ export const DefaultVariant: Story = {
98
+ name: "Default variant",
99
+ args: {
100
+ meta: true,
101
+ variant: breadcrumbVariants[0],
102
+ children: [
103
+ <Breadcrumbs.Item href="/products" key={1}>
104
+ Products
105
+ </Breadcrumbs.Item>,
106
+ <Breadcrumbs.Item key={2}>
107
+ <a href="/products/steel">Steel</a>
108
+ </Breadcrumbs.Item>,
109
+ <Breadcrumbs.Item key={3}>
110
+ <Link href="/products/steel/beskar">Beskar</Link>
111
+ </Breadcrumbs.Item>,
112
+ ],
113
+ },
114
+ parameters: {
115
+ design: [
116
+ {
117
+ name: "Breadcrumbs",
118
+ type: "figma",
119
+ url: "https://www.figma.com/file/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?node-id=27132%3A14635&mode=dev",
120
+ },
121
+ ],
122
+ },
123
+ };
124
+
125
+ export const NegativeVariant: Story = {
126
+ name: "Negative variant",
127
+ args: {
128
+ variant: breadcrumbVariants[1],
129
+ children: [
130
+ <Breadcrumbs.Item href="/galaxies" key={0}>
131
+ Galaxies
132
+ </Breadcrumbs.Item>,
133
+ <Breadcrumbs.Item href="/outer-rim" key={1}>
134
+ Outer Rim
135
+ </Breadcrumbs.Item>,
136
+ <Breadcrumbs.Item href="/mandalore" key={2}>
137
+ Mandalore
138
+ </Breadcrumbs.Item>,
139
+ ],
140
+ },
141
+ render: Renderer(false),
142
+ };
143
+
144
+ export const CustomItem: Story = {
145
+ name: "Custom Breadcrumb Items",
146
+ args: {
147
+ variant: breadcrumbVariants[0],
148
+ children: [
149
+ <Breadcrumbs.Item key={0} href="/products">
150
+ Products
151
+ </Breadcrumbs.Item>,
152
+ <Breadcrumbs.Item key={1}>
153
+ <Link href="/products/laser-swords">Laser Swords</Link>
154
+ </Breadcrumbs.Item>,
155
+ <Breadcrumbs.Item key={2}>
156
+ <a href="/products/laser-swords/darksaber">Darksaber</a>
157
+ </Breadcrumbs.Item>,
158
+ ],
159
+ },
160
+ render: Renderer(false),
161
+ };
162
+
163
+ export const BreadcrumbMetaData: Story = {
164
+ name: "Meta data by default",
165
+ args: {
166
+ variant: breadcrumbVariants[0],
167
+ children: [
168
+ <Breadcrumbs.Item key={0} href="/ships">
169
+ Ships
170
+ </Breadcrumbs.Item>,
171
+ <Breadcrumbs.Item key={1} href="/ships/twin-seaters/">
172
+ Twin Seaters
173
+ </Breadcrumbs.Item>,
174
+ <Breadcrumbs.Item key={2}>
175
+ <Link href="/ships/twin-seaters/naboo-n1">N1 Starfighter</Link>
176
+ </Breadcrumbs.Item>,
177
+ ],
178
+ },
179
+ render: Renderer(true),
180
+ };
@@ -0,0 +1,59 @@
1
+ import React from "react";
2
+ import * as matchers from "@testing-library/jest-dom/matchers";
3
+ import { cleanup, render, screen } from "@testing-library/react";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+
6
+ import { Breadcrumbs } from "./breadcrumbs";
7
+
8
+ expect.extend(matchers);
9
+ afterEach(cleanup);
10
+
11
+ describe("Breadcrumbs", () => {
12
+ it("renders a breadcrumb item", () => {
13
+ render(
14
+ <Breadcrumbs variant="default">
15
+ <Breadcrumbs.Item href="/link" data-testid="item">
16
+ Products
17
+ </Breadcrumbs.Item>
18
+ </Breadcrumbs>
19
+ );
20
+ expect(screen.getByTestId("item")).toBeInstanceOf(HTMLAnchorElement);
21
+ });
22
+
23
+ it("renders custom link", () => {
24
+ render(
25
+ <Breadcrumbs variant="default">
26
+ <Breadcrumbs.Item href="/link">Products</Breadcrumbs.Item>
27
+ <Breadcrumbs.Item data-testid="custom">
28
+ <a href="/">Custom link</a>
29
+ </Breadcrumbs.Item>
30
+ </Breadcrumbs>
31
+ );
32
+ expect(screen.getByTestId("custom")).toBeInTheDocument();
33
+ });
34
+
35
+ it("marks the last breadcrumb item as aria-current=page", () => {
36
+ render(
37
+ <Breadcrumbs variant="default">
38
+ <Breadcrumbs.Item href="/link">Products</Breadcrumbs.Item>
39
+ <Breadcrumbs.Item href="/link/sub" data-testid="last">
40
+ Sub
41
+ </Breadcrumbs.Item>
42
+ </Breadcrumbs>
43
+ );
44
+ expect(screen.getByText("Sub").getAttribute("aria-current")).toEqual("page");
45
+ });
46
+
47
+ it("renders schema+ld json for meta data", () => {
48
+ render(
49
+ <Breadcrumbs>
50
+ <Breadcrumbs.Item href="/link">Products</Breadcrumbs.Item>
51
+ <Breadcrumbs.Item href="/link/sub" data-testid="last">
52
+ Sub
53
+ </Breadcrumbs.Item>
54
+ </Breadcrumbs>
55
+ );
56
+ const parsedMeta = JSON.parse(screen.getByTestId("breadcrumbs-meta").innerHTML);
57
+ expect(parsedMeta["itemListElement"].length).toEqual(2);
58
+ })
59
+ });
@@ -0,0 +1,157 @@
1
+ import React, { Children, cloneElement, createElement, ReactElement } from "react";
2
+ import { IconHome } from "@purpurds/icon";
3
+ import c from "classnames";
4
+
5
+ import styles from "./breadcrumbs.module.scss";
6
+ import { MetaListItem, metaListItem, metaSchema } from "./meta";
7
+
8
+ export const BREADCRUMB_VARIANT = {
9
+ DEFAULT: "default",
10
+ NEGATIVE: "negative",
11
+ } as const;
12
+
13
+ export const breadcrumbVariants = Object.values(BREADCRUMB_VARIANT);
14
+ export type BreadcrumbVariant = (typeof BREADCRUMB_VARIANT)[keyof typeof BREADCRUMB_VARIANT];
15
+
16
+ export type BreadcrumbsProps = {
17
+ ["data-testid"]?: string;
18
+ ariaLabel?: string;
19
+ children: ReactElement<BreadcrumbsItemProps> | Array<ReactElement<BreadcrumbsItemProps>>;
20
+ className?: string;
21
+ meta?: boolean;
22
+ variant?: BreadcrumbVariant;
23
+ };
24
+
25
+ type CommonItemProps = {
26
+ current?: boolean;
27
+ variant?: BreadcrumbVariant;
28
+ ["data-testid"]?: string;
29
+ ariaLabel?: string;
30
+ meta?: boolean;
31
+ };
32
+
33
+ export type BreadcrumbsItemProps = CommonItemProps & Conditional;
34
+
35
+ type Conditional =
36
+ | {
37
+ href?: string;
38
+ children: string;
39
+ }
40
+ | {
41
+ href?: never;
42
+ children: ReactElement<HTMLAnchorElement>;
43
+ };
44
+
45
+ const rootClassName = "purpur-breadcrumbs";
46
+ const itemClassName = "purpur-breadcrumb-item";
47
+
48
+ const Breadcrumbs = ({
49
+ ["data-testid"]: dataTestId,
50
+ ariaLabel,
51
+ children,
52
+ className,
53
+ meta = true,
54
+ variant = "default",
55
+ }: BreadcrumbsProps) => {
56
+ const classes = c([className, styles[rootClassName], styles[`${rootClassName}--${variant}`]]);
57
+
58
+ const maxIndex = Children.count(children);
59
+
60
+ const metaListItems: MetaListItem[] = [];
61
+
62
+ const items = Children.map(children, (item, index) => {
63
+ const position = index + 1;
64
+ const current = maxIndex === position;
65
+
66
+ const grandChildren = item.props.children;
67
+ const grandGrandChildren = typeof grandChildren === "string" ? null : grandChildren.props;
68
+
69
+ let name = null,
70
+ href = null;
71
+
72
+ if (typeof grandChildren === "string") {
73
+ name = grandChildren;
74
+ href = item.props.href;
75
+ } else if (grandGrandChildren?.children && typeof grandGrandChildren?.children === "string") {
76
+ name = grandGrandChildren.children;
77
+ href = grandGrandChildren.href;
78
+ }
79
+
80
+ if (name && href) {
81
+ metaListItems.push(metaListItem(name, href, position));
82
+ }
83
+
84
+ const child = cloneElement(item, {
85
+ current,
86
+ variant,
87
+ });
88
+
89
+ return child;
90
+ });
91
+
92
+ const schema = metaListItems.length === maxIndex ? metaSchema(metaListItems) : null;
93
+
94
+ return (
95
+ <nav data-testid={dataTestId} aria-label={ariaLabel || "Breadcrumb"} className={classes}>
96
+ <ol className={styles[`${rootClassName}__list`]}>
97
+ <li aria-hidden="true" className={styles[`${rootClassName}__home`]}>
98
+ <IconHome size="xs" />
99
+ </li>
100
+ {items}
101
+ </ol>
102
+ {meta && schema ? (
103
+ <script
104
+ type="application/ld+json"
105
+ data-testid="breadcrumbs-meta"
106
+ dangerouslySetInnerHTML={{ __html: schema }}
107
+ />
108
+ ) : null}
109
+ </nav>
110
+ );
111
+ };
112
+
113
+ const Item = ({
114
+ href,
115
+ ["data-testid"]: dataTestId,
116
+ children,
117
+ current = false,
118
+ variant = "default",
119
+ ...rest
120
+ }: BreadcrumbsItemProps) => {
121
+ const classes = c([styles[itemClassName], styles[`${itemClassName}--${variant}`]], {
122
+ [styles[`${itemClassName}--current`]]: current,
123
+ });
124
+
125
+ const link = () => {
126
+ const commonProps = {
127
+ href,
128
+ ["data-testid"]: dataTestId,
129
+ "aria-current": current ? "page" : undefined,
130
+ };
131
+
132
+ const component =
133
+ href || typeof children === "string"
134
+ ? createElement("a", commonProps, children)
135
+ : cloneElement(children, {
136
+ ...commonProps,
137
+ ...children.props,
138
+ });
139
+
140
+ return component;
141
+ };
142
+
143
+ return (
144
+ <li {...rest} className={classes}>
145
+ {link()}
146
+ {!current ? (
147
+ <span aria-hidden className={styles[`${itemClassName}__separator`]}>
148
+ /
149
+ </span>
150
+ ) : null}
151
+ </li>
152
+ );
153
+ };
154
+
155
+ Breadcrumbs.Item = Item;
156
+
157
+ export { Breadcrumbs };