react-cm-ui
Version:
React UI for Healthy Church
351 lines (324 loc) • 12.6 kB
JSX
import React, {
useState,
useEffect,
useRef,
} from 'react';
import PropTypes from 'prop-types';
import {
get,
isNil,
} from 'lodash';
import Classnames from 'classnames';
import Typography from '../../dataDisplay/typography';
import Icon from '../../dataDisplay/icon';
import ToolTip from '../../dataDisplay/tooltip';
import makeStyles from '../../styles/makeStyles';
import { BEM_NAVIGATION_BREADCRUMBS } from '../../global/constants';
import {
routesToArray,
getPathNameBreadcrumbs,
} from './breadcrumbsUtils';
const propTypes = {
/**
* Separation string between breadcrumbs
*/
dividerString: PropTypes.string,
/**
* Instance of react-router core,
* breadcrumbs will be generated automatically using the routes object structure,
* location and push function.
*/
router: PropTypes.shape({}),
/**
* Manually defines the crumbs
*/
staticCrumbs: PropTypes.arrayOf(
PropTypes.shape({
onBreadcrumbClick: PropTypes.func,
originalPath: PropTypes.string,
title: PropTypes.string,
to: PropTypes.string,
}),
),
/**
* You can pass an object to apply custom titles
* when using a router instance to build the breadcrumbs automatically.
*/
routesNameMapper: PropTypes.arrayOf(
PropTypes.shape({
route: PropTypes.string,
title: PropTypes.string,
}),
),
/**
* When true, it'll show only the current location breadcrumb and the previous path breadcrumb.
* When false, it'll show the whole navigation breadcrumbs structure for the current location.
*/
showOnlyPreviousRoute: PropTypes.bool,
/**
* Max breadcrumb title length
*/
titlesMaxLength: PropTypes.number,
};
const defaultProps = {
dividerString: '/',
router: undefined,
routesNameMapper: [],
showOnlyPreviousRoute: false,
staticCrumbs: [],
titlesMaxLength: 20,
};
const useStyles = makeStyles((theme) => {
const borderRadiusMain = get(theme, 'shape.borderRadius');
const colorGrey500 = get(theme, 'palette.grey[500]');
const colorHighlight = get(theme, 'palette.cyan[500]');
const fontWeightBold = get(theme, 'typography.fontWeightBold');
const fontWeightMedium = get(theme, 'typography.fontWeightMedium');
const textColorPrimary = get(theme, 'palette.text.primary');
const textColorSecondary = get(theme, 'palette.text.secondary');
const transitionDurationShortest = get(theme, 'transitions.duration.shortest');
return {
root: {
overflow: 'hidden',
width: '100%',
},
tooltip: {
backgroundColor: colorGrey500,
borderRadius: borderRadiusMain,
fontSize: 12,
},
breadcrumbsList: {
overflow: 'hidden',
padding: 0,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
margin: '0 0 -1px 0',
},
breadcrumb: {
color: textColorSecondary,
cursor: 'pointer',
display: 'inline',
padding: ({ showOnlyPreviousRoute }) => (showOnlyPreviousRoute ? '0 5px 0 0' : '0 5px 0 0'),
transition: `color ${transitionDurationShortest}ms`,
'& .navigation_breadcrumbs--breadcrumb_title > p': {
fontWeight: fontWeightMedium,
fontSize: 12,
color: textColorSecondary,
},
'& .icon-use-path': {
transition: `fill ${transitionDurationShortest}ms !important`,
},
'&:hover': {
'& .icon-use-path': {
fill: colorHighlight,
},
'& .navigation_breadcrumbs--breadcrumb_title > p': {
color: colorHighlight,
},
},
},
breadcrumbLast: {
'& .navigation_breadcrumbs--breadcrumb_title > p': {
fontWeight: fontWeightBold,
fontSize: 16,
color: textColorPrimary,
},
'&:hover': {
'& .navigation_breadcrumbs--breadcrumb_title > p': {
color: textColorPrimary,
},
},
},
breadCrumbSeparator: {
display: 'inline',
padding: '0 5px 0 0',
'& .icon': {
marginRight: 5,
},
},
breadCrumbSeparatorTypography: {
display: 'inline',
},
breadcrumbTitle: {
display: 'inline',
transition: `color ${transitionDurationShortest}ms`,
},
breadcrumbTitleTypography: {
display: 'inline',
},
};
});
const ICON_TYPE_FIRST_BREADCRUMB = 'chevron-left';
/**
* Indicates the current location inside the application and the previous routes to navigate back.
* The breadcrumbs will be automatically generated using the passed react-router core instance
* (Each route must have a 'title' prop set).
*/
function Breadcrumbs(props) {
const {
dividerString,
router,
routesNameMapper,
showOnlyPreviousRoute,
staticCrumbs,
titlesMaxLength,
} = props;
const classes = useStyles(props);
const [existentRoutes, setExistentRoutes] = useState([]);
const [pathBreadcrumbs, setPathBreadcrumbs] = useState([]);
const pathName = get(router, 'location.pathname');
const pathParams = get(router, 'params');
const prevPathName = useRef();
const prevRoutesNameMapper = useRef();
const routerPushFunction = get(router, 'push');
useEffect(() => {
if (staticCrumbs.length === 0 && router) {
const routerRoutes = get(router, 'routes', []);
const currentExistentRoutes = routesToArray(routerRoutes);
setExistentRoutes(currentExistentRoutes);
const currentBreadCrumbs = getPathNameBreadcrumbs(
pathName,
pathParams,
routerPushFunction,
currentExistentRoutes,
routesNameMapper,
);
setPathBreadcrumbs(currentBreadCrumbs);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // didMount
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
const shouldUpdatePathBreadcrumbs = staticCrumbs.length === 0 && router &&
prevPathName.current && prevPathName.current !== pathName;
if (shouldUpdatePathBreadcrumbs) {
const currentBreadCrumbs = getPathNameBreadcrumbs(
pathName,
pathParams,
routerPushFunction,
existentRoutes,
routesNameMapper,
);
setPathBreadcrumbs(currentBreadCrumbs);
}
prevPathName.current = pathName;
prevRoutesNameMapper.current = routesNameMapper;
}); // didUpdate
const breadcrumbsListClasses = Classnames(
`${BEM_NAVIGATION_BREADCRUMBS}--list`,
classes.breadcrumbsList,
);
const breadcrumbSeparatorTypographyClasses = Classnames(
classes.breadCrumbSeparatorTypography,
`${BEM_NAVIGATION_BREADCRUMBS}--breadcrumb_separator_typography`,
'breadcrumbSeparator',
);
const breadcrumbTitleClasses = Classnames(
classes.breadcrumbTitle,
`${BEM_NAVIGATION_BREADCRUMBS}--breadcrumb_title`,
);
const breadcrumbTitleTypographyClasses = Classnames(
classes.breadcrumbTitleTypography,
`${BEM_NAVIGATION_BREADCRUMBS}--breadcrumb_title_typography`,
);
let finalBreadcrumbs = staticCrumbs;
const shouldUseDynamicBreadcrumbs = staticCrumbs.length === 0 && !isNil(router);
if (shouldUseDynamicBreadcrumbs) {
finalBreadcrumbs = pathBreadcrumbs;
}
return (
<div className={classes.root}>
<ul className={breadcrumbsListClasses}>
{finalBreadcrumbs
.filter((breadcrumb, index, originalArray) => {
const shouldRenderBreadcrumb = !showOnlyPreviousRoute || (
showOnlyPreviousRoute && (
index === originalArray.length - 1 || // last route
index === originalArray.length - 2 // previous route from the current one
)
);
return shouldRenderBreadcrumb;
})
.map((breadcrumb, index, arr) => {
const {
onBreadcrumbClick,
to,
title,
} = breadcrumb;
const shouldTitleBeTruncated = title.length > titlesMaxLength;
const parsedTitle = shouldTitleBeTruncated ?
`${title.substring(0, titlesMaxLength)}...` :
title;
const isFirst = index === 0;
const isLast = index === arr.length - 1;
const breadcrumbClasses = Classnames(
classes.breadcrumb,
`${BEM_NAVIGATION_BREADCRUMBS}--breadcrumb`,
{
[classes.breadcrumbLast]: isLast,
[`${BEM_NAVIGATION_BREADCRUMBS}--breadcrumb_is_last`]: isLast,
},
);
const breadcrumbSeparatorClasses = Classnames(
classes.breadCrumbSeparator,
`${BEM_NAVIGATION_BREADCRUMBS}--breadcrumb_separator`,
{
[`${BEM_NAVIGATION_BREADCRUMBS}--breadcrumb-divider_first`]: isFirst,
},
);
let breadcrumbTo = (
<Typography
variant="body1"
className={breadcrumbTitleTypographyClasses}
>
{parsedTitle}
</Typography>
);
if (shouldTitleBeTruncated) {
breadcrumbTo = (
<ToolTip
title={title}
classes={{
tooltip: classes.tooltip,
}}
>
{breadcrumbTo}
</ToolTip>
);
}
return (
<li
className={breadcrumbClasses}
key={`${BEM_NAVIGATION_BREADCRUMBS}--breadcrumb_key_${to}`}
onClick={onBreadcrumbClick}
role="presentation"
>
<div className={breadcrumbSeparatorClasses}>
{isFirst ? (
<Icon
type={ICON_TYPE_FIRST_BREADCRUMB}
size="xsmall"
color="static"
/>
) : (
<Typography
variant="h4"
className={breadcrumbSeparatorTypographyClasses}
>
{dividerString}
</Typography>
)}
</div>
<div className={breadcrumbTitleClasses}>
{breadcrumbTo}
</div>
</li>
);
})}
</ul>
</div>
);
}
Breadcrumbs.defaultProps = defaultProps;
Breadcrumbs.propTypes = propTypes;
export default Breadcrumbs;