Merge pull request #4511 from tudi2d/3965-breadcrumbs

Add breadcrumbs component
This commit is contained in:
Ben Lambert
2021-02-13 03:16:00 +01:00
committed by GitHub
7 changed files with 319 additions and 17 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core': patch
---
Add Breadcrumbs component
@@ -0,0 +1,137 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Box, List, ListItem, Popover, Typography } from '@material-ui/core';
import ExpandLessIcon from '@material-ui/icons/ExpandLess';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import React, { Fragment } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { Breadcrumbs } from '.';
import { Header, Page } from '..';
import { Link } from '../../components/Link';
export default {
title: 'Layout/Breadcrumbs',
component: Breadcrumbs,
};
export const InHeader = () => (
<MemoryRouter>
<h2>Standard breadcrumbs</h2>
<p>
Underlined pages are links. This should show a hierarchical relationship.
</p>
<Page themeId="other">
<Header title="Current Page" type="General Page" typeLink="/" />
</Page>
</MemoryRouter>
);
export const OutsideOfHeader = () => {
const [anchorEl, setAnchorEl] = React.useState<HTMLAnchorElement | null>(
null,
);
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
return (
<MemoryRouter>
<p>
It might be the case that you want to keep your breadcrumbs outside of
the header. In that case, they should be positioned above the title of
the page.
</p>
<h2>Standard breadcrumbs</h2>
<p>
Underlined pages are links. This should show a hierarchical
relationship.
</p>
<Breadcrumbs color="primaryText" />
<Breadcrumbs color="primaryText">
<Link to="/">General Page</Link>
<Link to="/">Second Page</Link>
<Typography>Current page</Typography>
</Breadcrumbs>
<h2>Hidden breadcrumbs</h2>
<p>
Use this when you have more than three breadcrumbs. When user clicks on
ellipses, expand the breadcrumbs out.
</p>
<Breadcrumbs color="primaryText">
<Link to="/">General Page</Link>
<Link to="/">Second Page</Link>
<Link to="/">Third Page</Link>
<Link to="/">Fourth Page</Link>
<Typography>Current page</Typography>
</Breadcrumbs>
<h2>Layered breadcrumbs</h2>
<p>
Use this when you want to show alternative breadcrumbs on the same
hierarchical level.
</p>
<Fragment>
<Breadcrumbs color="primaryText">
<Link to="/">General Page</Link>
<Link to="/" onClick={handleClick}>
<Box display="flex" alignItems="center">
<span>Second Page</span>
{open ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</Box>
</Link>
<Typography>Current page</Typography>
</Breadcrumbs>
<Popover
open={open}
onClose={handleClose}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<List>
<ListItem button style={{ textDecoration: 'underline' }}>
Parallel second page
</ListItem>
<ListItem button style={{ textDecoration: 'underline' }}>
Another parallel second page
</ListItem>
<ListItem button style={{ textDecoration: 'underline' }}>
Yet another, parallel second page
</ListItem>
</List>
</Popover>
</Fragment>
</MemoryRouter>
);
};
@@ -0,0 +1,54 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { renderInTestApp } from '@backstage/test-utils';
import { Typography } from '@material-ui/core';
import { fireEvent } from '@testing-library/react';
import React from 'react';
import { Link } from '../..';
import { Breadcrumbs } from './Breadcrumbs';
describe('<Breadcrumbs/>', () => {
it('should render', async () => {
const rendered = await renderInTestApp(
<Breadcrumbs>
<Link to="/">General Page</Link>
<Typography>Current Page</Typography>
</Breadcrumbs>,
);
expect(rendered.getByLabelText('breadcrumb')).toBeVisible();
expect(rendered.getByText('General Page')).toBeVisible();
expect(rendered.getByText('Current Page')).toBeVisible();
});
it('should render hidden breadcrumbs', async () => {
const rendered = await renderInTestApp(
<Breadcrumbs>
<Link to="/">General Page</Link>
<Link to="/">Second Page</Link>
<Link to="/">Third Page</Link>
<Link to="/">Fourth Page</Link>
<Typography>Current page</Typography>
</Breadcrumbs>,
);
expect(rendered.getByText('...')).toBeVisible();
expect(rendered.queryByText('Third Page')).not.toBeInTheDocument();
expect(rendered.queryByText('Fourth Page')).not.toBeInTheDocument();
fireEvent.click(rendered.getByText('...'));
expect(rendered.getByText('Third Page')).toBeVisible();
expect(rendered.getByText('Fourth Page')).toBeVisible();
});
});
@@ -0,0 +1,99 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Box,
Breadcrumbs as MaterialBreadcrumbs,
List,
ListItem,
Popover,
Typography,
withStyles,
} from '@material-ui/core';
import React, { ComponentProps, Fragment } from 'react';
type Props = ComponentProps<typeof MaterialBreadcrumbs>;
const ClickableText = withStyles({
root: {
textDecoration: 'underline',
cursor: 'pointer',
},
})(Typography);
const StyledBox = withStyles({
root: {
textDecoration: 'underline',
color: 'inherit',
},
})(Box);
export const Breadcrumbs = ({ children, ...props }: Props) => {
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(
null,
);
const childrenArray = React.Children.toArray(children);
const [firstPage, secondPage, ...expandablePages] = childrenArray;
const currentPage = expandablePages.length
? expandablePages.pop()
: childrenArray[childrenArray.length - 1];
const hasHiddenBreadcrumbs = childrenArray.length > 3;
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
return (
<Fragment>
<MaterialBreadcrumbs aria-label="breadcrumb" {...props}>
{childrenArray.length > 1 && <StyledBox clone>{firstPage}</StyledBox>}
{childrenArray.length > 2 && <StyledBox clone>{secondPage}</StyledBox>}
{hasHiddenBreadcrumbs && (
<ClickableText onClick={handleClick}>...</ClickableText>
)}
<Box style={{ fontStyle: 'italic' }}>{currentPage}</Box>
</MaterialBreadcrumbs>
<Popover
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<List>
{expandablePages.map(pageLink => (
<ListItem button>
<StyledBox clone>{pageLink}</StyledBox>
</ListItem>
))}
</List>
</Popover>
</Fragment>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { Breadcrumbs } from './Breadcrumbs';
+6 -17
View File
@@ -16,15 +16,10 @@
import React, { ReactNode, CSSProperties, PropsWithChildren } from 'react';
import { Helmet } from 'react-helmet';
import {
Link,
Typography,
Tooltip,
makeStyles,
Breadcrumbs,
} from '@material-ui/core';
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
import { Typography, Tooltip, makeStyles } from '@material-ui/core';
import { BackstageTheme } from '@backstage/theme';
import { Breadcrumbs } from '..';
import { Link } from '../../components';
const useStyles = makeStyles<BackstageTheme>(theme => ({
header: {
@@ -136,15 +131,9 @@ const TypeFragment = ({
}
return (
<Breadcrumbs
aria-label="breadcrumb"
separator={<ChevronRightIcon fontSize="small" />}
className={classes.breadcrumb}
>
<Link href={typeLink} color="inherit">
<Typography className={classes.breadcrumbType}> {type}</Typography>
</Link>
<Typography className={classes.breadcrumbTitle}>{pageTitle}</Typography>
<Breadcrumbs className={classes.breadcrumb}>
<Link to={typeLink}>{type}</Link>
<Typography>{pageTitle}</Typography>
</Breadcrumbs>
);
};
+1
View File
@@ -28,3 +28,4 @@ export * from './Page';
export * from './Sidebar';
export * from './SignInPage';
export * from './TabbedCard';
export * from './Breadcrumbs';