Skip to content

Commit

Permalink
feat(breadcrumbs): allow custom separators (#1663)
Browse files Browse the repository at this point in the history
* feat(breadcrumbs): allow custom separators

* refactor(breadcrumbs): specify separators

* test(breadcrumbs): throw new error not string
  • Loading branch information
jinlee93 authored and Jin Lee committed Jun 26, 2023
1 parent eadd005 commit 1fe0e6c
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 50 deletions.
5 changes: 2 additions & 3 deletions .storybook/pages/FeedbackOverview/FeedbackOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react';
import {
Button,
Breadcrumbs,
BreadcrumbsItem,
Card,
Heading,
Icon,
Expand Down Expand Up @@ -183,8 +182,8 @@ export const FeedbackOverview = ({ activeIndex = 0 }: Props) => {
return (
<PageShell className="body--alternate">
<Breadcrumbs className="mb-4">
<BreadcrumbsItem href="#" text="My Courses" />
<BreadcrumbsItem href="#" text="Modern World 2" />
<Breadcrumbs.Item href="#" text="My Courses" />
<Breadcrumbs.Item href="#" text="Modern World 2" />
</Breadcrumbs>
<PageHeader
className="!mb-8 !flex-row"
Expand Down
5 changes: 2 additions & 3 deletions .storybook/pages/ProjectOverview/ProjectOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from 'react';

import {
Breadcrumbs,
BreadcrumbsItem,
Button,
Card,
Grid,
Expand Down Expand Up @@ -34,8 +33,8 @@ export const ProjectOverview = ({ activeIndex = 0 }: Props) => {
return (
<PageShell className="body--alternate" mentoringIsActive>
<Breadcrumbs className="!mb-4">
<BreadcrumbsItem href="#" text="My Courses" />
<BreadcrumbsItem href="#" text="Disciplinary Science 7" />
<Breadcrumbs.Item href="#" text="My Courses" />
<Breadcrumbs.Item href="#" text="Disciplinary Science 7" />
</Breadcrumbs>
<PageHeader
headingSize="h3"
Expand Down
8 changes: 8 additions & 0 deletions src/components/Breadcrumbs/Breadcrumbs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ export const LongText: StoryObj<Args> = {
},
};

export const LongTextCustomSeparator: StoryObj<Args> = {
...LongText,
args: {
...LongText.args,
separator: '>',
},
};

/**
* Mostly for visual regression testing.
*/
Expand Down
107 changes: 65 additions & 42 deletions src/components/Breadcrumbs/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,53 @@
import clsx from 'clsx';
import debounce from 'lodash.debounce';
import React, { type ReactNode } from 'react';
import React, { createContext, useContext, type ReactNode } from 'react';
import { flattenReactChildren } from '../../util/flattenReactChildren';
import BreadcrumbsItem from '../BreadcrumbsItem';
import Menu from '../Menu';
import styles from './Breadcrumbs.module.css';

type Props = {
/**
* aria-label for `nav` element to describe Breadcrumbs navigation to screen readers
*/
'aria-label'?: string;
/**
* Child node(s) that can be nested inside component
*/
children?: ReactNode;
/**
* CSS class names that can be appended to the component.
* CSS class names that can be appended to the component
*/
className?: string;
/**
* aria-label for `nav` element to describe Breadcrumbs navigation to screen readers
*/
'aria-label'?: string;

/**
* HTML id for the component
*/
id?: string;
/**
* Custom string separator between individual breadcrumbs
* Defaults to '/'
*/
separator?: '|' | '>' | '/';
};

type Context = {
separator?: '|' | '>' | '/';
};

const BreadcrumbsContext = createContext<Context>({});
/**
* `import {Breadcrumbs} from "@chanzuckerberg/eds";`
*
* List of Breadcrumb components showing the user where they are in the system and allow them
* to navigate to parent pages.
*/

export const Breadcrumbs = ({
'aria-label': ariaLabel = 'breadcrumbs links',
className,
children,
id,
'aria-label': ariaLabel = 'breadcrumbs links',
separator,
...other
}: Props) => {
const [shouldTruncate, setShouldTruncate] = React.useState(false);
Expand Down Expand Up @@ -103,38 +114,41 @@ export const Breadcrumbs = ({

const componentClassName = clsx(styles['breadcrumbs'], className);
return (
<nav
aria-label={ariaLabel}
className={componentClassName}
id={id}
{...other}
>
<ul className={styles['breadcrumbs__list']} ref={ref}>
{/**
* Back icon breadcrumb always exists, just hidden via css depending on breakpoint to increase performance
*/}
{backBreadCrumb}
{/**
* The ellipsis breadcrumb with Menu only exists if there would be overflow and there are 3 or more breadcrumb items.
*/}
{shouldTruncate && breadcrumbsItems.length > 2 ? (
<>
{breadcrumbsItems[0]}
<BreadcrumbsItem
href={null}
menuItems={menuItems}
variant="collapsed"
/>
{breadcrumbsItems[breadcrumbsItems.length - 1]}
</>
) : (
/**
* If the above conditions aren't met, display all breadcrumbs.
*/
breadcrumbsItems
)}
</ul>
</nav>
<BreadcrumbsContext.Provider value={{ separator }}>
<nav
aria-label={ariaLabel}
className={componentClassName}
id={id}
{...other}
>
<ul className={styles['breadcrumbs__list']} ref={ref}>
{/**
* Back icon breadcrumb always exists, just hidden via css depending on breakpoint to increase performance
*/}
{backBreadCrumb}
{/**
* The ellipsis breadcrumb with Menu only exists if there would be overflow and there are 3 or more breadcrumb items.
*/}
{shouldTruncate && breadcrumbsItems.length > 2 ? (
<>
{breadcrumbsItems[0]}
<BreadcrumbsItem
href={null}
menuItems={menuItems}
separator={separator}
variant="collapsed"
/>
{breadcrumbsItems[breadcrumbsItems.length - 1]}
</>
) : (
/**
* If the above conditions aren't met, display all breadcrumbs.
*/
breadcrumbsItems
)}
</ul>
</nav>
</BreadcrumbsContext.Provider>
);
};

Expand All @@ -152,9 +166,18 @@ const flattenBreadcrumbsItems = (children: React.ReactNode) => {
},
);
if (process.env.NODE_ENV !== 'production' && shouldThrowError) {
throw 'Only <Breadcrumbs.Item>, <BreadcrumbsItem>, or React.Fragment of aforementioned components allowed';
throw new Error(
'Only <Breadcrumbs.Item> or React.Fragment of aforementioned components allowed',
);
}
return flattenedChildren;
};

Breadcrumbs.Item = BreadcrumbsItem;
const CustomSeparatorBreadcrumbsItem = (
props: React.ComponentProps<typeof BreadcrumbsItem>,
) => {
const { separator } = useContext(BreadcrumbsContext);
return <BreadcrumbsItem separator={separator} {...props} />;
};

Breadcrumbs.Item = CustomSeparatorBreadcrumbsItem;
109 changes: 109 additions & 0 deletions src/components/Breadcrumbs/__snapshots__/Breadcrumbs.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,115 @@ exports[`<Breadcrumbs /> LongText story renders snapshot 1`] = `
</div>
`;

exports[`<Breadcrumbs /> LongTextCustomSeparator story renders snapshot 1`] = `
<div
style="margin: 0.5rem;"
>
<nav
aria-label="breadcrumbs links"
class="breadcrumbs"
>
<ul
class="breadcrumbs__list"
>
<li
class="breadcrumbs__item breadcrumbs__item-back"
>
<a
class="breadcrumbs__link"
href="#"
>
<svg
class="icon breadcrumbs__back-icon"
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>
Breadcrumb 2 Lorem ipsum dolor sit amet, no overflow is two lines at 320px
</title>
<path
d="M16 22L6 12L16 2L17.775 3.775L9.55 12L17.775 20.225L16 22Z"
/>
</svg>
</a>
<span
aria-hidden="true"
class="breadcrumbs__separator"
>
&gt;
</span>
</li>
<li
class="breadcrumbs__item"
>
<a
class="breadcrumbs__link"
href="#"
>
Home
</a>
<span
aria-hidden="true"
class="breadcrumbs__separator"
>
&gt;
</span>
</li>
<li
class="breadcrumbs__item"
>
<a
class="breadcrumbs__link"
href="#"
>
Breadcrumb 1
</a>
<span
aria-hidden="true"
class="breadcrumbs__separator"
>
&gt;
</span>
</li>
<li
class="breadcrumbs__item"
>
<a
class="breadcrumbs__link"
href="#"
>
Breadcrumb 2 Lorem ipsum dolor sit amet, no overflow is two lines at 320px
</a>
<span
aria-hidden="true"
class="breadcrumbs__separator"
>
&gt;
</span>
</li>
<li
class="breadcrumbs__item"
>
<a
class="breadcrumbs__link"
href="#"
>
Breadcrumb 3 Lorem ipsum dolor sit amet, consectetur adipiscing elit, no overflow is 3 lines at 320px
</a>
<span
aria-hidden="true"
class="breadcrumbs__separator"
>
&gt;
</span>
</li>
</ul>
</nav>
</div>
`;

exports[`<Breadcrumbs /> OneCrumb story renders snapshot 1`] = `
<div
style="margin: 0.5rem;"
Expand Down
10 changes: 8 additions & 2 deletions src/components/BreadcrumbsItem/BreadcrumbsItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ type Props = {
* Should be <Menu.Item href={href}>{text}</Menu.Item>.
*/
menuItems?: React.ReactNode[];
/**
* Custom string separator after current breadcrumb item.
* Defaults to '/'
*/
separator?: '|' | '>' | '/';
/**
* Breadcrumbs item text.
*/
Expand All @@ -38,9 +43,10 @@ type Props = {
* A single breadcrumb subcomponent, to be used in the Breadcrumbs component.
*/
export const BreadcrumbsItem = ({
menuItems,
className,
href,
menuItems,
separator = '/',
text,
variant,
...other
Expand Down Expand Up @@ -96,7 +102,7 @@ export const BreadcrumbsItem = ({
<li className={componentClassName} {...other}>
{getInteractionElement()}
<span aria-hidden className={styles['breadcrumbs__separator']}>
/
{separator}
</span>
</li>
);
Expand Down

0 comments on commit 1fe0e6c

Please sign in to comment.