Merge pull request #4511 from tudi2d/3965-breadcrumbs
Add breadcrumbs component
This commit is contained in:
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,3 +28,4 @@ export * from './Page';
|
||||
export * from './Sidebar';
|
||||
export * from './SignInPage';
|
||||
export * from './TabbedCard';
|
||||
export * from './Breadcrumbs';
|
||||
|
||||
Reference in New Issue
Block a user