diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-04-11 22:56:43 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-04-11 22:56:43 +0200 |
| commit | 714273556f5278746a4022d0e87153ff431a61cf (patch) | |
| tree | d87f8df0d04abb66acba1079b0d55732ae5d3f8f /src | |
| parent | 62f06c40a4eac6d11f1a93f3b49dfe6c48ce16f8 (diff) | |
chore: add a Breadcrumb component
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/molecules/nav/breadcrumb.module.scss | 19 | ||||
| -rw-r--r-- | src/components/molecules/nav/breadcrumb.stories.tsx | 48 | ||||
| -rw-r--r-- | src/components/molecules/nav/breadcrumb.test.tsx | 15 | ||||
| -rw-r--r-- | src/components/molecules/nav/breadcrumb.tsx | 113 |
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; |
