Repository: mapbox/mr-ui
Pull request #54 (2018-11-09 20:04:48)

Update ChevronousText, Add GoLink and NewTabLink
### Related issues

This PR closes https://github.com/mapbox/mr-ui/issues/13 and closes https://github.com/mapbox/mr-ui/issues/52 as adding `GoLink` (79d30c3) is dependent on adding `NewTabLink` (9059937) and updating `ChevronousText` (afb1f7a and 5103c7d).

### Description of changes

👋 Open to discussion/feedback!

**ChevronousText**
- Remove `themeIcon` for 2 reasons: it broke from the last update to `Icon` and based on how Mapbox design has traditionally used this component. The text and chevron always match in color and the text can vary in `txt-s`, `txt-m`, and `txt-l` in size, but the chevron stays the same size.
- Add `iconBefore` prop. At Mapbox we've only used `ChevronousText` for links so it's beneficial for `GoLink` to wrap the styling behaviors that `ChevronousText` offers.
- Possible future feature for this component? Allow the user to choose a `left`, `right`, `up`, or `down` chevron. Based on the current name of the component, it's not intuitive that they're only allowed to use a `left` or `right` chevron.

**NewTabLink**
- New component, examples, and tests that opinionatedly applies `rel="noopener"` to encourage more browser content-sharing security.

**GoLink**
- New component, examples, and tests that uses `ChevronousText` and `NewTabLink`. Allows opinionated variation in the following: 
  - text size (`small`, `medium`, `large`)
  - link color (`light`, `dark`)
  - bolded or unbolded text
  - whether this is a go back link (`chevron-left` before text) or a go forward link (`chevron-right` after text)
  - whether link should go directly to the `href` or open it in new tab

### Testing

- Created/updated documentation examples for each component. See the documentation Batfish site with `npm run start-docs`. Noting here that the chevron looks off in the live reload view, but otherwise normal in the built site version with `npm run build-docs` and `npm run serve-static-docs`.
- Created/updated Jest test cases and snapshots for each component. See `npm test` and the test cases app with `npm start`.

![ChevronousText test cases](https://user-images.githubusercontent.com/9087698/48283198-a85bd380-e410-11e8-9433-3f251bc6333c.png)
___
![NewTabLink test cases](https://user-images.githubusercontent.com/9087698/48283190-a2fe8900-e410-11e8-85a2-fe4bf9d2ee23.png)
___
![GoLink test cases](https://user-images.githubusercontent.com/9087698/48283204-ac87f100-e410-11e8-8683-969673cf2bec.png)

### Notes for the future

This is 1 of the series of new features (https://github.com/mapbox/mr-ui/issues/51, https://github.com/mapbox/mr-ui/issues/53, and https://github.com/mapbox/mr-ui/issues/50) I'd like to add before updating the `CHANGELOG.md` and releasing a new version of `mr-ui`.

Expect to add the following to the `CHANGELOG.md`
```
- [feature] Add **GoLink** component.
- [feature] Add **NewTabLink** component.
- **ChevronousText**
  - 🚨 [breaking change] Remove `themeIcon` prop.
  - [feature] Add `beforeIcon` prop.
```


lshig: > Did you intend to keep this behavior (it looks like it's still working this way), or did you consider this behavior a bug? (I might lean towards considering it a bug, since it doesn't seem natural that the chevron would be the same size for very small and very large text.)

Originally this was a minor breaking change from `Icon` not having `iconTheme`. And then... I kept the behavior because there wasn't any Mapbox use cases of the text being so large that the chevron looked extra small (besides the `ChrevonousText` example in the documentation). Design-wise, it makes sense to at least allow the option to adjust the icon size in comparison to the text. I added this feature in 8e4d9f4 by adding an `iconSize` prop in `ChevronousText`. It will continue to allow a wrapper component around `ChevronousText` to determine if the icon size grows or stays at `18` by default. `Icon`'s `size` prop is also set to `18` by default.

> Do you know why the instance of `ChevronousText` in the documentation look misaligned?

The built version `lineHeight` from `setHeight()` in `Icon` returns back `24px`, while the development live server version returns back `normal`. Therefore... it won't adjust the height of the inlined `Icon`. I updated the example to ensure that a line-height was provided in 8e4d9f4, similar to `Icon`'s '`icon-b.js` example.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 94300a3..80948fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
 # Changelog
 
+## 0.4.0 - Next release
+- [feature] Add **GoLink** component.
+- [feature] Add **NewTabLink** component.
+- **ChevronousText**
+  - 🚨 [breaking change] Remove `themeIcon` prop.
+  - [feature] Add `iconSize` prop.
+  - [feature] Add `beforeIcon` prop.
+- **Icon**
+  - [feature] Allow `size` prop to be a number or string .
+
 ## 0.3.2
 
 - [fix] Put `ControlFile`'s icon and text on the same line.
diff --git a/package-lock.json b/package-lock.json
index e07fdd6..b4d8a78 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "@mapbox/mr-ui",
-  "version": "0.3.1",
+  "version": "0.3.2",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/src/components/chevronous-text/chevronous-text.js b/src/components/chevronous-text/chevronous-text.js
index df2e90d..90861b5 100644
--- a/src/components/chevronous-text/chevronous-text.js
+++ b/src/components/chevronous-text/chevronous-text.js
@@ -4,27 +4,48 @@ import Icon from '../icon';
 
 export default class ChevronousText extends React.PureComponent {
   static propTypes = {
+    /** When true, the text will follow after a left pointed chevron. */
+    iconBefore: PropTypes.bool,
+    /**
+     * The width and height size of the chevron icon. Note that this icon is
+     * inline and the height won't go beyond the line-height. This can be a
+     * number which will fall back to px, or a string in the units of
+     * px, em, %, or pt.
+     */
+    iconSize: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
     /** The text that should be aligned next to the chevron. */
-    text: PropTypes.string.isRequired,
-    /** Additional classes to apply to the icon element */
-    themeIcon: PropTypes.string
+    text: PropTypes.string.isRequired
+  };
+
+  static defaultProps = {
+    iconBefore: false,
+    iconSize: '1.5em'
   };
 
   render() {
-    const splitText = this.props.text.split(' ');
-    const lastWord = splitText.pop();
-    const textWithoutLastWord = splitText.join(' ');
+    const { iconBefore, iconSize, text } = this.props;
+    const splitText = text.split(' ');
+    const iconWord = iconBefore ? splitText.shift() : splitText.pop();
+    const textWithoutIconWord = splitText.join(' ');
+
+    if (iconBefore) {
+      return (
+        <span className="inline-block">
+          <span className="txt-nowrap">
+            <Icon name="chevron-left" inline={true} size={iconSize} />
+            {iconWord}
+          </span>{' '}
+          {textWithoutIconWord}
+        </span>
+      );
+    }
 
     return (
-      <span>
-        {textWithoutLastWord}{' '}
+      <span className="inline-block">
+        {textWithoutIconWord}{' '}
         <span className="txt-nowrap">
-          {lastWord}
-          <Icon
-            name="chevron-right"
-            inline={true}
-            themeIcon={this.props.themeIcon}
-          />
+          {iconWord}
+          <Icon name="chevron-right" inline={true} size={iconSize} />
         </span>
       </span>
     );
diff --git a/src/components/chevronous-text/examples/chevronous-text-example-basic.js b/src/components/chevronous-text/examples/chevronous-text-example-basic.js
index 4f64fd5..b5b1e5a 100644
--- a/src/components/chevronous-text/examples/chevronous-text-example-basic.js
+++ b/src/components/chevronous-text/examples/chevronous-text-example-basic.js
@@ -6,6 +6,10 @@ import ChevronousText from '../chevronous-text';
 
 export default class Example extends React.Component {
   render() {
-    return <ChevronousText text="Hello explorer" />;
+    return (
+      <div style={{ lineHeight: '24px' }}>
+        <ChevronousText text="Hello explorer" />
+      </div>
+    );
   }
 }
diff --git a/src/components/chevronous-text/examples/chevronous-text-example-heading.js b/src/components/chevronous-text/examples/chevronous-text-example-heading.js
index 9c1eee1..67f4565 100644
--- a/src/components/chevronous-text/examples/chevronous-text-example-heading.js
+++ b/src/components/chevronous-text/examples/chevronous-text-example-heading.js
@@ -8,7 +8,7 @@ export default class Example extends React.Component {
   render() {
     return (
       <div className="txt-xl">
-        <ChevronousText text="Hello Explorer" themeIcon="color-pink w30" />
+        <ChevronousText text="Hello Explorer" iconBefore={true} iconSize={45} />
       </div>
     );
   }
diff --git a/src/components/go-link/examples/go-link-a.js b/src/components/go-link/examples/go-link-a.js
new file mode 100644
index 0000000..bf414ae
--- /dev/null
+++ b/src/components/go-link/examples/go-link-a.js
@@ -0,0 +1,11 @@
+/*
+Basic.
+*/
+import React from 'react';
+import GoLink from '../go-link';
+
+export default class Example extends React.Component {
+  render() {
+    return <GoLink href="http://www.mapbox.com" text="View all of Mapbox" />;
+  }
+}
diff --git a/src/components/go-link/examples/go-link-b.js b/src/components/go-link/examples/go-link-b.js
new file mode 100644
index 0000000..dc38db0
--- /dev/null
+++ b/src/components/go-link/examples/go-link-b.js
@@ -0,0 +1,23 @@
+/*
+GoLink with options
+*/
+import React from 'react';
+import GoLink from '../go-link';
+
+export default class Example extends React.Component {
+  render() {
+    return (
+      <div className="bg-pink">
+        <GoLink
+          color="light"
+          goBack={true}
+          isBold={false}
+          isNewTab={true}
+          href="http://www.mapbox.com"
+          size="small"
+          text="Go back to Mapbox!"
+        />
+      </div>
+    );
+  }
+}
diff --git a/src/components/go-link/go-link.js b/src/components/go-link/go-link.js
new file mode 100644
index 0000000..ee5a4a3
--- /dev/null
+++ b/src/components/go-link/go-link.js
@@ -0,0 +1,61 @@
+import classnames from 'classnames';
+import ChevronousText from '../chevronous-text';
+import PropTypes from 'prop-types';
+import NewTabLink from '../new-tab-link';
+import React from 'react';
+
+/**
+ * Standard styled link for going to a previous or next location.
+ */
+export default class GoLink extends React.PureComponent {
+  static propTypes = {
+    /** Two colors: "light" or "dark". */
+    color: PropTypes.oneOf(['light', 'dark']),
+    /** Whether or not this is a go back or go forward link. */
+    goBack: PropTypes.bool,
+    /** URL or path to the page the new tab should go to when clicked. */
+    href: PropTypes.string.isRequired,
+    /** Whether or not the text is bold. */
+    isBold: PropTypes.bool,
+    /** Whether or not this link should go to the href in a new tab. */
+    isNewTab: PropTypes.bool,
+    /** Three sizes: "small", "medium", and "large". */
+    size: PropTypes.oneOf(['small', 'medium', 'large']),
+    /** Link text. */
+    text: PropTypes.string.isRequired
+  };
+
+  static defaultProps = {
+    color: 'dark',
+    goBack: false,
+    isBold: true,
+    isNewTab: false,
+    size: 'medium'
+  };
+
+  render() {
+    const { color, goBack, href, isBold, isNewTab, size, text } = this.props;
+    const linkClasses = classnames('inline-block link', {
+      'link--blue': color === 'dark',
+      'link--white': color === 'light',
+      'txt-l': size === 'large',
+      'txt-m': size === 'medium',
+      'txt-s': size === 'small',
+      'txt-bold': isBold
+    });
+
+    if (isNewTab) {
+      return (
+        <NewTabLink href={href} className={linkClasses}>
+          <ChevronousText text={text} iconBefore={goBack} />
+        </NewTabLink>
+      );
+    }
+
+    return (
+      <a href={href} className={linkClasses}>
+        <ChevronousText text={text} iconBefore={goBack} />
+      </a>
+    );
+  }
+}
diff --git a/src/components/go-link/index.js b/src/components/go-link/index.js
new file mode 100644
index 0000000..6fd5639
--- /dev/null
+++ b/src/components/go-link/index.js
@@ -0,0 +1,3 @@
+import main from './go-link';
+
+export default main;
diff --git a/src/components/icon/icon.js b/src/components/icon/icon.js
index 21869b9..d17b277 100644
--- a/src/components/icon/icon.js
+++ b/src/components/icon/icon.js
@@ -89,7 +89,7 @@ Icon.propTypes = {
    * be in accordance with your `size` value (because it's limited by the
    * width).
    */
-  size: PropTypes.number,
+  size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
   /**
    * If `true`, the icon will be adjusted after mounting so that its height
    * matches the line-height of its container. The result of this is that
diff --git a/src/components/new-tab-link/examples/new-tab-link-a.js b/src/components/new-tab-link/examples/new-tab-link-a.js
new file mode 100644
index 0000000..16121c4
--- /dev/null
+++ b/src/components/new-tab-link/examples/new-tab-link-a.js
@@ -0,0 +1,11 @@
+/*
+Basic.
+*/
+import React from 'react';
+import NewTabLink from '../new-tab-link';
+
+export default class Example extends React.Component {
+  render() {
+    return <NewTabLink href="">Hello explorer!</NewTabLink>;
+  }
+}
diff --git a/src/components/new-tab-link/examples/new-tab-link-b.js b/src/components/new-tab-link/examples/new-tab-link-b.js
new file mode 100644
index 0000000..738a64e
--- /dev/null
+++ b/src/components/new-tab-link/examples/new-tab-link-b.js
@@ -0,0 +1,15 @@
+/*
+NewTabLink with options.
+*/
+import React from 'react';
+import NewTabLink from '../new-tab-link';
+
+export default class Example extends React.Component {
+  render() {
+    return (
+      <NewTabLink className="link link--pink" href="http://www.mapbox.com">
+        Hello explorer!
+      </NewTabLink>
+    );
+  }
+}
diff --git a/src/components/new-tab-link/index.js b/src/components/new-tab-link/index.js
new file mode 100644
index 0000000..c8c2723
--- /dev/null
+++ b/src/components/new-tab-link/index.js
@@ -0,0 +1,3 @@
+import main from './new-tab-link';
+
+export default main;
diff --git a/src/components/new-tab-link/new-tab-link.js b/src/components/new-tab-link/new-tab-link.js
new file mode 100644
index 0000000..e2d148a
--- /dev/null
+++ b/src/components/new-tab-link/new-tab-link.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+/**
+ * A simple new tab link wrapper component.
+ *
+ * This component applies an opinionated relationship (rel) attribute to
+ * prevent the new tab from accessing the originating tab's content.
+ */
+export default class NewTabLink extends React.Component {
+  static propTypes = {
+    /** The content of the new tab link. */
+    children: PropTypes.node.isRequired,
+    /** The new tab location path. */
+    href: PropTypes.string.isRequired
+  };
+
+  render() {
+    const { props } = this;
+
+    return (
+      <a target="_blank" rel="noopener" {...props}>
+        {props.children}
+      </a>
+    );
+  }
+}


Test patch:
diff --git a/src/components/chevronous-text/__tests__/__snapshots__/chevronous-text.test.js.snap b/src/components/chevronous-text/__tests__/__snapshots__/chevronous-text.test.js.snap
index 300865c..f0d2ab9 100644
--- a/src/components/chevronous-text/__tests__/__snapshots__/chevronous-text.test.js.snap
+++ b/src/components/chevronous-text/__tests__/__snapshots__/chevronous-text.test.js.snap
@@ -1,7 +1,9 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`ChevronousText additional icon classes renders 1`] = `
-<span>
+exports[`ChevronousText basic renders as expected 1`] = `
+<span
+  className="inline-block"
+>
   Explore
    
   <span
@@ -12,26 +14,123 @@ exports[`ChevronousText additional icon classes renders 1`] = `
       inline={true}
       name="chevron-right"
       passthroughProps={Object {}}
-      size={18}
-      themeIcon="color-blue"
+      size="1.5em"
     />
   </span>
 </span>
 `;
 
-exports[`ChevronousText basic renders 1`] = `
-<span>
-  Explore
+exports[`ChevronousText chevron before multiline text renders as expected 1`] = `
+<span
+  className="inline-block"
+>
+  <span
+    className="txt-nowrap"
+  >
+    <Icon
+      inline={true}
+      name="chevron-left"
+      passthroughProps={Object {}}
+      size="1.5em"
+    />
+    "It
+  </span>
    
+  was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it was the winter of despair, we had everything before us, we had nothing before us..." - A Tale of Two Cities
+</span>
+`;
+
+exports[`ChevronousText chevron before one long unwrappable word text renders as expected 1`] = `
+<span
+  className="inline-block"
+>
   <span
     className="txt-nowrap"
   >
-    Mapbox
+    <Icon
+      inline={true}
+      name="chevron-left"
+      passthroughProps={Object {}}
+      size="1.5em"
+    />
+    Lopadotemachoselachogaleokranioleipsanodrimhypotrimmatosilphioparaomelitokatakechymenokichlepikossyphophattoperisteralektryonoptekephalliokigklopeleiolagoiosiraiobaphetraganopterygon
+  </span>
+   
+</span>
+`;
+
+exports[`ChevronousText multiline text renders as expected 1`] = `
+<span
+  className="inline-block"
+>
+  "It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it was the winter of despair, we had everything before us, we had nothing before us..." - A Tale of Two
+   
+  <span
+    className="txt-nowrap"
+  >
+    Cities
+    <Icon
+      inline={true}
+      name="chevron-right"
+      passthroughProps={Object {}}
+      size="1.5em"
+    />
+  </span>
+</span>
+`;
+
+exports[`ChevronousText one long unwrappable word text renders as expected 1`] = `
+<span
+  className="inline-block"
+>
+   
+  <span
+    className="txt-nowrap"
+  >
+    Pneumonoultramicroscopicsilicovolcanoconiosis
+    <Icon
+      inline={true}
+      name="chevron-right"
+      passthroughProps={Object {}}
+      size="1.5em"
+    />
+  </span>
+</span>
+`;
+
+exports[`ChevronousText sized icon before text renders as expected 1`] = `
+<span
+  className="inline-block"
+>
+  <span
+    className="txt-nowrap"
+  >
+    <Icon
+      inline={true}
+      name="chevron-left"
+      passthroughProps={Object {}}
+      size="1.7em"
+    />
+    Cheers!
+  </span>
+   
+</span>
+`;
+
+exports[`ChevronousText sized icon renders as expected 1`] = `
+<span
+  className="inline-block"
+>
+   
+  <span
+    className="txt-nowrap"
+  >
+    Cheers!
     <Icon
       inline={true}
       name="chevron-right"
       passthroughProps={Object {}}
-      size={18}
+      size={10}
     />
   </span>
 </span>
diff --git a/src/components/chevronous-text/__tests__/chevronous-text-test-cases.js b/src/components/chevronous-text/__tests__/chevronous-text-test-cases.js
index a72acdd..41b6abe 100644
--- a/src/components/chevronous-text/__tests__/chevronous-text-test-cases.js
+++ b/src/components/chevronous-text/__tests__/chevronous-text-test-cases.js
@@ -10,12 +10,59 @@ testCases.basic = {
   }
 };
 
-testCases.additionalIconClasses = {
-  description: 'additional icon classes',
+testCases.oneLongWord = {
+  description: 'one long unwrappable word text',
   component: ChevronousText,
   props: {
-    text: 'Explore Mapbox',
-    themeIcon: 'color-blue'
+    text: 'Pneumonoultramicroscopicsilicovolcanoconiosis'
+  }
+};
+
+testCases.multilineText = {
+  description: 'multiline text',
+  component: ChevronousText,
+  props: {
+    text:
+      '"It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it was the winter of despair, we had everything before us, we had nothing before us..." - A Tale of Two Cities'
+  }
+};
+
+testCases.sizedIcon = {
+  description: 'sized icon',
+  component: ChevronousText,
+  props: {
+    iconSize: 10,
+    text: 'Cheers!'
+  }
+};
+
+testCases.iconBeforeOneLongWord = {
+  description: 'chevron before one long unwrappable word text',
+  component: ChevronousText,
+  props: {
+    iconBefore: true,
+    text:
+      'Lopadotemachoselachogaleokranioleipsanodrimhypotrimmatosilphioparaomelitokatakechymenokichlepikossyphophattoperisteralektryonoptekephalliokigklopeleiolagoiosiraiobaphetraganopterygon'
+  }
+};
+
+testCases.iconBeforeMultilineText = {
+  description: 'chevron before multiline text',
+  component: ChevronousText,
+  props: {
+    iconBefore: true,
+    text:
+      '"It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it was the winter of despair, we had everything before us, we had nothing before us..." - A Tale of Two Cities'
+  }
+};
+
+testCases.sizedIconBeforeText = {
+  description: 'sized icon before text',
+  component: ChevronousText,
+  props: {
+    iconBefore: true,
+    iconSize: '1.7em',
+    text: 'Cheers!'
   }
 };
 
diff --git a/src/components/chevronous-text/__tests__/chevronous-text.test.js b/src/components/chevronous-text/__tests__/chevronous-text.test.js
index accd934..dc4e829 100644
--- a/src/components/chevronous-text/__tests__/chevronous-text.test.js
+++ b/src/components/chevronous-text/__tests__/chevronous-text.test.js
@@ -3,31 +3,83 @@ import { shallow } from 'enzyme';
 import { testCases } from './chevronous-text-test-cases';
 
 describe('ChevronousText', () => {
-  let testCase;
-  let wrapper;
-
   describe(testCases.basic.description, () => {
-    beforeEach(() => {
-      testCase = testCases.basic;
-      wrapper = shallow(
-        React.createElement(testCase.component, testCase.props)
-      );
+    const wrapper = shallow(
+      React.createElement(testCases.basic.component, testCases.basic.props)
+    );
+    test('renders as expected', () => {
+      expect(wrapper).toMatchSnapshot();
     });
+  });
 
-    test('renders', () => {
+  describe(testCases.oneLongWord.description, () => {
+    const wrapper = shallow(
+      React.createElement(
+        testCases.oneLongWord.component,
+        testCases.oneLongWord.props
+      )
+    );
+    test('renders as expected', () => {
       expect(wrapper).toMatchSnapshot();
     });
   });
 
-  describe(testCases.additionalIconClasses.description, () => {
-    beforeEach(() => {
-      testCase = testCases.additionalIconClasses;
-      wrapper = shallow(
-        React.createElement(testCase.component, testCase.props)
-      );
+  describe(testCases.multilineText.description, () => {
+    const wrapper = shallow(
+      React.createElement(
+        testCases.multilineText.component,
+        testCases.multilineText.props
+      )
+    );
+    test('renders as expected', () => {
+      expect(wrapper).toMatchSnapshot();
     });
+  });
+
+  describe(testCases.sizedIcon.description, () => {
+    const wrapper = shallow(
+      React.createElement(
+        testCases.sizedIcon.component,
+        testCases.sizedIcon.props
+      )
+    );
+    test('renders as expected', () => {
+      expect(wrapper).toMatchSnapshot();
+    });
+  });
+
+  describe(testCases.iconBeforeOneLongWord.description, () => {
+    const wrapper = shallow(
+      React.createElement(
+        testCases.iconBeforeOneLongWord.component,
+        testCases.iconBeforeOneLongWord.props
+      )
+    );
+    test('renders as expected', () => {
+      expect(wrapper).toMatchSnapshot();
+    });
+  });
+
+  describe(testCases.iconBeforeMultilineText.description, () => {
+    const wrapper = shallow(
+      React.createElement(
+        testCases.iconBeforeMultilineText.component,
+        testCases.iconBeforeMultilineText.props
+      )
+    );
+    test('renders as expected', () => {
+      expect(wrapper).toMatchSnapshot();
+    });
+  });
 
-    test('renders', () => {
+  describe(testCases.sizedIconBeforeText.description, () => {
+    const wrapper = shallow(
+      React.createElement(
+        testCases.sizedIconBeforeText.component,
+        testCases.sizedIconBeforeText.props
+      )
+    );
+    test('renders as expected', () => {
       expect(wrapper).toMatchSnapshot();
     });
   });
diff --git a/src/components/go-link/__tests__/__snapshots__/go-link.test.js.snap b/src/components/go-link/__tests__/__snapshots__/go-link.test.js.snap
new file mode 100644
index 0000000..231d9cf
--- /dev/null
+++ b/src/components/go-link/__tests__/__snapshots__/go-link.test.js.snap
@@ -0,0 +1,105 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`GoLink basic go forward link renders as expected 1`] = `
+<a
+  className="inline-block link link--blue txt-m txt-bold"
+  href="http://www.mapbox.com"
+>
+  <ChevronousText
+    iconBefore={false}
+    iconSize="1.5em"
+    text="Read Mapbox"
+  />
+</a>
+`;
+
+exports[`GoLink go forward new tab link renders as expected 1`] = `
+<NewTabLink
+  className="inline-block link link--blue txt-m txt-bold"
+  href="http://www.mapbox.com"
+>
+  <ChevronousText
+    iconBefore={false}
+    iconSize="1.5em"
+    text="See all of the things"
+  />
+</NewTabLink>
+`;
+
+exports[`GoLink large go forward link renders as expected 1`] = `
+<a
+  className="inline-block link link--blue txt-l txt-bold"
+  href="http://www.mapbox.com"
+>
+  <ChevronousText
+    iconBefore={false}
+    iconSize="1.5em"
+    text="Reach for the stars"
+  />
+</a>
+`;
+
+exports[`GoLink light go forward link renders as expected 1`] = `
+<a
+  className="inline-block link link--white txt-m txt-bold"
+  href="http://www.mapbox.com"
+>
+  <ChevronousText
+    iconBefore={false}
+    iconSize="1.5em"
+    text="View all of Mapbox"
+  />
+</a>
+`;
+
+exports[`GoLink multiline text, go forward link renders as expected 1`] = `
+<a
+  className="inline-block link link--blue txt-m txt-bold"
+  href="http://www.mapbox.com"
+>
+  <ChevronousText
+    iconBefore={false}
+    iconSize="1.5em"
+    text="\\"It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it was the winter of despair, we had everything before us, we had nothing before us...\\" - A Tale of Two Cities"
+  />
+</a>
+`;
+
+exports[`GoLink small go forward link renders as expected 1`] = `
+<a
+  className="inline-block link link--blue txt-s txt-bold"
+  href="http://www.mapbox.com"
+>
+  <ChevronousText
+    iconBefore={false}
+    iconSize="1.5em"
+    text="Maybe there's something cool if you go here"
+  />
+</a>
+`;
+
+exports[`GoLink small, unbolded, go back new tab link renders as expected 1`] = `
+<NewTabLink
+  className="inline-block link link--blue txt-s"
+  href="http://www.mapbox.com"
+>
+  <ChevronousText
+    iconBefore={true}
+    iconSize="1.5em"
+    text="Jusk kidding! Go back to this new place."
+  />
+</NewTabLink>
+`;
+
+exports[`GoLink unbolded go forward link renders as expected 1`] = `
+<a
+  className="inline-block link link--blue txt-m"
+  href="http://www.mapbox.com"
+>
+  <ChevronousText
+    iconBefore={false}
+    iconSize="1.5em"
+    text="Go to Mapbox"
+  />
+</a>
+`;
diff --git a/src/components/go-link/__tests__/go-link-test-cases.js b/src/components/go-link/__tests__/go-link-test-cases.js
new file mode 100644
index 0000000..9574a42
--- /dev/null
+++ b/src/components/go-link/__tests__/go-link-test-cases.js
@@ -0,0 +1,87 @@
+import GoLink from '../go-link';
+
+const testCases = {};
+
+testCases.basic = {
+  description: 'basic go forward link',
+  component: GoLink,
+  props: {
+    href: 'http://www.mapbox.com',
+    text: 'Read Mapbox'
+  }
+};
+
+testCases.multilineTextGoForward = {
+  description: 'multiline text, go forward link',
+  component: GoLink,
+  props: {
+    href: 'http://www.mapbox.com',
+    text:
+      '"It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it was the winter of despair, we had everything before us, we had nothing before us..." - A Tale of Two Cities'
+  }
+};
+
+testCases.smallGoForward = {
+  description: 'small go forward link',
+  component: GoLink,
+  props: {
+    size: 'small',
+    href: 'http://www.mapbox.com',
+    text: "Maybe there's something cool if you go here"
+  }
+};
+
+testCases.largeGoForward = {
+  description: 'large go forward link',
+  component: GoLink,
+  props: {
+    size: 'large',
+    href: 'http://www.mapbox.com',
+    text: 'Reach for the stars'
+  }
+};
+
+testCases.unboldedGoForward = {
+  description: 'unbolded go forward link',
+  component: GoLink,
+  props: {
+    href: 'http://www.mapbox.com',
+    isBold: false,
+    text: 'Go to Mapbox'
+  }
+};
+
+testCases.lightGoForward = {
+  description: 'light go forward link',
+  component: GoLink,
+  props: {
+    color: 'light',
+    href: 'http://www.mapbox.com',
+    text: 'View all of Mapbox'
+  }
+};
+
+testCases.newTabGoForward = {
+  description: 'go forward new tab link',
+  component: GoLink,
+  props: {
+    href: 'http://www.mapbox.com',
+    isNewTab: true,
+    text: 'See all of the things'
+  }
+};
+
+testCases.smallUnboldedNewTabGoBack = {
+  description: 'small, unbolded, go back new tab link',
+  component: GoLink,
+  props: {
+    goBack: true,
+    href: 'http://www.mapbox.com',
+    isBold: false,
+    isNewTab: true,
+    size: 'small',
+    text: 'Jusk kidding! Go back to this new place.'
+  }
+};
+
+export { testCases };
diff --git a/src/components/go-link/__tests__/go-link.test.js b/src/components/go-link/__tests__/go-link.test.js
new file mode 100644
index 0000000..daac8a1
--- /dev/null
+++ b/src/components/go-link/__tests__/go-link.test.js
@@ -0,0 +1,98 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { testCases } from './go-link-test-cases';
+
+describe('GoLink', () => {
+  describe(testCases.basic.description, () => {
+    const wrapper = shallow(
+      React.createElement(testCases.basic.component, testCases.basic.props)
+    );
+    test('renders as expected', () => {
+      expect(wrapper).toMatchSnapshot();
+    });
+  });
+
+  describe(testCases.multilineTextGoForward.description, () => {
+    const wrapper = shallow(
+      React.createElement(
+        testCases.multilineTextGoForward.component,
+        testCases.multilineTextGoForward.props
+      )
+    );
+    test('renders as expected', () => {
+      expect(wrapper).toMatchSnapshot();
+    });
+  });
+
+  describe(testCases.smallGoForward.description, () => {
+    const wrapper = shallow(
+      React.createElement(
+        testCases.smallGoForward.component,
+        testCases.smallGoForward.props
+      )
+    );
+    test('renders as expected', () => {
+      expect(wrapper).toMatchSnapshot();
+    });
+  });
+
+  describe(testCases.largeGoForward.description, () => {
+    const wrapper = shallow(
+      React.createElement(
+        testCases.largeGoForward.component,
+        testCases.largeGoForward.props
+      )
+    );
+    test('renders as expected', () => {
+      expect(wrapper).toMatchSnapshot();
+    });
+  });
+
+  describe(testCases.unboldedGoForward.description, () => {
+    const wrapper = shallow(
+      React.createElement(
+        testCases.unboldedGoForward.component,
+        testCases.unboldedGoForward.props
+      )
+    );
+    test('renders as expected', () => {
+      expect(wrapper).toMatchSnapshot();
+    });
+  });
+
+  describe(testCases.lightGoForward.description, () => {
+    const wrapper = shallow(
+      React.createElement(
+        testCases.lightGoForward.component,
+        testCases.lightGoForward.props
+      )
+    );
+    test('renders as expected', () => {
+      expect(wrapper).toMatchSnapshot();
+    });
+  });
+
+  describe(testCases.newTabGoForward.description, () => {
+    const wrapper = shallow(
+      React.createElement(
+        testCases.newTabGoForward.component,
+        testCases.newTabGoForward.props
+      )
+    );
+    test('renders as expected', () => {
+      expect(wrapper).toMatchSnapshot();
+    });
+  });
+
+  describe(testCases.smallUnboldedNewTabGoBack.description, () => {
+    const wrapper = shallow(
+      React.createElement(
+        testCases.smallUnboldedNewTabGoBack.component,
+        testCases.smallUnboldedNewTabGoBack.props
+      )
+    );
+    test('renders as expected', () => {
+      expect(wrapper).toMatchSnapshot();
+    });
+  });
+});
diff --git a/src/components/new-tab-link/__tests__/__snapshots__/new-tab-link.test.js.snap b/src/components/new-tab-link/__tests__/__snapshots__/new-tab-link.test.js.snap
new file mode 100644
index 0000000..94457cd
--- /dev/null
+++ b/src/components/new-tab-link/__tests__/__snapshots__/new-tab-link.test.js.snap
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NewTabLink basic 1`] = `
+<a
+  href=""
+  rel="noopener"
+  target="_blank"
+>
+  Hello explorer!
+</a>
+`;
+
+exports[`NewTabLink styling 1`] = `
+<a
+  className="link link--pink"
+  href="http://www.mapbox.com"
+  rel="noopener"
+  target="_blank"
+>
+  Hello explorer!
+</a>
+`;
diff --git a/src/components/new-tab-link/__tests__/new-tab-link-test-cases.js b/src/components/new-tab-link/__tests__/new-tab-link-test-cases.js
new file mode 100644
index 0000000..2225a00
--- /dev/null
+++ b/src/components/new-tab-link/__tests__/new-tab-link-test-cases.js
@@ -0,0 +1,24 @@
+import NewTabLink from '../new-tab-link';
+
+const testCases = {};
+
+testCases.basic = {
+  description: 'basic',
+  component: NewTabLink,
+  props: {
+    children: 'Hello explorer!',
+    href: ''
+  }
+};
+
+testCases.styling = {
+  description: 'styling',
+  component: NewTabLink,
+  props: {
+    children: 'Hello explorer!',
+    className: 'link link--pink',
+    href: 'http://www.mapbox.com'
+  }
+};
+
+export { testCases };
diff --git a/src/components/new-tab-link/__tests__/new-tab-link.test.js b/src/components/new-tab-link/__tests__/new-tab-link.test.js
new file mode 100644
index 0000000..c33b961
--- /dev/null
+++ b/src/components/new-tab-link/__tests__/new-tab-link.test.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { testCases } from './new-tab-link-test-cases';
+
+describe('NewTabLink', () => {
+  test(testCases.basic.description, () => {
+    const wrapper = shallow(
+      React.createElement(testCases.basic.component, testCases.basic.props)
+    );
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  test(testCases.styling.description, () => {
+    const wrapper = shallow(
+      React.createElement(testCases.styling.component, testCases.styling.props)
+    );
+    expect(wrapper).toMatchSnapshot();
+  });
+});