diff --git a/docs/src/modules/components/Link.js b/docs/src/modules/components/Link.js index d6cd78770b5427..035693d9211abd 100644 --- a/docs/src/modules/components/Link.js +++ b/docs/src/modules/components/Link.js @@ -15,7 +15,6 @@ function NextWrapper(props) { return ( ({ }); class ListItemLink extends React.Component { - renderLink = itemProps => ; + renderLink = itemProps => ; render() { const { icon, primary } = this.props; @@ -51,7 +51,7 @@ function ListItemLinkShorthand(props) { const { primary, to } = props; return (
  • - +
  • diff --git a/docs/src/pages/lab/breadcrumbs/CollapsedBreadcrumbs.js b/docs/src/pages/lab/breadcrumbs/CollapsedBreadcrumbs.js new file mode 100644 index 00000000000000..ce20cb7e2fb257 --- /dev/null +++ b/docs/src/pages/lab/breadcrumbs/CollapsedBreadcrumbs.js @@ -0,0 +1,54 @@ +/* eslint-disable jsx-a11y/anchor-is-valid */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import Paper from '@material-ui/core/Paper'; +import Breadcrumbs from '@material-ui/lab/Breadcrumbs'; +import Typography from '@material-ui/core/Typography'; +import Link from '@material-ui/core/Link'; + +const styles = theme => ({ + root: { + justifyContent: 'center', + flexWrap: 'wrap', + }, + paper: { + padding: `${theme.spacing.unit}px ${theme.spacing.unit * 2}px`, + }, +}); + +function handleClick(event) { + event.preventDefault(); + alert('You clicked a breadcrumb.'); // eslint-disable-line no-alert +} + +function CollapsedBreadcrumbs(props) { + const { classes } = props; + + return ( + + + + Home + + + Catalog + + + Accessories + + + New Collection + + Belts + + + ); +} + +CollapsedBreadcrumbs.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(CollapsedBreadcrumbs); diff --git a/docs/src/pages/lab/breadcrumbs/CustomSeparator.js b/docs/src/pages/lab/breadcrumbs/CustomSeparator.js new file mode 100644 index 00000000000000..453c9c67524e68 --- /dev/null +++ b/docs/src/pages/lab/breadcrumbs/CustomSeparator.js @@ -0,0 +1,73 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import Paper from '@material-ui/core/Paper'; +import Breadcrumbs from '@material-ui/lab/Breadcrumbs'; +import Typography from '@material-ui/core/Typography'; +import Link from '@material-ui/core/Link'; +import NavigateNextIcon from '@material-ui/icons/NavigateNext'; + +const styles = theme => ({ + root: { + justifyContent: 'center', + flexWrap: 'wrap', + }, + paper: { + padding: `${theme.spacing.unit}px ${theme.spacing.unit * 2}px`, + }, +}); + +function handleClick(event) { + event.preventDefault(); + alert('You clicked a breadcrumb.'); // eslint-disable-line no-alert +} + +function CustomSeparator(props) { + const { classes } = props; + + return ( +
    + + + + Material-UI + + + Lab + + Breadcrumb + + +
    + + + + Material-UI + + + Lab + + Breadcrumb + + +
    + + } arial-label="Breadcrumb"> + + Material-UI + + + Lab + + Breadcrumb + + +
    + ); +} + +CustomSeparator.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(CustomSeparator); diff --git a/docs/src/pages/lab/breadcrumbs/CustomizedBreadcrumbs.js b/docs/src/pages/lab/breadcrumbs/CustomizedBreadcrumbs.js new file mode 100644 index 00000000000000..16982966150805 --- /dev/null +++ b/docs/src/pages/lab/breadcrumbs/CustomizedBreadcrumbs.js @@ -0,0 +1,84 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import { emphasize } from '@material-ui/core/styles/colorManipulator'; +import Paper from '@material-ui/core/Paper'; +import Breadcrumbs from '@material-ui/lab/Breadcrumbs'; +import Chip from '@material-ui/core/Chip'; +import Avatar from '@material-ui/core/Avatar'; +import HomeIcon from '@material-ui/icons/Home'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; + +const styles = theme => ({ + root: { + padding: theme.spacing.unit, + }, + chip: { + backgroundColor: theme.palette.grey[100], + height: 24, + color: theme.palette.grey[800], + fontWeight: theme.typography.fontWeightRegular, + '&:hover, &:focus': { + backgroundColor: theme.palette.grey[300], + }, + '&:active': { + boxShadow: theme.shadows[1], + backgroundColor: emphasize(theme.palette.grey[300], 0.12), + }, + }, + avatar: { + background: 'none', + marginRight: -theme.spacing.unit * 1.5, + }, +}); + +function handleClick(event) { + event.preventDefault(); + alert('You clicked a breadcrumb.'); // eslint-disable-line no-alert +} + +function CustomBreadcrumb(props) { + const { classes, ...rest } = props; + return ; +} + +CustomBreadcrumb.propTypes = { + classes: PropTypes.object.isRequired, +}; + +const StyledBreadcrumb = withStyles(styles)(CustomBreadcrumb); + +function CustomizedBreadcrumbs(props) { + const { classes } = props; + + return ( + + + + + + } + onClick={handleClick} + /> + + } + onClick={handleClick} + onDelete={handleClick} + /> + + + ); +} + +CustomizedBreadcrumbs.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(CustomizedBreadcrumbs); diff --git a/docs/src/pages/lab/breadcrumbs/IconBreadcrumbs.js b/docs/src/pages/lab/breadcrumbs/IconBreadcrumbs.js new file mode 100644 index 00000000000000..d3769370c930e0 --- /dev/null +++ b/docs/src/pages/lab/breadcrumbs/IconBreadcrumbs.js @@ -0,0 +1,57 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import Paper from '@material-ui/core/Paper'; +import Typography from '@material-ui/core/Typography'; +import Breadcrumbs from '@material-ui/lab/Breadcrumbs'; +import Link from '@material-ui/core/Link'; +import HomeIcon from '@material-ui/icons/Home'; +import WhatshotIcon from '@material-ui/icons/Whatshot'; +import GrainIcon from '@material-ui/icons/Grain'; + +const styles = theme => ({ + root: { + padding: `${theme.spacing.unit}px ${theme.spacing.unit * 2}px`, + }, + link: { + display: 'flex', + }, + icon: { + marginRight: theme.spacing.unit / 2, + width: 20, + height: 20, + }, +}); + +function handleClick(event) { + event.preventDefault(); + alert('You clicked a breadcrumb.'); // eslint-disable-line no-alert +} + +function IconBreadcrumbs(props) { + const { classes } = props; + return ( + + + + + Material-UI + + + + Lab + + + + Breadcrumb + + + + ); +} + +IconBreadcrumbs.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(IconBreadcrumbs); diff --git a/docs/src/pages/lab/breadcrumbs/RouterBreadcrumbs.js b/docs/src/pages/lab/breadcrumbs/RouterBreadcrumbs.js new file mode 100644 index 00000000000000..22ef506216a9fa --- /dev/null +++ b/docs/src/pages/lab/breadcrumbs/RouterBreadcrumbs.js @@ -0,0 +1,129 @@ +/* eslint-disable no-nested-ternary */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import List from '@material-ui/core/List'; +import Link from '@material-ui/core/Link'; +import NoSsr from '@material-ui/core/NoSsr'; +import ListItem from '@material-ui/core/ListItem'; +import Collapse from '@material-ui/core/Collapse'; +import ListItemText from '@material-ui/core/ListItemText'; +import Typography from '@material-ui/core/Typography'; +import ExpandLess from '@material-ui/icons/ExpandLess'; +import ExpandMore from '@material-ui/icons/ExpandMore'; +import Breadcrumbs from '@material-ui/lab/Breadcrumbs'; +import MemoryRouter from 'react-router/MemoryRouter'; +import Route from 'react-router/Route'; +import { Link as RouterLink } from 'react-router-dom'; + +const breadcrumbNameMap = { + '/inbox': 'Inbox', + '/inbox/important': 'Important', + '/trash': 'Trash', + '/spam': 'Spam', + '/drafts': 'Drafts', +}; + +function ListItemLink(props) { + const { to, open, ...other } = props; + const primary = breadcrumbNameMap[to]; + + return ( +
  • + + + {open != null ? open ? : : null} + +
  • + ); +} + +ListItemLink.propTypes = { + open: PropTypes.bool, + to: PropTypes.string.isRequired, +}; + +const styles = theme => ({ + root: { + display: 'flex', + flexDirection: 'column', + width: 360, + }, + lists: { + backgroundColor: theme.palette.background.paper, + marginTop: theme.spacing.unit, + }, + nested: { + paddingLeft: theme.spacing.unit * 4, + }, +}); + +class RouterBreadcrumbs extends React.Component { + state = { + open: true, + }; + + handleClick = () => { + this.setState(state => ({ open: !state.open })); + }; + + render() { + const { classes } = this.props; + + // Use NoSsr to avoid SEO issues with the documentation website. + return ( + + +
    + + {({ location }) => { + const pathnames = location.pathname.split('/').filter(x => x); + + return ( + + + Home + + {pathnames.map((value, index) => { + const last = index === pathnames.length - 1; + const to = `/${pathnames.slice(0, index + 1).join('/')}`; + + return last ? ( + + {breadcrumbNameMap[to]} + + ) : ( + + {breadcrumbNameMap[to]} + + ); + })} + + ); + }} + +
    + + + + + + + + + + +
    +
    +
    +
    + ); + } +} + +RouterBreadcrumbs.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(RouterBreadcrumbs); diff --git a/docs/src/pages/lab/breadcrumbs/SimpleBreadcrumbs.js b/docs/src/pages/lab/breadcrumbs/SimpleBreadcrumbs.js new file mode 100644 index 00000000000000..4e0e426d3b87af --- /dev/null +++ b/docs/src/pages/lab/breadcrumbs/SimpleBreadcrumbs.js @@ -0,0 +1,66 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import Paper from '@material-ui/core/Paper'; +import Typography from '@material-ui/core/Typography'; +import Breadcrumbs from '@material-ui/lab/Breadcrumbs'; +import Link from '@material-ui/core/Link'; + +const styles = theme => ({ + root: { + justifyContent: 'center', + flexWrap: 'wrap', + }, + paper: { + padding: `${theme.spacing.unit}px ${theme.spacing.unit * 2}px`, + }, +}); + +function handleClick(event) { + event.preventDefault(); + alert('You clicked a breadcrumb.'); // eslint-disable-line no-alert +} + +function SimpleBreadcrumbs(props) { + const { classes } = props; + return ( +
    + + + + Material-UI + + + Lab + + Breadcrumb + + +
    + + + + Material-UI + + + Lab + + + Breadcrumb + + + +
    + ); +} + +SimpleBreadcrumbs.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(SimpleBreadcrumbs); diff --git a/docs/src/pages/lab/breadcrumbs/breadcrumbs.md b/docs/src/pages/lab/breadcrumbs/breadcrumbs.md new file mode 100644 index 00000000000000..cc30e0afd3e751 --- /dev/null +++ b/docs/src/pages/lab/breadcrumbs/breadcrumbs.md @@ -0,0 +1,48 @@ +--- +title: Breadcrumbs React component +components: Breadcrumbs +--- + +# Breadcrumbs + +

    Breadcrumbs allow users to make selections from a range of values.

    + +## Simple breadcrumbs + +{{"demo": "pages/lab/breadcrumbs/SimpleBreadcrumbs.js"}} + +## Custom separator + +In the following examples, we are using two string separators, and an SVG icon. + +{{"demo": "pages/lab/breadcrumbs/CustomSeparator.js"}} + +## Breadcrumbs with icons + +{{"demo": "pages/lab/breadcrumbs/IconBreadcrumbs.js"}} + +## Collapsed breadcrumbs + +{{"demo": "pages/lab/breadcrumbs/CollapsedBreadcrumbs.js"}} + +## Customized breadcrumbs + +If you have been reading the [overrides documentation page](/customization/overrides/) +but you are not confident jumping in, +here is one example of how you can change the breadcrumb link design. + +{{"demo": "pages/lab/breadcrumbs/CustomizedBreadcrumbs.js"}} + +## Accessibility + +Be sure to add a `aria-label` description on the `Breadcrumbs` component. + +The accessibility of this component relies on: + +- The set of links is structured using an ordered list (`
      ` element). +- To prevent screen reader announcement of the visual separators between links, they are hidden with `aria-hidden`. +- A nav element labeled with `aria-label` identifies the structure as a breadcrumb trail and makes it a navigation landmark so that it is easy to locate. + +## Integration with react-router + +{{"demo": "pages/lab/breadcrumbs/RouterBreadcrumbs.js"}} diff --git a/packages/material-ui-lab/src/Breadcrumbs/BreadcrumbCollapsed.js b/packages/material-ui-lab/src/Breadcrumbs/BreadcrumbCollapsed.js new file mode 100644 index 00000000000000..ff11c31dbd3b5a --- /dev/null +++ b/packages/material-ui-lab/src/Breadcrumbs/BreadcrumbCollapsed.js @@ -0,0 +1,49 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles } from '@material-ui/core/styles'; +import { emphasize } from '@material-ui/core/styles/colorManipulator'; +import MoreHorizIcon from '@material-ui/icons/MoreHoriz'; + +const styles = theme => ({ + root: { + display: 'flex', + }, + icon: { + width: 24, + height: 16, + backgroundColor: theme.palette.grey[100], + color: theme.palette.grey[700], + borderRadius: 2, + marginLeft: theme.spacing.unit / 2, + marginRight: theme.spacing.unit / 2, + cursor: 'pointer', + '&:hover, &:focus': { + backgroundColor: theme.palette.grey[200], + }, + '&:active': { + boxShadow: theme.shadows[0], + backgroundColor: emphasize(theme.palette.grey[200], 0.12), + }, + }, +}); + +/** + * @ignore - internal component. + */ +function BreadcrumbCollapsed(props) { + const { classes, ...other } = props; + return ( +
    1. + +
    2. + ); +} + +BreadcrumbCollapsed.propTypes = { + /** + * @ignore + */ + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles, { name: 'MuiPrivateBreadcrumbCollapsed' })(BreadcrumbCollapsed); diff --git a/packages/material-ui-lab/src/Breadcrumbs/BreadcrumbCollapsed.test.js b/packages/material-ui-lab/src/Breadcrumbs/BreadcrumbCollapsed.test.js new file mode 100644 index 00000000000000..0317f9038c7dc8 --- /dev/null +++ b/packages/material-ui-lab/src/Breadcrumbs/BreadcrumbCollapsed.test.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { assert } from 'chai'; +import { createShallow, getClasses } from '@material-ui/core/test-utils'; +import BreadcrumbCollapsed from './BreadcrumbCollapsed'; +import MoreHorizIcon from '@material-ui/icons/MoreHoriz'; + +describe('', () => { + let shallow; + let classes; + + before(() => { + shallow = createShallow({ dive: true }); + classes = getClasses(); + }); + + it('should render an ', () => { + const wrapper = shallow(); + + assert.strictEqual(wrapper.find(MoreHorizIcon).length, 1); + }); + + it('should render the root class', () => { + const wrapper = shallow(); + + assert.strictEqual(wrapper.hasClass(classes.root), true); + }); +}); diff --git a/packages/material-ui-lab/src/Breadcrumbs/BreadcrumbSeparator.js b/packages/material-ui-lab/src/Breadcrumbs/BreadcrumbSeparator.js new file mode 100644 index 00000000000000..661e58a35d0e40 --- /dev/null +++ b/packages/material-ui-lab/src/Breadcrumbs/BreadcrumbSeparator.js @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { withStyles } from '@material-ui/core/styles'; + +const styles = { + root: { + display: 'flex', + userSelect: 'none', + marginLeft: 8, + marginRight: 8, + }, +}; + +/** + * @ignore - internal component. + */ +function BreadcrumbSeparator(props) { + const { children, classes, className, ...other } = props; + + return ( + + ); +} + +BreadcrumbSeparator.propTypes = { + children: PropTypes.node.isRequired, + classes: PropTypes.object.isRequired, + className: PropTypes.string, +}; + +export default withStyles(styles, { name: 'MuiPrivateBreadcrumbSeparator' })(BreadcrumbSeparator); diff --git a/packages/material-ui-lab/src/Breadcrumbs/BreadcrumbSeparator.test.js b/packages/material-ui-lab/src/Breadcrumbs/BreadcrumbSeparator.test.js new file mode 100644 index 00000000000000..a1797e0fc2bc22 --- /dev/null +++ b/packages/material-ui-lab/src/Breadcrumbs/BreadcrumbSeparator.test.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { assert } from 'chai'; +import { createShallow, getClasses } from '@material-ui/core/test-utils'; +import BreadcrumbSeparator from './BreadcrumbSeparator'; + +describe('', () => { + let shallow; + let classes; + + before(() => { + shallow = createShallow({ dive: true }); + classes = getClasses(/); + }); + + it('should render a
    3. element', () => { + const wrapper = shallow(/); + + assert.strictEqual(wrapper.type(), 'li'); + }); + + it('should render the root class', () => { + const wrapper = shallow(/); + + assert.strictEqual(wrapper.hasClass(classes.root), true); + }); +}); diff --git a/packages/material-ui-lab/src/Breadcrumbs/Breadcrumbs.d.ts b/packages/material-ui-lab/src/Breadcrumbs/Breadcrumbs.d.ts new file mode 100644 index 00000000000000..8a493ad95bc0d0 --- /dev/null +++ b/packages/material-ui-lab/src/Breadcrumbs/Breadcrumbs.d.ts @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { StandardProps } from '@material-ui/core'; + +export interface BreadcrumbsProps + extends StandardProps, BreadcrumbsClassKey> { + itemsAfterCollapse?: boolean; + itemsBeforeCollapse?: boolean; + maxItems?: number; + separator?: React.ReactNode; +} + +export type BreadcrumbsClassKey = 'root' | 'ol' | 'separator'; + +declare const Breadcrumbs: React.ComponentType; + +export default Breadcrumbs; diff --git a/packages/material-ui-lab/src/Breadcrumbs/Breadcrumbs.js b/packages/material-ui-lab/src/Breadcrumbs/Breadcrumbs.js new file mode 100644 index 00000000000000..800435403baac3 --- /dev/null +++ b/packages/material-ui-lab/src/Breadcrumbs/Breadcrumbs.js @@ -0,0 +1,164 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { withStyles } from '@material-ui/core/styles'; +import { componentPropType } from '@material-ui/utils'; +import BreadcrumbCollapsed from './BreadcrumbCollapsed'; +import BreadcrumbSeparator from './BreadcrumbSeparator'; +import Typography from '@material-ui/core/Typography'; + +const styles = { + /* Styles applied to the root element. */ + root: {}, + /* Styles applied to the ol element. */ + ol: { + display: 'flex', + flexWrap: 'wrap', + alignItems: 'center', + padding: 0, // Reset + margin: 0, // Reset + '& li': { + listStyle: 'none', + }, + }, + /* Styles applied to the li element. */ + li: {}, + /* Styles applied to the separator element. */ + separator: {}, +}; + +class Breadcrumbs extends React.Component { + state = { + expanded: false, + }; + + handleClickExpand = () => { + this.setState({ expanded: true }); + }; + + insertSeparators(items) { + return items.reduce((acc, current, index) => { + if (index < items.length - 1) { + acc = acc.concat( + current, + + {this.props.separator} + , + ); + } else { + acc.push(current); + } + + return acc; + }, []); + } + + renderItemsBeforeAndAfter(allItems) { + const { itemsBeforeCollapse, itemsAfterCollapse } = this.props; + + // This defends against someone passing weird data, to ensure that if all + // items would be shown anyway, we just show all items without the EllipsisItem + if (itemsBeforeCollapse + itemsAfterCollapse >= allItems.length) { + return allItems; + } + + return [ + ...allItems.slice(0, itemsBeforeCollapse), + , + ...allItems.slice(allItems.length - itemsAfterCollapse, allItems.length), + ]; + } + + render() { + const { + children, + classes, + className: classNameProp, + component: Component, + itemsAfterCollapse, + itemsBeforeCollapse, + maxItems, + separator, + ...other + } = this.props; + + const allItems = React.Children.toArray(children) + .filter(child => React.isValidElement(child)) + .map((child, index) => ( +
    4. + {child} +
    5. + )); + + return ( + +
        + {this.insertSeparators( + this.state.expanded || (maxItems && allItems.length <= maxItems) + ? allItems + : this.renderItemsBeforeAndAfter(allItems), + )} +
      +
      + ); + } +} + +Breadcrumbs.propTypes = { + /** + * The breadcrumb children. + */ + children: PropTypes.node.isRequired, + /** + * Override or extend the styles applied to the component. + * See [CSS API](#css-api) below for more details. + */ + classes: PropTypes.object.isRequired, + /** + * @ignore + */ + className: PropTypes.string, + /** + * The component used for the root node. + * Either a string to use a DOM element or a component. + * By default, it maps the variant to a good default headline component. + */ + component: componentPropType, + /** + * If max items is exceeded, the number of items to show after the ellipsis. + */ + itemsAfterCollapse: PropTypes.number, + /** + * If max items is exceeded, the number of items to show before the ellipsis. + */ + itemsBeforeCollapse: PropTypes.number, + /** + * Specifies the maximum number of breadcrumbs to display. When there are more + * than the maximum number, only the first and last will be shown, with an + * ellipsis in between. + */ + maxItems: PropTypes.number, + /** + * Custom separator node. + */ + separator: PropTypes.node, +}; + +Breadcrumbs.defaultProps = { + component: 'nav', + itemsAfterCollapse: 1, + itemsBeforeCollapse: 1, + maxItems: 8, + separator: '/', +}; + +export default withStyles(styles, { name: 'MuiBreadcrumbs' })(Breadcrumbs); diff --git a/packages/material-ui-lab/src/Breadcrumbs/Breadcrumbs.test.js b/packages/material-ui-lab/src/Breadcrumbs/Breadcrumbs.test.js new file mode 100644 index 00000000000000..c3615db2611b63 --- /dev/null +++ b/packages/material-ui-lab/src/Breadcrumbs/Breadcrumbs.test.js @@ -0,0 +1,48 @@ +import React from 'react'; +import { assert } from 'chai'; +import { createShallow, getClasses } from '@material-ui/core/test-utils'; +import Breadcrumbs from './Breadcrumbs'; +import Typography from '@material-ui/core/Typography'; + +describe('', () => { + let shallow; + let classes; + + before(() => { + shallow = createShallow({ dive: true }); + classes = getClasses( + + Hello World + , + ); + }); + + it('should render a