@purpurds/breadcrumbs 6.12.5 → 7.1.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 +1 -1
- package/dist/breadcrumbs.cjs.js +3 -3
- package/dist/breadcrumbs.cjs.js.map +1 -1
- package/dist/breadcrumbs.d.ts +31 -21
- package/dist/breadcrumbs.d.ts.map +1 -1
- package/dist/breadcrumbs.es.js +120 -107
- package/dist/breadcrumbs.es.js.map +1 -1
- package/dist/meta.d.ts +2 -2
- package/dist/meta.d.ts.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +4 -4
- package/src/breadcrumbs.module.scss +45 -39
- package/src/breadcrumbs.stories.tsx +46 -26
- package/src/breadcrumbs.test.tsx +32 -16
- package/src/breadcrumbs.tsx +87 -82
- package/src/meta.ts +15 -8
package/src/breadcrumbs.tsx
CHANGED
|
@@ -1,65 +1,85 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
type AnchorHTMLAttributes,
|
|
3
|
+
Children,
|
|
4
|
+
cloneElement,
|
|
5
|
+
type ForwardRefExoticComponent,
|
|
6
|
+
type ReactElement,
|
|
7
|
+
} from "react";
|
|
2
8
|
import { IconHome } from "@purpurds/icon/home";
|
|
3
|
-
import c from "classnames";
|
|
9
|
+
import c from "classnames/bind";
|
|
4
10
|
|
|
5
11
|
import styles from "./breadcrumbs.module.scss";
|
|
6
12
|
import { MetaListItem, metaListItem, metaSchema } from "./meta";
|
|
7
13
|
import { DataAttributes } from "./types.ts";
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
const cx = c.bind(styles);
|
|
16
|
+
|
|
17
|
+
type WithMeta = {
|
|
18
|
+
meta?: true;
|
|
19
|
+
baseUrl: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type WithoutMeta = {
|
|
23
|
+
meta: false;
|
|
24
|
+
baseUrl?: never;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type BreadcrumbsProps = Omit<React.HTMLAttributes<HTMLElement>, "aria-label"> &
|
|
10
28
|
DataAttributes & {
|
|
11
|
-
|
|
29
|
+
/** Describes the breadcrumb navigation for screen readers, e.g. "Breadcrumbs" or "Breadcrumb navigation". */
|
|
30
|
+
["aria-label"]: string;
|
|
12
31
|
children: ReactElement<BreadcrumbsItemProps> | Array<ReactElement<BreadcrumbsItemProps>>;
|
|
13
|
-
/** Set to generate breadcrumbs metadata for crawlers. Defaults to true. */
|
|
14
|
-
meta?: boolean;
|
|
15
32
|
/** Set to render the breadcrumbs with light font color. Ideally used when rendered on dark backgrounds. */
|
|
16
33
|
negative?: boolean;
|
|
17
34
|
/** Set to render the last breadcrumb item in bold font to indicate that this is the current page. Setting this to false will render the last item as a normal breadcrumb link. Defaults to true. */
|
|
18
35
|
highlightLast?: boolean;
|
|
19
36
|
/** Set to not render the home icon in front of the first breadcrumb item. */
|
|
20
37
|
hideHomeIcon?: boolean;
|
|
21
|
-
|
|
38
|
+
linkElement?: ForwardRefExoticComponent<AnchorHTMLAttributes<HTMLAnchorElement>> | "a";
|
|
39
|
+
} & (WithMeta | WithoutMeta);
|
|
22
40
|
|
|
23
|
-
type
|
|
41
|
+
export type BreadcrumbsItemProps = Omit<
|
|
42
|
+
React.HTMLAttributes<HTMLLIElement>,
|
|
43
|
+
"onClick" | "children"
|
|
44
|
+
> &
|
|
24
45
|
DataAttributes & {
|
|
46
|
+
children: string;
|
|
47
|
+
href?: string;
|
|
48
|
+
onClick?: () => void;
|
|
49
|
+
/** @ignore */
|
|
25
50
|
current?: boolean;
|
|
26
|
-
|
|
27
|
-
|
|
51
|
+
/** @ignore */
|
|
52
|
+
first?: boolean;
|
|
53
|
+
/** @ignore */
|
|
54
|
+
last?: boolean;
|
|
55
|
+
/** @ignore */
|
|
56
|
+
linkElement?: BreadcrumbsProps["linkElement"];
|
|
57
|
+
/** @ignore */
|
|
28
58
|
meta?: boolean;
|
|
29
|
-
|
|
59
|
+
/** @ignore */
|
|
60
|
+
negative?: boolean;
|
|
30
61
|
};
|
|
31
62
|
|
|
32
|
-
export type BreadcrumbsItemProps = CommonItemProps & Conditional;
|
|
33
|
-
|
|
34
|
-
type Conditional =
|
|
35
|
-
| {
|
|
36
|
-
href?: string;
|
|
37
|
-
children: string;
|
|
38
|
-
}
|
|
39
|
-
| {
|
|
40
|
-
href?: never;
|
|
41
|
-
children: ReactElement<HTMLAnchorElement>;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
63
|
const rootClassName = "purpur-breadcrumbs";
|
|
45
64
|
const itemClassName = "purpur-breadcrumb-item";
|
|
46
65
|
|
|
47
66
|
const Breadcrumbs = ({
|
|
48
|
-
ariaLabel,
|
|
67
|
+
["aria-label"]: ariaLabel,
|
|
68
|
+
baseUrl,
|
|
49
69
|
children,
|
|
50
70
|
className,
|
|
71
|
+
linkElement = "a",
|
|
51
72
|
meta = true,
|
|
52
73
|
negative = false,
|
|
53
74
|
highlightLast = true,
|
|
54
75
|
hideHomeIcon,
|
|
55
76
|
...props
|
|
56
77
|
}: BreadcrumbsProps) => {
|
|
57
|
-
const classes =
|
|
78
|
+
const classes = cx([
|
|
58
79
|
className,
|
|
59
|
-
|
|
60
|
-
|
|
80
|
+
rootClassName,
|
|
81
|
+
`${rootClassName}--${negative ? "negative" : "default"}`,
|
|
61
82
|
]);
|
|
62
|
-
|
|
63
83
|
const maxIndex = Children.count(children);
|
|
64
84
|
|
|
65
85
|
const metaListItems: MetaListItem[] = [];
|
|
@@ -69,39 +89,16 @@ const Breadcrumbs = ({
|
|
|
69
89
|
const last = maxIndex === position;
|
|
70
90
|
const current = highlightLast && last;
|
|
71
91
|
|
|
72
|
-
const
|
|
73
|
-
const grandGrandChildren = typeof grandChildren === "string" ? null : grandChildren.props;
|
|
74
|
-
|
|
75
|
-
let name = null,
|
|
76
|
-
href = null;
|
|
92
|
+
const { children, href } = item.props;
|
|
77
93
|
|
|
78
|
-
|
|
79
|
-
name = grandChildren;
|
|
80
|
-
href = item.props.href;
|
|
81
|
-
} else if (grandGrandChildren?.children && typeof grandGrandChildren?.children === "string") {
|
|
82
|
-
name = grandGrandChildren.children;
|
|
83
|
-
href = grandGrandChildren.href;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (name && href) {
|
|
87
|
-
metaListItems.push(metaListItem(name, href, position));
|
|
88
|
-
}
|
|
94
|
+
metaListItems.push(metaListItem(children, position, href && `${baseUrl}${href}`));
|
|
89
95
|
|
|
90
96
|
const child = cloneElement(item, {
|
|
91
97
|
current,
|
|
92
98
|
negative,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
{!hideHomeIcon ? (
|
|
97
|
-
<span className={styles[`${rootClassName}__home`]} aria-hidden="true">
|
|
98
|
-
<IconHome size="xs" />
|
|
99
|
-
</span>
|
|
100
|
-
) : null}
|
|
101
|
-
{item.props.children}
|
|
102
|
-
</>
|
|
103
|
-
),
|
|
104
|
-
}),
|
|
99
|
+
first: !hideHomeIcon && position === 1,
|
|
100
|
+
last,
|
|
101
|
+
linkElement,
|
|
105
102
|
});
|
|
106
103
|
|
|
107
104
|
return child;
|
|
@@ -110,7 +107,7 @@ const Breadcrumbs = ({
|
|
|
110
107
|
const schema = metaListItems.length === maxIndex ? metaSchema(metaListItems) : null;
|
|
111
108
|
|
|
112
109
|
return (
|
|
113
|
-
<nav aria-label={ariaLabel
|
|
110
|
+
<nav aria-label={ariaLabel} className={classes} {...props}>
|
|
114
111
|
<ol className={styles[`${rootClassName}__list`]}>{items}</ol>
|
|
115
112
|
{meta && schema ? (
|
|
116
113
|
<script
|
|
@@ -128,39 +125,47 @@ const Item = ({
|
|
|
128
125
|
["data-testid"]: dataTestId,
|
|
129
126
|
children,
|
|
130
127
|
current = false,
|
|
128
|
+
linkElement = "a",
|
|
131
129
|
negative = false,
|
|
132
130
|
onClick,
|
|
133
|
-
|
|
131
|
+
first = false,
|
|
132
|
+
last = false,
|
|
133
|
+
...props
|
|
134
134
|
}: BreadcrumbsItemProps) => {
|
|
135
|
-
const
|
|
136
|
-
|
|
135
|
+
const LinkElement = linkElement;
|
|
136
|
+
const classes = cx(
|
|
137
|
+
[itemClassName],
|
|
138
|
+
styles[`${itemClassName}--${negative ? "negative" : "default"}`],
|
|
137
139
|
{
|
|
138
|
-
[
|
|
140
|
+
[`${itemClassName}--current`]: current,
|
|
139
141
|
}
|
|
140
142
|
);
|
|
141
143
|
|
|
142
|
-
const link = () => {
|
|
143
|
-
const commonProps = {
|
|
144
|
-
href,
|
|
145
|
-
["data-testid"]: dataTestId,
|
|
146
|
-
"aria-current": current ? "page" : undefined,
|
|
147
|
-
onClick,
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const component =
|
|
151
|
-
href || typeof children === "string"
|
|
152
|
-
? createElement("a", commonProps, children)
|
|
153
|
-
: cloneElement(children, {
|
|
154
|
-
...commonProps,
|
|
155
|
-
...children.props,
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
return component;
|
|
159
|
-
};
|
|
160
|
-
|
|
161
144
|
return (
|
|
162
|
-
<li {...
|
|
163
|
-
{
|
|
145
|
+
<li {...props} className={classes}>
|
|
146
|
+
{!last ? (
|
|
147
|
+
<LinkElement
|
|
148
|
+
href={href}
|
|
149
|
+
data-testid={dataTestId}
|
|
150
|
+
onClick={onClick}
|
|
151
|
+
className={cx(`${itemClassName}__element`, `${itemClassName}__link`)}
|
|
152
|
+
>
|
|
153
|
+
{first && (
|
|
154
|
+
<span className={styles[`${itemClassName}__home`]} aria-hidden="true">
|
|
155
|
+
<IconHome size="xs" />
|
|
156
|
+
</span>
|
|
157
|
+
)}
|
|
158
|
+
{children}
|
|
159
|
+
</LinkElement>
|
|
160
|
+
) : (
|
|
161
|
+
<span
|
|
162
|
+
aria-current="page"
|
|
163
|
+
data-testid={dataTestId}
|
|
164
|
+
className={cx(`${itemClassName}__element`)}
|
|
165
|
+
>
|
|
166
|
+
{children}
|
|
167
|
+
</span>
|
|
168
|
+
)}
|
|
164
169
|
</li>
|
|
165
170
|
);
|
|
166
171
|
};
|
package/src/meta.ts
CHANGED
|
@@ -2,17 +2,24 @@ export type MetaListItem = {
|
|
|
2
2
|
"@type": string;
|
|
3
3
|
position: number;
|
|
4
4
|
name: string;
|
|
5
|
-
item
|
|
5
|
+
item?: string;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
type MakeMetaListItem = (name: string,
|
|
8
|
+
type MakeMetaListItem = (name: string, position: number, item?: string) => MetaListItem;
|
|
9
9
|
|
|
10
|
-
export const metaListItem: MakeMetaListItem = (name,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
10
|
+
export const metaListItem: MakeMetaListItem = (name, position, item) => {
|
|
11
|
+
const schema: MetaListItem = {
|
|
12
|
+
"@type": "ListItem",
|
|
13
|
+
position,
|
|
14
|
+
name,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
if (item) {
|
|
18
|
+
schema.item = item;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return schema;
|
|
22
|
+
};
|
|
16
23
|
|
|
17
24
|
export const metaSchema = (itemListElement: MetaListItem[]) =>
|
|
18
25
|
JSON.stringify({
|