Rewrite layout/Header to use TypeScript and hooks
* use makeStyles hook generator * write fragments as components * use types instead of PropTypes
This commit is contained in:
@@ -1,157 +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 PropTypes from 'prop-types';
|
||||
import { Typography, withStyles, Tooltip } from '@material-ui/core';
|
||||
import { Theme } from '../Page/Page';
|
||||
// import { Link } from 'shared/components';
|
||||
import Waves from './Waves';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
class Header extends Component {
|
||||
static propTypes = {
|
||||
type: PropTypes.string,
|
||||
typeLink: PropTypes.string,
|
||||
title: PropTypes.node.isRequired,
|
||||
tooltip: PropTypes.string,
|
||||
subtitle: PropTypes.node,
|
||||
pageTitleOverride: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
component: PropTypes.object,
|
||||
};
|
||||
|
||||
typeFragment() {
|
||||
const { type, typeLink, classes } = this.props;
|
||||
if (!type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return typeLink ? (
|
||||
// <Link to={typeLink}>
|
||||
<Typography className={classes.type}>{type}</Typography>
|
||||
) : (
|
||||
// </Link>
|
||||
<Typography className={classes.type}>{type}</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
titleFragment() {
|
||||
const { title, pageTitleOverride, classes, tooltip } = this.props;
|
||||
const FinalTitle = (
|
||||
<Typography className={classes.title} variant="h4">
|
||||
{title || pageTitleOverride}
|
||||
</Typography>
|
||||
);
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip title={tooltip} placement="top-start">
|
||||
{FinalTitle}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return FinalTitle;
|
||||
}
|
||||
|
||||
subtitleFragment() {
|
||||
const { subtitle, classes } = this.props;
|
||||
if (!subtitle) {
|
||||
return null;
|
||||
} else if (typeof subtitle !== 'string') {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography className={classes.subtitle} variant="subtitle1">
|
||||
{subtitle}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { title, pageTitleOverride, children, style, classes } = this.props;
|
||||
const pageTitle = pageTitleOverride || title;
|
||||
return (
|
||||
<Fragment>
|
||||
<Helmet
|
||||
titleTemplate={`${pageTitle} | %s | Backstage`}
|
||||
defaultTitle={`${pageTitle} | Backstage`}
|
||||
/>
|
||||
<Theme.Consumer>
|
||||
{theme => (
|
||||
<header style={style} className={classes.header}>
|
||||
<Waves theme={theme} />
|
||||
<div className={classes.leftItemsBox}>
|
||||
{this.typeFragment()}
|
||||
{this.titleFragment()}
|
||||
{this.subtitleFragment()}
|
||||
</div>
|
||||
<div className={classes.rightItemsBox}>{children}</div>
|
||||
</header>
|
||||
)}
|
||||
</Theme.Consumer>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = theme => ({
|
||||
header: {
|
||||
gridArea: 'pageHeader',
|
||||
padding: theme.spacing(3),
|
||||
minHeight: 118,
|
||||
width: '100%',
|
||||
boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)',
|
||||
position: 'relative',
|
||||
zIndex: 100,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
},
|
||||
leftItemsBox: {
|
||||
flex: '1 1 auto',
|
||||
},
|
||||
rightItemsBox: {
|
||||
flex: '0 1 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
marginRight: theme.spacing(6),
|
||||
},
|
||||
title: {
|
||||
color: theme.palette.bursts.fontColor,
|
||||
lineHeight: '1.0em',
|
||||
wordBreak: 'break-all',
|
||||
fontSize: 'calc(24px + 6 * ((100vw - 320px) / 680))',
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
subtitle: {
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
lineHeight: '1.0em',
|
||||
},
|
||||
type: {
|
||||
textTransform: 'uppercase',
|
||||
fontSize: 9,
|
||||
opacity: 0.8,
|
||||
marginBottom: 10,
|
||||
color: theme.palette.bursts.fontColor,
|
||||
},
|
||||
});
|
||||
|
||||
export default withStyles(styles)(Header);
|
||||
+9
-3
@@ -36,18 +36,24 @@ describe('<Header/>', () => {
|
||||
});
|
||||
|
||||
it('should override document title', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<Header title="Title1" pageTitleOverride="Title2" />));
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(<Header title="Title1" pageTitleOverride="Title2" />),
|
||||
);
|
||||
rendered.getByText('Title1');
|
||||
rendered.getByText('defaultTitle: Title2 | Backstage');
|
||||
});
|
||||
|
||||
it('should have subtitle', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<Header title="Title" subtitle="Subtitle" />));
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(<Header title="Title" subtitle="Subtitle" />),
|
||||
);
|
||||
rendered.getByText('Subtitle');
|
||||
});
|
||||
|
||||
it('should have type rendered', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<Header title="Title" type="tool" />));
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(<Header title="Title" type="tool" />),
|
||||
);
|
||||
rendered.getByText('tool');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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, { Fragment, ReactNode, CSSProperties, FC } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { Typography, Tooltip, makeStyles } from '@material-ui/core';
|
||||
import { Theme } from '../Page/Page';
|
||||
// import { Link } from 'shared/components';
|
||||
import { BackstageTheme } from '../../theme/theme';
|
||||
import Waves from './Waves';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
header: {
|
||||
gridArea: 'pageHeader',
|
||||
padding: theme.spacing(3),
|
||||
minHeight: 118,
|
||||
width: '100%',
|
||||
boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)',
|
||||
position: 'relative',
|
||||
zIndex: 100,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
},
|
||||
leftItemsBox: {
|
||||
flex: '1 1 auto',
|
||||
},
|
||||
rightItemsBox: {
|
||||
flex: '0 1 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
marginRight: theme.spacing(6),
|
||||
},
|
||||
title: {
|
||||
color: theme.palette.bursts.fontColor,
|
||||
lineHeight: '1.0em',
|
||||
wordBreak: 'break-all',
|
||||
fontSize: 'calc(24px + 6 * ((100vw - 320px) / 680))',
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
subtitle: {
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
lineHeight: '1.0em',
|
||||
},
|
||||
type: {
|
||||
textTransform: 'uppercase',
|
||||
fontSize: 9,
|
||||
opacity: 0.8,
|
||||
marginBottom: 10,
|
||||
color: theme.palette.bursts.fontColor,
|
||||
},
|
||||
}));
|
||||
|
||||
type HeaderStyles = ReturnType<typeof useStyles>;
|
||||
|
||||
type Props = {
|
||||
component?: ReactNode;
|
||||
pageTitleOverride?: string;
|
||||
style?: CSSProperties;
|
||||
subtitle?: ReactNode;
|
||||
title: ReactNode;
|
||||
tooltip?: string;
|
||||
type?: string;
|
||||
typeLink?: string;
|
||||
};
|
||||
|
||||
type TypeFragmentProps = {
|
||||
classes: HeaderStyles;
|
||||
type?: Props['title'];
|
||||
typeLink?: Props['typeLink'];
|
||||
};
|
||||
|
||||
type TitleFragmentProps = {
|
||||
classes: HeaderStyles;
|
||||
pageTitle: string | ReactNode;
|
||||
tooltip?: Props['tooltip'];
|
||||
};
|
||||
|
||||
type SubtitleFragmentProps = {
|
||||
classes: HeaderStyles;
|
||||
subtitle?: Props['subtitle'];
|
||||
};
|
||||
|
||||
const TypeFragment: FC<TypeFragmentProps> = ({ type, typeLink, classes }) => {
|
||||
if (!type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!typeLink) {
|
||||
return (
|
||||
// </Link>
|
||||
<Typography className={classes.type}>{type}</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// <Link to={typeLink}>
|
||||
<Typography className={classes.type}>{type}</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
const TitleFragment: FC<TitleFragmentProps> = ({
|
||||
pageTitle,
|
||||
classes,
|
||||
tooltip,
|
||||
}) => {
|
||||
const FinalTitle = (
|
||||
<Typography className={classes.title} variant="h4">
|
||||
{pageTitle}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return FinalTitle;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title={tooltip} placement="top-start">
|
||||
{FinalTitle}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const SubtitleFragment: FC<SubtitleFragmentProps> = ({ classes, subtitle }) => {
|
||||
if (!subtitle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof subtitle !== 'string') {
|
||||
return <>{subtitle}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography className={classes.subtitle} variant="subtitle1">
|
||||
{subtitle}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
export const Header: FC<Props> = ({
|
||||
children,
|
||||
pageTitleOverride,
|
||||
style,
|
||||
subtitle,
|
||||
title,
|
||||
tooltip,
|
||||
type,
|
||||
typeLink,
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const documentTitle = pageTitleOverride || title;
|
||||
const pageTitle = title || pageTitleOverride;
|
||||
const titleTemplate = `${documentTitle} | %s | Backstage`;
|
||||
const defaultTitle = `${documentTitle} | Backstage`;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Helmet titleTemplate={titleTemplate} defaultTitle={defaultTitle} />
|
||||
<Theme.Consumer>
|
||||
{theme => (
|
||||
<header style={style} className={classes.header}>
|
||||
<Waves theme={theme} />
|
||||
<div className={classes.leftItemsBox}>
|
||||
<TypeFragment classes={classes} type={type} typeLink={typeLink} />
|
||||
<TitleFragment
|
||||
classes={classes}
|
||||
pageTitle={pageTitle}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
<SubtitleFragment classes={classes} subtitle={subtitle} />
|
||||
</div>
|
||||
<div className={classes.rightItemsBox}>{children}</div>
|
||||
</header>
|
||||
)}
|
||||
</Theme.Consumer>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
Vendored
+7
@@ -39,5 +39,12 @@ export type BackstageTheme = Theme & {
|
||||
linkHover: string;
|
||||
link: string;
|
||||
gold: string;
|
||||
bursts: {
|
||||
fontColor: string;
|
||||
slackChannelText: string;
|
||||
backgroundColor: {
|
||||
default: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user