Skip to content

Commit 4cbed35

Browse files
authored
Merge pull request #653 from strapi/enhancement/breadcrumbs-simplified
Breadcrumbs: Implement v2 version
2 parents 51c892f + 9cf0163 commit 4cbed35

File tree

13 files changed

+1936
-624
lines changed

13 files changed

+1936
-624
lines changed

packages/strapi-design-system/src/Breadcrumbs/Breadcrumbs.js

+11-7
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,28 @@ import { VisuallyHidden } from '../VisuallyHidden';
99

1010
const CrumbWrapper = styled(Flex)`
1111
svg {
12-
height: 10px;
13-
width: 10px;
14-
}
15-
svg path {
16-
fill: ${({ theme }) => theme.colors.neutral300};
12+
height: ${10 / 16}rem;
13+
width: ${10 / 16}rem;
14+
path {
15+
fill: ${({ theme }) => theme.colors.neutral500};
16+
}
1717
}
1818
:last-of-type ${Box} {
1919
display: none;
2020
}
21+
:last-of-type ${Typography} {
22+
color: ${({ theme }) => theme.colors.neutral800};
23+
font-weight: ${({ theme }) => theme.fontWeights.bold};
24+
}
2125
`;
2226

2327
export const Crumb = ({ children }) => {
2428
return (
2529
<CrumbWrapper inline as="li">
26-
<Typography fontWeight="bold" color="neutral800">
30+
<Typography variant="pi" textColor="neutral600">
2731
{children}
2832
</Typography>
29-
<Box paddingLeft={3} paddingRight={3}>
33+
<Box aria-hidden paddingLeft={3} paddingRight={3}>
3034
<ChevronRight />
3135
</Box>
3236
</CrumbWrapper>

packages/strapi-design-system/src/Layout/HeaderLayout.js

+20-8
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ const StickyBox = styled(Box)`
6363

6464
export const BaseHeaderLayout = React.forwardRef(
6565
({ navigationAction, primaryAction, secondaryAction, subtitle, title, sticky, width, ...props }, ref) => {
66+
const isSubtitleString = typeof subtitle === 'string';
67+
6668
if (sticky) {
6769
return (
6870
<StickyBox
@@ -81,9 +83,13 @@ export const BaseHeaderLayout = React.forwardRef(
8183
<Typography variant="beta" as="h1" {...props}>
8284
{title}
8385
</Typography>
84-
<Typography variant="pi" textColor="neutral600">
85-
{subtitle}
86-
</Typography>
86+
{isSubtitleString ? (
87+
<Typography variant="pi" textColor="neutral600">
88+
{subtitle}
89+
</Typography>
90+
) : (
91+
subtitle
92+
)}
8793
</Box>
8894
{secondaryAction ? <Box paddingLeft={4}>{secondaryAction}</Box> : null}
8995
</Flex>
@@ -113,9 +119,13 @@ export const BaseHeaderLayout = React.forwardRef(
113119
</Flex>
114120
{primaryAction}
115121
</Flex>
116-
<Typography variant="epsilon" textColor="neutral600" as="p">
117-
{subtitle}
118-
</Typography>
122+
{isSubtitleString ? (
123+
<Typography variant="epsilon" textColor="neutral600" as="p">
124+
{subtitle}
125+
</Typography>
126+
) : (
127+
subtitle
128+
)}
119129
</Box>
120130
);
121131
},
@@ -137,7 +147,8 @@ BaseHeaderLayout.propTypes = {
137147
primaryAction: PropTypes.node,
138148
secondaryAction: PropTypes.node,
139149
sticky: PropTypes.bool,
140-
subtitle: PropTypes.string,
150+
// TODO V2: Remove the string fallback
151+
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
141152
title: PropTypes.string.isRequired,
142153
width: PropTypes.number,
143154
};
@@ -153,6 +164,7 @@ HeaderLayout.propTypes = {
153164
navigationAction: PropTypes.node,
154165
primaryAction: PropTypes.node,
155166
secondaryAction: PropTypes.node,
156-
subtitle: PropTypes.string,
167+
// TODO V2: Remove the string fallback
168+
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
157169
title: PropTypes.string.isRequired,
158170
};

packages/strapi-design-system/src/Layout/HeaderLayout.stories.mdx

+22-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Plus from '@strapi/icons/Plus';
99
import { BaseHeaderLayout, HeaderLayout } from './HeaderLayout';
1010
import { ModalLayout } from '../ModalLayout';
1111
import { Link } from '../Link';
12+
import { Breadcrumbs, Crumb } from '../Breadcrumbs';
1213
import { Button } from '../Button';
1314
import { Box } from '../Box';
1415
import { VisuallyHidden } from '../VisuallyHidden';
@@ -62,7 +63,7 @@ Headers are placed at the very top of each page. Descriptions and buttons are op
6263
</Story>
6364
</Canvas>
6465

65-
### BaseHeaderLayout without nav action
66+
### HeaderLayout without nav action
6667

6768
<Canvas>
6869
<Story name="base without nav action">
@@ -82,7 +83,26 @@ Headers are placed at the very top of each page. Descriptions and buttons are op
8283
</Story>
8384
</Canvas>
8485

85-
### BaseHeaderLayout sticky
86+
### HeaderLayout with custom subtitle
87+
88+
<Canvas>
89+
<Story name="base with custom subtitle">
90+
<Box background="neutral100">
91+
<BaseHeaderLayout
92+
title="Media Library"
93+
subtitle={
94+
<Breadcrumbs label="folders">
95+
<Crumb>Animals</Crumb>
96+
<Crumb>Cats</Crumb>
97+
</Breadcrumbs>
98+
}
99+
as="h2"
100+
/>
101+
</Box>
102+
</Story>
103+
</Canvas>
104+
105+
### HeaderLayout sticky
86106

87107
<Canvas>
88108
<Story name="sticky">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React, { Children } from 'react';
2+
import PropTypes from 'prop-types';
3+
import styled from 'styled-components';
4+
5+
import { Box } from '../../Box';
6+
import { Crumb } from './Crumb';
7+
import { CrumbLink } from './CrumbLink';
8+
import { CrumbSimpleMenu } from './CrumbSimpleMenu';
9+
import { Divider } from './Divider';
10+
import { Flex } from '../../Flex';
11+
12+
const AlignedList = styled(Flex)`
13+
// CrumbLinks do have padding-x, because they need to have a
14+
// interaction effect, which mis-aligns the breadcrumbs on the left.
15+
// This normalizes the behavior by moving the first item to left by
16+
// the same amount it has inner padding
17+
:first-child {
18+
margin-left: ${({ theme }) => `calc(-1*${theme.spaces[2]})`};
19+
}
20+
`;
21+
22+
export const Breadcrumbs = ({ label, children, ...props }) => {
23+
return (
24+
<Box aria-label={label} {...props}>
25+
<AlignedList as="ol" horizontal>
26+
{Children.map(children, (child, index) => {
27+
const isLast = index + 1 === children.length;
28+
29+
return (
30+
<Flex inline as="li">
31+
{child}
32+
{!isLast && <Divider />}
33+
</Flex>
34+
);
35+
})}
36+
</AlignedList>
37+
</Box>
38+
);
39+
};
40+
41+
const crumbType = PropTypes.shape({ type: PropTypes.oneOf([Crumb, CrumbLink, CrumbSimpleMenu]) });
42+
43+
Breadcrumbs.displayName = 'Breadcrumbs';
44+
45+
Breadcrumbs.propTypes = {
46+
children: PropTypes.oneOfType([PropTypes.arrayOf(crumbType), crumbType]).isRequired,
47+
label: PropTypes.string.isRequired,
48+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<!--- Breadcrumbs.stories.mdx --->
2+
3+
import { Meta, Story, Canvas } from '@storybook/addon-docs';
4+
import { ArgsTable } from '@storybook/addon-docs';
5+
import { Breadcrumbs } from './Breadcrumbs';
6+
import { Crumb } from './Crumb';
7+
import { CrumbLink } from './CrumbLink';
8+
import { CrumbSimpleMenu } from './CrumbSimpleMenu';
9+
import { MenuItem } from '../SimpleMenu';
10+
import { Stack } from '../../Stack';
11+
import CollectionType from '@strapi/icons/CollectionType';
12+
13+
<Meta title="Design System/Components/v2/Breadcrumbs" component={Breadcrumbs} />
14+
15+
# Breadcrumbs
16+
17+
Breadcrumbs are a list of hierarchical links that inform users about their possible navigation path upwards.
18+
19+
**Best practices**
20+
21+
- Use breadcrumbs whenever a long path to achieve an action is required.
22+
- Use breadcrumbs in modal's headers.
23+
- Using breadcrumbs in a page or a form is _not_ a consistent experience.
24+
25+
[View source](https://github.com/strapi/design-system/tree/main/packages/strapi-design-system/src/Breadcrumbs)
26+
27+
## Imports
28+
29+
```js
30+
import { Breadcrumbs } from '@strapi/design-system/Breadcrumbs';
31+
```
32+
33+
## Usage
34+
35+
Breadcrumbs with CrumbLink are a list of link to help navigation.
36+
37+
<Canvas>
38+
<Story name="base">
39+
<Stack horizontal spacing={3}>
40+
<Breadcrumbs label="Folder navigatation" as="nav">
41+
<CrumbLink href="/">Media Library</CrumbLink>
42+
<Crumb isCurrent>
43+
Cats
44+
</Crumb>
45+
</Breadcrumbs>
46+
</Stack>
47+
</Story>
48+
</Canvas>
49+
50+
## Usage
51+
52+
Breadcrumbs with CrumbSimpleMenu displays a list of potential options or actions via a Popover component.
53+
54+
<Canvas>
55+
<Story name="with-menu">
56+
<Stack horizontal spacing={3}>
57+
<Breadcrumbs label="Folder navigatation" as="nav">
58+
<CrumbLink href="/">Media Library</CrumbLink>
59+
<CrumbSimpleMenu label="..." aria-label="Some items are not displayed">
60+
<MenuItem to="/">Home</MenuItem>
61+
<MenuItem to="/somewhere">Somewhere internal</MenuItem>
62+
</CrumbSimpleMenu>
63+
<Crumb isCurrent>
64+
Cats
65+
</Crumb>
66+
</Breadcrumbs>
67+
</Stack>
68+
</Story>
69+
</Canvas>
70+
71+
## Without navigation
72+
73+
Breadcrumbs with Crumb are visual information only and cannot be navigated. They are mostly part of modals' headers.
74+
75+
<Canvas>
76+
<Story name="without-nagivation">
77+
<Stack horizontal spacing={3}>
78+
<CollectionType />
79+
<Breadcrumbs label="Category model, name field">
80+
<Crumb>Category</Crumb>
81+
<Crumb isCurrent>Name</Crumb>
82+
</Breadcrumbs>
83+
</Stack>
84+
</Story>
85+
</Canvas>
86+
87+
### Usage with other routing libraries
88+
89+
To use the Link component with a routing library (e.g. react-router-dom), you'll need to pass the `NavLink` component to the `as` prop in order to replace the default HTML anchor `<a />`.
90+
You'll now be able to pass all `NavLink` props.
91+
92+
```jsx
93+
import { Breadcrumbs, CrumbLink } from '@strapi/design-system/Breadcrumbs';
94+
import { NavLink } from 'react-router-dom';
95+
96+
<Breadcrumbs label="Folder navigatation" as="nav">
97+
<CrumbLink as={NavLink} to="/">
98+
Media Library
99+
</CrumbLink>
100+
</Breadcrumbs>;
101+
```
102+
103+
#### NextJS usage
104+
105+
For NextJS, you'll need to wrap the `CrumbLink` with the `NextLink` component
106+
107+
```jsx
108+
import { Breadcrumbs, CrumbLink } from '@strapi/design-system/Breadcrumbs';
109+
import NextLink from 'next/link';
110+
111+
<Breadcrumbs label="Folder navigatation" as="nav">
112+
<NextLink href="/home" passHref>
113+
<CrumbLink>Media Library</CrumbLink>
114+
</NextLink>
115+
</Breadcrumbs>;
116+
```
117+
118+
## Props
119+
120+
<ArgsTable of={Breadcrumbs} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import { Box } from '../../Box';
5+
import { Typography } from '../../Typography';
6+
7+
export const Crumb = ({ children, isCurrent, ...props }) => (
8+
<Box paddingLeft={1} paddingRight={1}>
9+
<Typography
10+
variant="pi"
11+
textColor="neutral800"
12+
fontWeight={isCurrent ? 'bold' : 'normal'}
13+
aria-current={isCurrent}
14+
{...props}
15+
>
16+
{children}
17+
</Typography>
18+
</Box>
19+
);
20+
21+
Crumb.displayName = 'Crumb';
22+
23+
Crumb.defaultProps = {
24+
isCurrent: false,
25+
};
26+
27+
Crumb.propTypes = {
28+
children: PropTypes.string.isRequired,
29+
isCurrent: PropTypes.bool,
30+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import styled from 'styled-components';
4+
5+
import { BaseLink } from '../../BaseLink';
6+
7+
const StyledLink = styled(BaseLink)`
8+
border-radius: ${({ theme }) => theme.borderRadius};
9+
color: ${({ theme }) => theme.colors.neutral600};
10+
font-size: ${({ theme }) => theme.fontSizes[1]};
11+
line-height: ${({ theme }) => theme.lineHeights[4]};
12+
padding: ${({ theme }) => `${theme.spaces[1]} ${theme.spaces[2]}`};
13+
text-decoration: none;
14+
15+
:hover,
16+
:focus {
17+
background-color: ${({ theme }) => theme.colors.neutral100};
18+
color: ${({ theme }) => theme.colors.neutral700};
19+
}
20+
`;
21+
22+
export const CrumbLink = ({ children, ...props }) => <StyledLink {...props}>{children}</StyledLink>;
23+
24+
CrumbLink.displayName = 'CrumbLink';
25+
26+
CrumbLink.propTypes = {
27+
children: PropTypes.string.isRequired,
28+
href: PropTypes.string.isRequired,
29+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import PropTypes from 'prop-types';
2+
import React from 'react';
3+
4+
import { SimpleMenu } from '../SimpleMenu';
5+
6+
import CarretDown from '@strapi/icons/CarretDown';
7+
8+
export const CrumbSimpleMenu = ({ children, ...props }) => (
9+
<SimpleMenu noBorder icon={<CarretDown />} size="S" {...props}>
10+
{children}
11+
</SimpleMenu>
12+
);
13+
14+
CrumbSimpleMenu.displayName = 'CrumbSimpleMenu';
15+
16+
CrumbSimpleMenu.propTypes = {
17+
'aria-label': PropTypes.string.isRequired,
18+
children: PropTypes.arrayOf(SimpleMenu),
19+
};

0 commit comments

Comments
 (0)