summaryrefslogtreecommitdiffstats
path: root/src/components/molecules/nav
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/molecules/nav')
-rw-r--r--src/components/molecules/nav/breadcrumb.module.scss19
-rw-r--r--src/components/molecules/nav/breadcrumb.stories.tsx48
-rw-r--r--src/components/molecules/nav/breadcrumb.test.tsx15
-rw-r--r--src/components/molecules/nav/breadcrumb.tsx113
4 files changed, 195 insertions, 0 deletions
diff --git a/src/components/molecules/nav/breadcrumb.module.scss b/src/components/molecules/nav/breadcrumb.module.scss
new file mode 100644
index 0000000..c26f60a
--- /dev/null
+++ b/src/components/molecules/nav/breadcrumb.module.scss
@@ -0,0 +1,19 @@
+@use "@styles/abstracts/placeholders";
+
+.list {
+ @extend %reset-ordered-list;
+
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ gap: var(--spacing-2xs);
+}
+
+.item {
+ &:not(:last-of-type) {
+ &::after {
+ content: ">";
+ margin-left: var(--spacing-2xs);
+ }
+ }
+}
diff --git a/src/components/molecules/nav/breadcrumb.stories.tsx b/src/components/molecules/nav/breadcrumb.stories.tsx
new file mode 100644
index 0000000..d283619
--- /dev/null
+++ b/src/components/molecules/nav/breadcrumb.stories.tsx
@@ -0,0 +1,48 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { IntlProvider } from 'react-intl';
+import BreadcrumbComponent, { type BreadcrumbItem } from './breadcrumb';
+
+export default {
+ title: 'Molecules/Nav',
+ component: BreadcrumbComponent,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ table: {
+ category: 'Styles',
+ },
+ description: 'Set additional classnames to the nav element.',
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ items: {
+ description: 'The breadcrumb items.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ },
+} as ComponentMeta<typeof BreadcrumbComponent>;
+
+const Template: ComponentStory<typeof BreadcrumbComponent> = (args) => (
+ <IntlProvider locale="en">
+ <BreadcrumbComponent {...args} />
+ </IntlProvider>
+);
+
+const items: BreadcrumbItem[] = [
+ { id: 'home', url: '#', name: 'Home' },
+ { id: 'blog', url: '#', name: 'Blog' },
+ { id: 'post1', url: '#', name: 'A Post' },
+];
+
+export const Breadcrumb = Template.bind({});
+Breadcrumb.args = {
+ items,
+};
diff --git a/src/components/molecules/nav/breadcrumb.test.tsx b/src/components/molecules/nav/breadcrumb.test.tsx
new file mode 100644
index 0000000..43220c9
--- /dev/null
+++ b/src/components/molecules/nav/breadcrumb.test.tsx
@@ -0,0 +1,15 @@
+import { render, screen } from '@test-utils';
+import Breadcrumb, { type BreadcrumbItem } from './breadcrumb';
+
+const items: BreadcrumbItem[] = [
+ { id: 'home', url: '#', name: 'Home' },
+ { id: 'blog', url: '#', name: 'Blog' },
+ { id: 'post1', url: '#', name: 'A Post' },
+];
+
+describe('Breadcrumb', () => {
+ it('renders a navigation', () => {
+ render(<Breadcrumb items={items} />);
+ expect(screen.getByRole('navigation')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/nav/breadcrumb.tsx b/src/components/molecules/nav/breadcrumb.tsx
new file mode 100644
index 0000000..33af735
--- /dev/null
+++ b/src/components/molecules/nav/breadcrumb.tsx
@@ -0,0 +1,113 @@
+import Link from '@components/atoms/links/link';
+import { settings } from '@utils/config';
+import Script from 'next/script';
+import { VFC } from 'react';
+import { useIntl } from 'react-intl';
+import { BreadcrumbList, ListItem, WithContext } from 'schema-dts';
+import styles from './breadcrumb.module.scss';
+
+export type BreadcrumbItem = {
+ /**
+ * The item id.
+ */
+ id: string;
+ /**
+ * The item URL.
+ */
+ url: string;
+ /**
+ * The item name.
+ */
+ name: string;
+};
+
+export type BreadcrumbProps = {
+ /**
+ * Set additional classnames to the nav element.
+ */
+ className?: string;
+ /**
+ * The breadcrumb items
+ */
+ items: BreadcrumbItem[];
+};
+
+/**
+ * Breadcrumb component
+ *
+ * Render a breadcrumb navigation.
+ */
+const Breadcrumb: VFC<BreadcrumbProps> = ({ items, ...props }) => {
+ const intl = useIntl();
+
+ /**
+ * Retrieve the breadcrumb list items.
+ *
+ * @param {BreadcrumbItem[]} list - The breadcrumb items.
+ * @returns {JSX.Element[]} The list items.
+ */
+ const getListItems = (list: BreadcrumbItem[]): JSX.Element[] => {
+ return list.map((item, index) => {
+ const isLastItem = index === list.length - 1;
+ const itemClassnames = isLastItem
+ ? `${styles.item} screen-reader-text`
+ : styles.item;
+
+ return (
+ <li key={item.id} className={itemClassnames}>
+ {isLastItem ? item.name : <Link href={item.url}>{item.name}</Link>}
+ </li>
+ );
+ });
+ };
+
+ /**
+ * Retrieve the breadcrumb list items with Schema.org format.
+ *
+ * @param {BreadcrumbItem[]} list - The breadcrumb items.
+ * @returns {ListItem[]} An array of list items using Schema.org format.
+ */
+ const getSchemaItems = (list: BreadcrumbItem[]): ListItem[] => {
+ const schemaItems: ListItem[] = [];
+
+ list.forEach((item, index) => {
+ schemaItems.push({
+ '@type': 'ListItem',
+ position: index + 1,
+ name: item.name,
+ item: item.url,
+ });
+ });
+
+ return schemaItems;
+ };
+
+ const schemaJsonLd: WithContext<BreadcrumbList> = {
+ '@context': 'https://schema.org',
+ '@type': 'BreadcrumbList',
+ '@id': `${settings.url}/#breadcrumb`,
+ itemListElement: getSchemaItems(items),
+ };
+
+ return (
+ <>
+ <Script
+ id="schema-breadcrumb"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ />
+ <nav {...props}>
+ <span className="screen-reader-text">
+ {intl.formatMessage({
+ defaultMessage: 'You are here:',
+ description: 'Breadcrumb: You are here prefix',
+ id: '16zl9Z',
+ })}
+ </span>
+ <ol className={styles.list}>{getListItems(items)}</ol>
+ </nav>
+ </>
+ );
+};
+
+export default Breadcrumb;