Skip to content

Commit 2e091b6

Browse files
committed
feat(breadcrumbs): adds breadcrumbs components
Adds `Breadcrumbs` and `Crumb` components to support adding breadcrumbs to a page. Follows WAI-ARIA guidelines for Breadcrumbs. Supports using child link component for routing.
1 parent 87ca830 commit 2e091b6

File tree

7 files changed

+159
-0
lines changed

7 files changed

+159
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { ComponentStory, Meta } from '@storybook/react'
2+
import React from 'react'
3+
import {
4+
Link as RouterLink,
5+
MemoryRouter,
6+
Route,
7+
Routes,
8+
} from 'react-router-dom'
9+
import { Breadcrumbs, Crumb } from '.'
10+
import { Divider } from '../Divider'
11+
12+
export default {
13+
title: 'Components/Breadcrumbs',
14+
component: Breadcrumbs,
15+
subcomponents: { Crumb },
16+
} as Meta
17+
18+
const Template: ComponentStory<typeof Breadcrumbs> = (args) => (
19+
<Breadcrumbs {...args}>
20+
<Crumb href="/">Home</Crumb>
21+
<Crumb href="/first">First</Crumb>
22+
<Crumb href="/first/second" isCurrentPage>
23+
Second
24+
</Crumb>
25+
</Breadcrumbs>
26+
)
27+
export const Primary = Template.bind({})
28+
Primary.args = {}
29+
30+
export const WithRouter = () => {
31+
return (
32+
<MemoryRouter>
33+
<Breadcrumbs>
34+
<Crumb asChild>
35+
<RouterLink to="/">Home</RouterLink>
36+
</Crumb>
37+
<Crumb asChild>
38+
<RouterLink to="/one">One</RouterLink>
39+
</Crumb>
40+
<Crumb asChild isCurrentPage>
41+
<RouterLink to="/one/two">Two</RouterLink>
42+
</Crumb>
43+
</Breadcrumbs>
44+
<Divider />
45+
<Routes>
46+
<Route index element={<div>Home</div>} />
47+
<Route path="/one" element={<div>Route 1</div>} />
48+
<Route path="/one/two" element={<div>Route 2</div>} />
49+
</Routes>
50+
</MemoryRouter>
51+
)
52+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react'
2+
import { renderDark, renderLight } from '../../test'
3+
import { Primary } from './Breadcrumbs.stories'
4+
5+
it('renders light without error', () => {
6+
const { asFragment } = renderLight(<Primary />)
7+
expect(asFragment()).toBeDefined()
8+
})
9+
10+
it('renders dark without error', () => {
11+
const { asFragment } = renderDark(<Primary />)
12+
expect(asFragment()).toBeDefined()
13+
})
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React, { ComponentProps, ElementRef, forwardRef } from 'react'
2+
import { CSSProps, styled, VariantProps } from '../../stitches.config'
3+
import { Link, LinkProps } from '../Link'
4+
5+
const BREADCRUMB_TAG = 'nav'
6+
const BREADCRUMB_LIST_TAG = 'ol'
7+
const CRUMB_TAG = 'li'
8+
9+
/**
10+
* StyledBreadcrumbs base component
11+
*/
12+
const StyledBreadcrumbs = styled(BREADCRUMB_TAG, {})
13+
const BreadcrumbList = styled(BREADCRUMB_LIST_TAG, {
14+
padding: 0,
15+
margin: 0,
16+
listStyleType: 'none',
17+
})
18+
19+
const StyledCrumb = styled(CRUMB_TAG, {
20+
display: 'inline',
21+
$$spacing: '$space$3',
22+
23+
'&:not(:first-of-type)': {
24+
marginLeft: '$$spacing',
25+
26+
'&::before': {
27+
content: '',
28+
opacity: 0.25,
29+
marginRight: '$$spacing',
30+
display: 'inline-block',
31+
transform: 'rotate(15deg)',
32+
borderRight: '1px solid',
33+
height: '0.8em',
34+
},
35+
},
36+
})
37+
38+
type BreadcrumbsVariants = VariantProps<typeof StyledBreadcrumbs>
39+
type BreadcrumbsProps = BreadcrumbsVariants &
40+
CSSProps &
41+
ComponentProps<typeof BREADCRUMB_TAG>
42+
43+
export const Breadcrumbs = forwardRef<
44+
ElementRef<typeof StyledBreadcrumbs>,
45+
BreadcrumbsProps
46+
>(({ children, ...props }, forwardedRef) => {
47+
return (
48+
<StyledBreadcrumbs aria-label="Breadcrumb" {...props} ref={forwardedRef}>
49+
<BreadcrumbList>{children}</BreadcrumbList>
50+
</StyledBreadcrumbs>
51+
)
52+
})
53+
Breadcrumbs.toString = () => `.${StyledBreadcrumbs.className}`
54+
55+
export type CrumbProps = LinkProps & {
56+
isCurrentPage?: boolean
57+
}
58+
59+
export const Crumb = ({
60+
isCurrentPage = false,
61+
children,
62+
...props
63+
}: CrumbProps) => {
64+
return (
65+
<StyledCrumb>
66+
<Link
67+
variant={isCurrentPage ? 'clear' : 'hovered'}
68+
aria-current={isCurrentPage ? 'page' : undefined}
69+
{...props}
70+
>
71+
{children}
72+
</Link>
73+
</StyledCrumb>
74+
)
75+
}

src/components/Breadcrumbs/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Breadcrumbs'

src/components/Link/Link.stories.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ export const Clear: Story = () => (
4747
</Text>
4848
)
4949

50+
/**
51+
* For when a link should be hidden but shown on hover
52+
*/
53+
export const Hovered: Story = () => (
54+
<Text>
55+
<Link variant="hovered" href="#">
56+
Link
57+
</Link>
58+
</Text>
59+
)
60+
5061
/**
5162
* The styled variant is intended for use in articles such as blog posts.
5263
*/

src/components/Link/Link.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ export const linkStyles = css({
8686
default: {
8787
textDecoration: 'underline',
8888
},
89+
hovered: {
90+
textDecoration: 'none',
91+
'&:hover': {
92+
textDecoration: 'revert',
93+
},
94+
},
8995
styled: {
9096
'@motion': {
9197
transition: 'background 100ms ease-out',

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './Avatar'
66
export * from './Backdrop'
77
export * from './Badge'
88
export * from './Box'
9+
export * from './Breadcrumbs'
910
export * from './Button'
1011
export * from './Card'
1112
export * from './Checkbox'

0 commit comments

Comments
 (0)