@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/LICENSE.txt +38 -0
- package/dist/breadcrumbs.cjs.js +10 -0
- package/dist/breadcrumbs.cjs.js.map +1 -0
- package/dist/breadcrumbs.d.ts +36 -0
- package/dist/breadcrumbs.d.ts.map +1 -0
- package/dist/breadcrumbs.es.js +219 -0
- package/dist/breadcrumbs.es.js.map +1 -0
- package/dist/breadcrumbs.system.js +10 -0
- package/dist/breadcrumbs.system.js.map +1 -0
- package/dist/meta.d.ts +11 -0
- package/dist/meta.d.ts.map +1 -0
- package/dist/styles.css +1 -0
- package/package.json +60 -0
- package/readme.mdx +101 -0
- package/src/breadcrumbs.module.scss +118 -0
- package/src/breadcrumbs.stories.tsx +180 -0
- package/src/breadcrumbs.test.tsx +59 -0
- package/src/breadcrumbs.tsx +157 -0
- package/src/global.d.ts +4 -0
- package/src/meta.ts +22 -0
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"}
|
package/dist/styles.css
ADDED
|
@@ -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 };
|