Merge pull request #425 from guillaumeLamanda/407-header-2
#407 Rewrite layout/Header to use TypeScript and hooks
This commit is contained in:
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* 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 React, { Component, Fragment } from 'react';
|
||||
import { Typography, withStyles } from '@material-ui/core';
|
||||
import Helmet from 'react-helmet';
|
||||
// import FavoriteButton from 'shared/components/layout/FavoriteButton';
|
||||
|
||||
const styles = theme => ({
|
||||
container: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
},
|
||||
leftItemsBox: {
|
||||
flex: '1 1 auto',
|
||||
marginBottom: theme.spacing(1),
|
||||
minWidth: 0,
|
||||
overflow: 'visible',
|
||||
},
|
||||
rightItemsBox: {
|
||||
flex: '0 1 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
marginLeft: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
minWidth: 0,
|
||||
overflow: 'visible',
|
||||
},
|
||||
description: {},
|
||||
title: {
|
||||
display: 'inline-flex',
|
||||
},
|
||||
});
|
||||
|
||||
class ContentHeader extends Component {
|
||||
static defaultProps = {
|
||||
favoriteable: true,
|
||||
title: 'Unknown page',
|
||||
titleComponent: undefined,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
/* favoriteable,*/ children,
|
||||
classes,
|
||||
} = this.props;
|
||||
const TitleComponent = this.props.titleComponent;
|
||||
const renderedTitle =
|
||||
TitleComponent !== undefined ? (
|
||||
<TitleComponent />
|
||||
) : (
|
||||
<Typography
|
||||
variant="h4"
|
||||
className={classes.title}
|
||||
data-testid="header-title"
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Helmet title={title} />
|
||||
<div className={classes.container}>
|
||||
<div className={classes.leftItemsBox}>
|
||||
{renderedTitle}
|
||||
{/* favoriteable && <FavoriteButton /> */}
|
||||
{description && (
|
||||
<Typography className={classes.description} variant="body2">
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<div className={classes.rightItemsBox}>{children}</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(ContentHeader);
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import ContentHeader from './ContentHeader';
|
||||
import { wrapInThemedTestApp } from '../../testUtils';
|
||||
|
||||
jest.mock('react-helmet', () => {
|
||||
return ({ defaultTitle }) => <div>defaultTitle: {defaultTitle}</div>;
|
||||
});
|
||||
|
||||
describe('<ContentHeader/>', () => {
|
||||
it('should render with title', () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(<ContentHeader title="Title" />),
|
||||
);
|
||||
rendered.getByText('Title');
|
||||
});
|
||||
|
||||
it('should render with titleComponent', () => {
|
||||
const title = 'Custom title';
|
||||
const titleComponent = () => <h1>{title}</h1>;
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(<ContentHeader titleComponent={titleComponent} />),
|
||||
);
|
||||
rendered.getByText(title);
|
||||
});
|
||||
|
||||
it('should render with description', () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(<ContentHeader description="description" />),
|
||||
);
|
||||
rendered.getByText('description');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* TODO favoriteable capability
|
||||
*/
|
||||
|
||||
import React, { ComponentType, Fragment, FC } from 'react';
|
||||
import { Typography, makeStyles } from '@material-ui/core';
|
||||
import Helmet from 'react-helmet';
|
||||
import { BackstageTheme } from '../../theme/theme';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
container: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
},
|
||||
leftItemsBox: {
|
||||
flex: '1 1 auto',
|
||||
marginBottom: theme.spacing(1),
|
||||
minWidth: 0,
|
||||
overflow: 'visible',
|
||||
},
|
||||
rightItemsBox: {
|
||||
flex: '0 1 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
marginLeft: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
minWidth: 0,
|
||||
overflow: 'visible',
|
||||
},
|
||||
description: {},
|
||||
title: {
|
||||
display: 'inline-flex',
|
||||
},
|
||||
}));
|
||||
|
||||
type DefaultTitleProps = {
|
||||
title?: string;
|
||||
className: string;
|
||||
};
|
||||
|
||||
const DefaultTitle: FC<DefaultTitleProps> = ({
|
||||
title = 'Unknown page',
|
||||
className,
|
||||
}) => (
|
||||
<Typography variant="h4" className={className} data-testid="header-title">
|
||||
{title}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
type ContentHeaderProps = {
|
||||
title?: DefaultTitleProps['title'];
|
||||
titleComponent?: ComponentType;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
const ContentHeader: FC<ContentHeaderProps> = ({
|
||||
description,
|
||||
title,
|
||||
titleComponent: TitleComponent = undefined,
|
||||
children,
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const renderedTitle = TitleComponent ? (
|
||||
<TitleComponent />
|
||||
) : (
|
||||
<DefaultTitle title={title} className={classes.title} />
|
||||
);
|
||||
return (
|
||||
<Fragment>
|
||||
<Helmet title={title} />
|
||||
<div className={classes.container}>
|
||||
<div className={classes.leftItemsBox}>
|
||||
{renderedTitle}
|
||||
{description && (
|
||||
<Typography className={classes.description} variant="body2">
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<div className={classes.rightItemsBox}>{children}</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentHeader;
|
||||
+2
-2
@@ -41,7 +41,7 @@ describe('<ComponentContextMenu />', () => {
|
||||
'aria-disabled',
|
||||
'true',
|
||||
);
|
||||
fireEvent.click(rendered.queryByText('Some label'));
|
||||
fireEvent.click(rendered.queryByText('Some label') as Node);
|
||||
expect(onClickFunction).toHaveBeenCalled();
|
||||
// We do not expect the dropdown to disappear after click
|
||||
expect(rendered.queryByText('Some label')).toBeInTheDocument();
|
||||
@@ -84,7 +84,7 @@ describe('<ComponentContextMenu />', () => {
|
||||
expect(onClickFunction).not.toHaveBeenCalled();
|
||||
fireEvent.click(rendered.getByTestId('header-action-menu'));
|
||||
expect(onClickFunction).not.toHaveBeenCalled();
|
||||
fireEvent.click(rendered.queryByText('Secondary label'));
|
||||
fireEvent.click(rendered.queryByText('Secondary label') as Node);
|
||||
expect(onClickFunction).toHaveBeenCalled();
|
||||
// We do not expect the dropdown to disappear after click
|
||||
expect(rendered.queryByText('Some label')).toBeInTheDocument();
|
||||
+21
-5
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import React, { Fragment, ReactElement, FC, ComponentType } from 'react';
|
||||
import {
|
||||
IconButton,
|
||||
List,
|
||||
@@ -22,10 +22,20 @@ import {
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Popover,
|
||||
ListItemTextProps,
|
||||
} from '@material-ui/core';
|
||||
import { default as KebabMenuIcon } from './MenuVertical';
|
||||
|
||||
const ActionItem = ({
|
||||
type ActionItemProps = {
|
||||
label?: ListItemTextProps['primary'];
|
||||
secondaryLabel?: ListItemTextProps['secondary'];
|
||||
icon?: ReactElement;
|
||||
disabled?: boolean;
|
||||
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
WrapperComponent?: ComponentType;
|
||||
};
|
||||
|
||||
const ActionItem: FC<ActionItemProps> = ({
|
||||
label,
|
||||
secondaryLabel,
|
||||
icon,
|
||||
@@ -52,7 +62,11 @@ const ActionItem = ({
|
||||
);
|
||||
};
|
||||
|
||||
const HeaderActionMenu = ({ actionItems }) => {
|
||||
export type HeaderActionMenuProps = {
|
||||
actionItems: ActionItemProps[];
|
||||
};
|
||||
|
||||
const HeaderActionMenu: FC<HeaderActionMenuProps> = ({ actionItems }) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const anchorElRef = React.useRef(null);
|
||||
|
||||
@@ -80,8 +94,10 @@ const HeaderActionMenu = ({ actionItems }) => {
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<List>
|
||||
{actionItems.map(actionItem => {
|
||||
return <ActionItem key={actionItem.label} {...actionItem} />;
|
||||
{actionItems.map((actionItem, i) => {
|
||||
return (
|
||||
<ActionItem key={`header-action-menu-${i}`} {...actionItem} />
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Popover>
|
||||
+8
-1
@@ -30,6 +30,13 @@ describe('<HeaderLabel />', () => {
|
||||
expect(rendered.getByText('<Unknown>')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should say unknown when passing null as value prop', () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(<HeaderLabel label="Label" value={null} />),
|
||||
);
|
||||
expect(rendered.getByText('<Unknown>')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have value', () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(<HeaderLabel label="Label" value="Value" />),
|
||||
@@ -43,7 +50,7 @@ describe('<HeaderLabel />', () => {
|
||||
<HeaderLabel label="Label" value="Value" url="/test" />,
|
||||
),
|
||||
);
|
||||
const anchor = rendered.container.querySelector('a');
|
||||
const anchor = rendered.container.querySelector('a') as HTMLAnchorElement;
|
||||
expect(rendered.getByText('Value')).toBeInTheDocument();
|
||||
expect(anchor.href).toBe('http://localhost/test');
|
||||
});
|
||||
+36
-25
@@ -14,12 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Typography, withStyles } from '@material-ui/core';
|
||||
import React, { FC } from 'react';
|
||||
import { Typography, makeStyles } from '@material-ui/core';
|
||||
import { Link } from '@material-ui/core';
|
||||
import { BackstageTheme } from '../../theme/theme';
|
||||
|
||||
const style = theme => ({
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
root: {
|
||||
textAlign: 'left',
|
||||
margin: theme.spacing(2),
|
||||
@@ -40,27 +40,38 @@ const style = theme => ({
|
||||
fontSize: 14,
|
||||
height: '16px',
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
class HeaderLabel extends Component {
|
||||
static propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
value: PropTypes.node,
|
||||
url: PropTypes.string,
|
||||
};
|
||||
type HeaderLabelContentProps = {
|
||||
value: React.ReactNode;
|
||||
className: string;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { label, value, url, classes } = this.props;
|
||||
const content = (
|
||||
<Typography className={classes.value}>{value || '<Unknown>'}</Typography>
|
||||
);
|
||||
return (
|
||||
<span className={classes.root}>
|
||||
<Typography className={classes.label}>{label}</Typography>
|
||||
{url ? <Link href={url}>{content}</Link> : content}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
const HeaderLabelContent: FC<HeaderLabelContentProps> = ({
|
||||
value,
|
||||
className,
|
||||
}) => <Typography className={className}>{value}</Typography>;
|
||||
|
||||
export default withStyles(style)(HeaderLabel);
|
||||
type HeaderLabelProps = {
|
||||
label: string;
|
||||
value?: HeaderLabelContentProps['value'];
|
||||
url?: string;
|
||||
};
|
||||
|
||||
const HeaderLabel: FC<HeaderLabelProps> = ({ label, value, url }) => {
|
||||
const classes = useStyles();
|
||||
const content = (
|
||||
<HeaderLabelContent
|
||||
className={classes.value}
|
||||
value={value || '<Unknown>'}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<span className={classes.root}>
|
||||
<Typography className={classes.label}>{label}</Typography>
|
||||
{url ? <Link href={url}>{content}</Link> : content}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderLabel;
|
||||
Reference in New Issue
Block a user