@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.
@@ -1,65 +1,85 @@
1
- import React, { Children, cloneElement, createElement, ReactElement } from "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
- export type BreadcrumbsProps = React.HTMLAttributes<HTMLElement> &
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
- ariaLabel?: string;
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 CommonItemProps = Omit<React.HTMLAttributes<HTMLLIElement>, "onClick"> &
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
- negative?: boolean;
27
- ariaLabel?: string;
51
+ /** @ignore */
52
+ first?: boolean;
53
+ /** @ignore */
54
+ last?: boolean;
55
+ /** @ignore */
56
+ linkElement?: BreadcrumbsProps["linkElement"];
57
+ /** @ignore */
28
58
  meta?: boolean;
29
- onClick?: () => void;
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 = c([
78
+ const classes = cx([
58
79
  className,
59
- styles[rootClassName],
60
- styles[`${rootClassName}--${negative ? "negative" : "default"}`],
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 grandChildren = item.props.children;
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
- if (typeof grandChildren === "string") {
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
- ...(position === 1 && {
94
- children: (
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 || "Breadcrumb"} className={classes} {...props}>
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
- ...rest
131
+ first = false,
132
+ last = false,
133
+ ...props
134
134
  }: BreadcrumbsItemProps) => {
135
- const classes = c(
136
- [styles[itemClassName], styles[`${itemClassName}--${negative ? "negative" : "default"}`]],
135
+ const LinkElement = linkElement;
136
+ const classes = cx(
137
+ [itemClassName],
138
+ styles[`${itemClassName}--${negative ? "negative" : "default"}`],
137
139
  {
138
- [styles[`${itemClassName}--current`]]: current,
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 {...rest} className={classes}>
163
- {link()}
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: string;
5
+ item?: string;
6
6
  };
7
7
 
8
- type MakeMetaListItem = (name: string, item: string, position: number) => MetaListItem;
8
+ type MakeMetaListItem = (name: string, position: number, item?: string) => MetaListItem;
9
9
 
10
- export const metaListItem: MakeMetaListItem = (name, item, position) => ({
11
- "@type": "ListItem",
12
- position,
13
- name,
14
- item,
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({