Skip to content

Commit fdbbcc8

Browse files
authored
feat(components): add Breadcrumbs component (#5928)
* feat(components): add `Breadcrumbs` component * feat(Storybook): add Breadcrumbs Hidden Home story * fix(Breadcrumbs): fix CSS indentation * fix(Breadcrumbs): fix typo * fix(Breadcrumbs): fix eslint import order * refactor(Breadcrumbs): split Breadcrumb into sub components * style(Breadcrumbs): remove comment and update component * fix(Breadcrumbs): use LocalizedLink * style(Breadcrumbs): remove abbreviation * refactor(Breadcrumbs): improve code readability * style(Breadcrumbs): format Breadcrumbs story * fix(Breadcrumbs): add default value for links prop * test(Breadcrumbs): add unit test * test(Breadcrumbs): remove unit test * feat(Breadcrumbs): add microdata * feat(Breadcrumbs): use intl for home aria label * fix(Breadcrumbs): itemProp without wrapping with span * refactor(Breadcrumbs): modularize Breadcrumb and refine logic * style(Breadcrumbs): rename other to props * fix(Breadcrumbs): move name to link * fix(Breadcrumbs): disable microdata for truncated item
1 parent 7d6b69a commit fdbbcc8

File tree

12 files changed

+319
-0
lines changed

12 files changed

+319
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.icon {
2+
@apply h-4
3+
w-4;
4+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import HomeIcon from '@heroicons/react/24/outline/HomeIcon';
2+
import type { ComponentProps, FC } from 'react';
3+
import { useIntl } from 'react-intl';
4+
5+
import BreadcrumbLink from '@/components/Common/Breadcrumbs/BreadcrumbLink';
6+
7+
import styles from './index.module.css';
8+
9+
type BreadcrumbHomeLinkProps = Omit<
10+
ComponentProps<typeof BreadcrumbLink>,
11+
'href'
12+
> &
13+
Partial<Pick<ComponentProps<typeof BreadcrumbLink>, 'href'>>;
14+
15+
const BreadcrumbHomeLink: FC<BreadcrumbHomeLinkProps> = ({
16+
href = '/',
17+
...props
18+
}) => {
19+
const { formatMessage } = useIntl();
20+
21+
const navigateToHome = formatMessage({
22+
id: 'components.common.breadcrumbs.navigateToHome',
23+
});
24+
25+
return (
26+
<BreadcrumbLink href={href} {...props}>
27+
<HomeIcon
28+
title={navigateToHome}
29+
aria-label={navigateToHome}
30+
className={styles.icon}
31+
/>
32+
</BreadcrumbLink>
33+
);
34+
};
35+
36+
export default BreadcrumbHomeLink;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.item {
2+
@apply flex
3+
items-center
4+
gap-5
5+
text-sm
6+
font-medium
7+
text-neutral-800
8+
dark:text-neutral-200;
9+
10+
&.visuallyHidden {
11+
@apply hidden;
12+
}
13+
14+
.separator {
15+
@apply h-4
16+
w-4
17+
text-neutral-600
18+
dark:text-neutral-400;
19+
}
20+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import ChevronRightIcon from '@heroicons/react/24/outline/ChevronRightIcon';
2+
import classNames from 'classnames';
3+
import type { ComponentProps, FC, PropsWithChildren } from 'react';
4+
5+
import styles from './index.module.css';
6+
7+
type BreadcrumbItemProps = {
8+
disableMicrodata?: boolean;
9+
hidden?: boolean;
10+
hideSeparator?: boolean;
11+
position?: number;
12+
} & ComponentProps<'li'>;
13+
14+
const BreadcrumbItem: FC<PropsWithChildren<BreadcrumbItemProps>> = ({
15+
disableMicrodata,
16+
children,
17+
hidden = false,
18+
hideSeparator = false,
19+
position,
20+
...props
21+
}) => (
22+
<li
23+
{...props}
24+
itemProp={!disableMicrodata ? 'itemListElement' : undefined}
25+
itemScope={!disableMicrodata ? true : undefined}
26+
itemType={!disableMicrodata ? 'https://schema.org/ListItem' : undefined}
27+
className={classNames(
28+
styles.item,
29+
{ [styles.visuallyHidden]: hidden },
30+
props.className
31+
)}
32+
aria-hidden={hidden ? 'true' : undefined}
33+
>
34+
{children}
35+
{position && <meta itemProp="position" content={`${position}`} />}
36+
{!hideSeparator && (
37+
<ChevronRightIcon aria-hidden="true" className={styles.separator} />
38+
)}
39+
</li>
40+
);
41+
42+
export default BreadcrumbItem;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.active {
2+
@apply rounded
3+
bg-green-600
4+
px-2
5+
py-1
6+
font-semibold
7+
text-white;
8+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import classNames from 'classnames';
2+
import type { ComponentProps, FC } from 'react';
3+
4+
import LocalizedLink from '@/components/LocalizedLink';
5+
6+
import styles from './index.module.css';
7+
8+
type BreadcrumbLinkProps = {
9+
active?: boolean;
10+
} & ComponentProps<typeof LocalizedLink>;
11+
12+
const BreadcrumbLink: FC<BreadcrumbLinkProps> = ({
13+
href,
14+
active,
15+
...props
16+
}) => (
17+
<LocalizedLink
18+
itemScope
19+
itemType="http://schema.org/Thing"
20+
itemProp="item"
21+
itemID={href.toString()}
22+
href={href}
23+
className={classNames({ [styles.active]: active }, props.className)}
24+
aria-current={active ? 'page' : undefined}
25+
{...props}
26+
>
27+
<span itemProp="name">{props.children}</span>
28+
</LocalizedLink>
29+
);
30+
31+
export default BreadcrumbLink;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.list {
2+
@apply flex
3+
items-center
4+
gap-5;
5+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { FC, PropsWithChildren, ComponentProps } from 'react';
2+
3+
import styles from './index.module.css';
4+
5+
const BreadcrumbRoot: FC<PropsWithChildren<ComponentProps<'nav'>>> = ({
6+
children,
7+
...props
8+
}) => (
9+
<nav aria-label="breadcrumb" {...props}>
10+
<ol
11+
itemScope
12+
itemType="https://schema.org/BreadcrumbList"
13+
className={styles.list}
14+
>
15+
{children}
16+
</ol>
17+
</nav>
18+
);
19+
20+
export default BreadcrumbRoot;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import BreadcrumbItem from '@/components/Common/Breadcrumbs/BreadcrumbItem';
2+
3+
const BreadcrumbTruncatedItem = () => (
4+
<BreadcrumbItem disableMicrodata>
5+
<button disabled></button>
6+
</BreadcrumbItem>
7+
);
8+
9+
export default BreadcrumbTruncatedItem;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { Meta as MetaObj, StoryObj } from '@storybook/react';
2+
3+
import Breadcrumbs from './';
4+
5+
type Story = StoryObj<typeof Breadcrumbs>;
6+
type Meta = MetaObj<typeof Breadcrumbs>;
7+
8+
export const Default: Story = {
9+
args: {
10+
links: [
11+
{
12+
label: 'Learn',
13+
href: '/learn',
14+
},
15+
{
16+
label: 'Getting Started',
17+
href: '/learn/getting-started',
18+
},
19+
{
20+
label: 'Introduction to Node.js',
21+
href: '/learn/getting-started/intro',
22+
},
23+
],
24+
},
25+
};
26+
27+
export const Truncate: Story = {
28+
args: {
29+
links: [
30+
{
31+
label: 'Learn',
32+
href: '/learn',
33+
},
34+
{
35+
label: 'Getting Started',
36+
href: '/learn/getting-started',
37+
},
38+
{
39+
label: 'Introduction to Node.js',
40+
href: '/learn/getting-started/intro',
41+
},
42+
{
43+
label: 'Installation',
44+
href: '/learn/getting-started/intro/installation',
45+
},
46+
{
47+
label: 'Documentation',
48+
href: '/learn/getting-started/intro/installation/documentation',
49+
},
50+
],
51+
maxLength: 1,
52+
},
53+
};
54+
55+
export const HiddenHome: Story = {
56+
args: {
57+
hideHome: true,
58+
links: [
59+
{
60+
label: 'Learn',
61+
href: '/learn',
62+
},
63+
{
64+
label: 'Getting Started',
65+
href: '/learn/getting-started',
66+
},
67+
{
68+
label: 'Introduction to Node.js',
69+
href: '/learn/getting-started/intro',
70+
},
71+
],
72+
},
73+
};
74+
75+
export default { component: Breadcrumbs } as Meta;

0 commit comments

Comments
 (0)