Repository: uber-web/baseui
Pull request #563 (2018-11-29 20:26:32)

feat: introduce Breadcrumbs component
#### Minor: New Feature

This PR introduces the new `Breadcrumbs` component. To increase flexibility this component only applies styling and chevron separators between its children.

<img width="394" alt="screen shot 2018-11-29 at 12 26 15 pm" src="https://user-images.githubusercontent.com/4030377/49249777-fe99b200-f3d1-11e8-92c2-b9fa5d8e622a.png">

#### Tests Added + Pass

Yes

#### License

MIT


nadiia: The font size seems to be different for active and non-active elements
austin94: > The font size seems to be different for active and non-active elements

I've bumped the typography to now match the `StyledLink` size. Should be good now.
chasestarr: Deploy preview for *baseui* ready!

Built with commit d90946baacd5a911861036e2c0c494e7ee835f9d

https://deploy-preview-563--baseui.netlify.com
nadiia: Just to reiterate here, we should consider looking into how Breadcrumbs component can handle the look and feel of its items. Other than that this PR looks ok.

Patch:
diff --git a/rfcs/breadcrumbs-component.md b/rfcs/breadcrumbs-component.md
new file mode 100644
index 000000000..97b6b3d26
--- /dev/null
+++ b/rfcs/breadcrumbs-component.md
@@ -0,0 +1,42 @@
+# Breadcrumbs Component
+
+The `Breadcrumbs` component is used to display links to the parent pages of the current page in hierarchical order.
+
+## Usage
+
+### Basic usage
+
+```javascript
+import * as React from 'react';
+import {Breadcrumbs} from 'baseui/breadcrumbs';
+import {Link} from 'baseui/link';
+
+export default () => (
+  <Breadcrumbs>
+    <Link href="#">Parent Page</Link>
+    <Link href="#">Sub-Parent Page</Link>
+    <span>Current Page</span>
+  </Breadcrumbs>
+);
+```
+
+## Exports
+
+* `Breadcrumbs`
+* `StyledRoot`
+* `StyledSeparator`
+* `StyledIcon`
+
+## `Breadcrumbs` API
+
+* `overrides: {}` - Optional
+  * `Root?: React.ComponentType | {props: {}, style: {}, component: React.ComponentType}`
+    Component to use for Root container styling.
+  * `Separator?: React.ComponentType | {props: {}, style: {}, component: React.ComponentType}`
+    Component to use for separating element.
+  * `Icon?: React.ComponentType | {props: {}, style: {}, component: React.ComponentType}`
+    Component to use for the separator icon.
+
+## Accessibility
+
+This component is built to the [WAI-ARIA 1.1 Breadcrumb Spec](https://www.w3.org/TR/wai-aria-practices-1.1/#breadcrumb).
diff --git a/screener.config.js b/screener.config.js
index 70d547a27..e33ddf09e 100644
--- a/screener.config.js
+++ b/screener.config.js
@@ -10,6 +10,7 @@ LICENSE file in the root directory of this source tree.
 
 const Accordion = require('./src/accordion/examples-list');
 const Button = require('./src/button/examples-list');
+const Breadcrumbs = require('./src/breadcrumbs/examples-list');
 const Card = require('./src/card/examples-list');
 const Checkbox = require('./src/checkbox/examples-list');
 const Input = require('./src/input/examples-list');
@@ -34,6 +35,7 @@ module.exports = {
     new RegExp(`${Accordion.ACCORDION_EXAMPLE}$`),
     new RegExp(Button.BUTTON_COMPACT_WITH_ENHANCERS),
     new RegExp(Button.BUTTON_WITH_ENHANCERS),
+    new RegExp(Breadcrumbs.DEFAULT),
     new RegExp(Card.TEXT_IMAGE_LINK),
     new RegExp(Card.TEXT_ONLY),
     new RegExp(Checkbox.SIMPLE_EXAMPLE),
diff --git a/src/breadcrumbs/breadcrumbs.js b/src/breadcrumbs/breadcrumbs.js
new file mode 100644
index 000000000..2e902f0fd
--- /dev/null
+++ b/src/breadcrumbs/breadcrumbs.js
@@ -0,0 +1,49 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+
+// @flow
+
+import React, {Children} from 'react';
+import type {BreadcrumbsPropsT} from './types';
+import {StyledRoot, StyledSeparator, StyledIcon} from './styled-components';
+import {getOverrides} from '../helpers/overrides';
+
+function Breadcrumbs({children, overrides = {}}: BreadcrumbsPropsT) {
+  const numChildren = Children.count(children);
+  const childrenWithSeparators = [];
+
+  const [Root, baseRootProps] = getOverrides(overrides.Root, StyledRoot);
+  const [Icon, baseIconProps] = getOverrides(overrides.Icon, StyledIcon);
+  const [Separator, baseSeparatorProps] = getOverrides(
+    overrides.Separator,
+    StyledSeparator,
+  );
+
+  Children.forEach(children, (child, index) => {
+    childrenWithSeparators.push(child);
+
+    if (index !== numChildren - 1) {
+      childrenWithSeparators.push(
+        <Separator {...baseSeparatorProps} key={index}>
+          <Icon {...baseIconProps} />
+        </Separator>,
+      );
+    }
+  });
+
+  return (
+    <Root aria-label="Breadcrumbs navigation" {...baseRootProps}>
+      {childrenWithSeparators}
+    </Root>
+  );
+}
+
+Breadcrumbs.defaultProps = {
+  overrides: {},
+};
+
+export default Breadcrumbs;
diff --git a/src/breadcrumbs/examples-list.js b/src/breadcrumbs/examples-list.js
new file mode 100644
index 000000000..6e85e6d27
--- /dev/null
+++ b/src/breadcrumbs/examples-list.js
@@ -0,0 +1,14 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+
+/* eslint-env node */
+/* eslint-disable flowtype/require-valid-file-annotation */
+
+module.exports = {
+  DEFAULT: 'Breadcrumbs with Link component',
+  OVERRIDES: 'Breadcrumbs with custom overrides',
+};
diff --git a/src/breadcrumbs/examples.js b/src/breadcrumbs/examples.js
new file mode 100644
index 000000000..48b741d44
--- /dev/null
+++ b/src/breadcrumbs/examples.js
@@ -0,0 +1,43 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+
+// @flow
+
+import React from 'react';
+import type {ThemeT} from '../styles/types';
+import {StyledLink} from '../link';
+import {Breadcrumbs} from './';
+import examples from './examples-list';
+
+export default {
+  [examples.DEFAULT]: function Story1() {
+    return (
+      <Breadcrumbs>
+        <StyledLink href="#">Parent Page</StyledLink>
+        <StyledLink href="#">Sub-Parent Page</StyledLink>
+        <span>Current Page</span>
+      </Breadcrumbs>
+    );
+  },
+  [examples.OVERRIDES]: function Story2() {
+    return (
+      <Breadcrumbs
+        overrides={{
+          Separator: {
+            style: ({$theme}: {$theme: ThemeT}) => ({
+              color: $theme.colors.negative400,
+            }),
+          },
+        }}
+      >
+        <StyledLink href="#">Parent Page</StyledLink>
+        <StyledLink href="#">Sub-Parent Page</StyledLink>
+        <span>Current Page</span>
+      </Breadcrumbs>
+    );
+  },
+};
diff --git a/src/breadcrumbs/index.js b/src/breadcrumbs/index.js
new file mode 100644
index 000000000..876b2d17b
--- /dev/null
+++ b/src/breadcrumbs/index.js
@@ -0,0 +1,12 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+
+// @flow
+
+export {default as Breadcrumbs} from './breadcrumbs';
+export * from './styled-components';
+export * from './types';
diff --git a/src/breadcrumbs/stories.js b/src/breadcrumbs/stories.js
new file mode 100644
index 000000000..68a89eb74
--- /dev/null
+++ b/src/breadcrumbs/stories.js
@@ -0,0 +1,23 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+
+/* @flow */
+/*global module */
+
+import {storiesOf} from '@storybook/react';
+import {withReadme} from 'storybook-readme';
+
+import examples from './examples';
+//$FlowFixMe
+import BreadcrumbsREADME from '../../rfcs/breadcrumbs-component.md';
+
+Object.entries(examples).forEach(([description, example]) =>
+  storiesOf('Breadcrumbs', module)
+    .addDecorator(withReadme(BreadcrumbsREADME))
+    // $FlowFixMe
+    .add(description, example),
+);
diff --git a/src/breadcrumbs/styled-components.js b/src/breadcrumbs/styled-components.js
new file mode 100644
index 000000000..9d58ae300
--- /dev/null
+++ b/src/breadcrumbs/styled-components.js
@@ -0,0 +1,34 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+
+// @flow
+
+import {styled} from '../styles/index';
+import type {StyledRootPropsT, StyledSeparatorT} from './types';
+import {ChevronRight} from '../icon';
+
+export const StyledRoot = styled('nav', ({$theme}: StyledRootPropsT) => {
+  return {
+    color: $theme.colors.breadcrumbsText,
+    ...$theme.typography.font450,
+  };
+});
+
+export const StyledSeparator = styled('div', ({$theme}: StyledSeparatorT) => {
+  return {
+    display: 'inline-block',
+    color: $theme.colors.breadcrumbsSeparatorFill,
+    marginLeft: $theme.sizing.scale300,
+    marginRight: $theme.sizing.scale300,
+  };
+});
+
+export const StyledIcon = styled(ChevronRight, () => {
+  return {
+    verticalAlign: 'text-bottom',
+  };
+});
diff --git a/src/breadcrumbs/types.js b/src/breadcrumbs/types.js
new file mode 100644
index 000000000..740487b2e
--- /dev/null
+++ b/src/breadcrumbs/types.js
@@ -0,0 +1,31 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+
+// @flow
+
+import type {Node} from 'react';
+import type {ThemeT} from '../styles/types';
+import type {OverrideT} from '../helpers/overrides';
+
+export type OverridesT = {
+  Root?: OverrideT<*>,
+  Separator?: OverrideT<*>,
+  Icon?: OverrideT<*>,
+};
+
+export type BreadcrumbsPropsT = {
+  children?: Node,
+  overrides?: OverridesT,
+};
+
+export type StyledRootPropsT = {
+  $theme: ThemeT,
+};
+
+export type StyledSeparatorT = {
+  $theme: ThemeT,
+};
diff --git a/src/styles/types.js b/src/styles/types.js
index e53bcb3b1..fd8213946 100644
--- a/src/styles/types.js
+++ b/src/styles/types.js
@@ -106,6 +106,10 @@ export type ColorsT = {
   buttonDisabledFill: string,
   buttonDisabledText: string,
 
+  // Breadcrumbs
+  breadcrumbsText: string,
+  breadcrumbsSeparatorFill: string,
+
   // Links
   linkText: string,
   linkVisited: string,
diff --git a/src/themes/creator.js b/src/themes/creator.js
index cbe9759f7..1277424de 100644
--- a/src/themes/creator.js
+++ b/src/themes/creator.js
@@ -118,6 +118,10 @@ export default function createTheme(
       buttonDisabledFill: primitives.mono300,
       buttonDisabledText: primitives.mono600,
 
+      // Breadcrumbs
+      breadcrumbsText: primitives.mono900,
+      breadcrumbsSeparatorFill: primitives.mono700,
+
       // Links
       linkText: primitives.primary400,
       linkVisited: primitives.primary500,
diff --git a/src/themes/dark-theme.js b/src/themes/dark-theme.js
index 031f17d01..6a3e09263 100644
--- a/src/themes/dark-theme.js
+++ b/src/themes/dark-theme.js
@@ -54,6 +54,10 @@ export const DarkTheme = createTheme(
       buttonDisabledFill: primitives.mono700,
       buttonDisabledText: primitives.mono500,
 
+      // Breadcrumbs
+      breadcrumbsText: primitives.mono100,
+      breadcrumbsSeparatorFill: primitives.mono200,
+
       // Links
       linkText: primitives.primary300,
       linkVisited: primitives.primary300,


Test patch:
diff --git a/src/breadcrumbs/__tests__/__snapshots__/breadcrumbs.test.js.snap b/src/breadcrumbs/__tests__/__snapshots__/breadcrumbs.test.js.snap
new file mode 100644
index 000000000..acd3b5207
--- /dev/null
+++ b/src/breadcrumbs/__tests__/__snapshots__/breadcrumbs.test.js.snap
@@ -0,0 +1,31 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Breadcrumbs displays separators in correct positions 1`] = `
+<MockStyledComponent
+  aria-label="Breadcrumbs navigation"
+>
+  <MockStyledComponent
+    href="#"
+  >
+    Parent Page
+  </MockStyledComponent>
+  <MockStyledComponent
+    key="0"
+  >
+    <MockStyledComponent />
+  </MockStyledComponent>
+  <MockStyledComponent
+    href="#"
+  >
+    Sub-Parent Page
+  </MockStyledComponent>
+  <MockStyledComponent
+    key="1"
+  >
+    <MockStyledComponent />
+  </MockStyledComponent>
+  <span>
+    Current Page
+  </span>
+</MockStyledComponent>
+`;
diff --git a/src/breadcrumbs/__tests__/__snapshots__/styled-components.test.js.snap b/src/breadcrumbs/__tests__/__snapshots__/styled-components.test.js.snap
new file mode 100644
index 000000000..1a7509482
--- /dev/null
+++ b/src/breadcrumbs/__tests__/__snapshots__/styled-components.test.js.snap
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`StyledComponents StyledIcon displays correct styles by default 1`] = `
+Object {
+  "verticalAlign": "text-bottom",
+}
+`;
+
+exports[`StyledComponents StyledRoot displays correct styles by default 1`] = `
+Object {
+  "color": "$theme.colors.breadcrumbsText",
+  "fontFamily": "$theme.typography.font450.fontFamily",
+  "fontSize": "$theme.typography.font450.fontSize",
+  "fontWeight": "$theme.typography.font450.fontWeight",
+  "lineHeight": "$theme.typography.font450.lineHeight",
+}
+`;
+
+exports[`StyledComponents StyledSeparator displays correct styles by default 1`] = `
+Object {
+  "color": "$theme.colors.breadcrumbsSeparatorFill",
+  "display": "inline-block",
+  "marginLeft": "$theme.sizing.scale300",
+  "marginRight": "$theme.sizing.scale300",
+}
+`;
diff --git a/src/breadcrumbs/__tests__/breadcrumbs.test.js b/src/breadcrumbs/__tests__/breadcrumbs.test.js
new file mode 100644
index 000000000..a2ca16f8b
--- /dev/null
+++ b/src/breadcrumbs/__tests__/breadcrumbs.test.js
@@ -0,0 +1,37 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+
+// @flow
+import React from 'react';
+import {shallow} from 'enzyme';
+import {StyledLink} from '../../link';
+import {Breadcrumbs} from '../';
+
+describe('Breadcrumbs', () => {
+  it('applies correct accessibility attributes to root element', () => {
+    const example = shallow(
+      <Breadcrumbs>
+        <StyledLink href="#">Parent Page</StyledLink>
+        <StyledLink href="#">Sub-Parent Page</StyledLink>
+        <span>Current Page</span>
+      </Breadcrumbs>,
+    );
+    expect(example).toHaveProp('aria-label', 'Breadcrumbs navigation');
+  });
+
+  it('displays separators in correct positions', () => {
+    expect(
+      shallow(
+        <Breadcrumbs>
+          <StyledLink href="#">Parent Page</StyledLink>
+          <StyledLink href="#">Sub-Parent Page</StyledLink>
+          <span>Current Page</span>
+        </Breadcrumbs>,
+      ),
+    ).toMatchSnapshot();
+  });
+});
diff --git a/src/breadcrumbs/__tests__/styled-components.test.js b/src/breadcrumbs/__tests__/styled-components.test.js
new file mode 100644
index 000000000..89eac5f37
--- /dev/null
+++ b/src/breadcrumbs/__tests__/styled-components.test.js
@@ -0,0 +1,34 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+
+// @flow
+import React from 'react';
+import {shallow} from 'enzyme';
+import {StyledRoot, StyledSeparator, StyledIcon} from '../styled-components';
+
+describe('StyledComponents', () => {
+  describe('StyledRoot', () => {
+    it('displays correct styles by default', () => {
+      const component = shallow(<StyledRoot />);
+      expect(component.instance().getStyles()).toMatchSnapshot();
+    });
+  });
+
+  describe('StyledSeparator', () => {
+    it('displays correct styles by default', () => {
+      const component = shallow(<StyledSeparator />);
+      expect(component.instance().getStyles()).toMatchSnapshot();
+    });
+  });
+
+  describe('StyledIcon', () => {
+    it('displays correct styles by default', () => {
+      const component = shallow(<StyledIcon />);
+      expect(component.instance().getStyles()).toMatchSnapshot();
+    });
+  });
+});