Repository: mapbox/mr-ui
Pull request #21 (2018-07-31 23:43:29)

MbxIconText component
A simple little component for putting an icon next to text.

I realized this would enable us to easily follow the `MbxTextButton` pattern described in https://github.com/mapbox/mr-ui/issues/11, but would also be more flexible. 

At long last we'll have a component that does nothing except implement some standard vertical centering and horizontal spacing! 🌋 Here's what you get:

<img width="396" alt="screen shot 2018-07-31 at 4 38 30 pm" src="https://user-images.githubusercontent.com/628431/43493024-b4d6dd78-94e0-11e8-972b-01b93e47436b.png">

<img width="264" alt="screen shot 2018-07-31 at 4 38 25 pm" src="https://user-images.githubusercontent.com/628431/43493032-b97ffa80-94e0-11e8-8319-ddf33ef8b708.png">

@jseppi for review, please.


Patch:
diff --git a/src/components/mbx-icon-text/examples/mbx-icon-text-a.js b/src/components/mbx-icon-text/examples/mbx-icon-text-a.js
new file mode 100644
index 0000000..0c44735
--- /dev/null
+++ b/src/components/mbx-icon-text/examples/mbx-icon-text-a.js
@@ -0,0 +1,15 @@
+/*
+The standard button-without-a-background look. Also useful for menu items.
+*/
+import React from 'react';
+import MbxIconText from '../mbx-icon-text';
+
+export default class Example extends React.Component {
+  render() {
+    return (
+      <button type="button" className="link txt-bold txt-s block">
+        <MbxIconText iconBefore="duplicate">Duplicate</MbxIconText>
+      </button>
+    );
+  }
+}
diff --git a/src/components/mbx-icon-text/examples/mbx-icon-text-b.js b/src/components/mbx-icon-text/examples/mbx-icon-text-b.js
new file mode 100644
index 0000000..08ad0ec
--- /dev/null
+++ b/src/components/mbx-icon-text/examples/mbx-icon-text-b.js
@@ -0,0 +1,15 @@
+/*
+Go see the next thing!
+*/
+import React from 'react';
+import MbxIconText from '../mbx-icon-text';
+
+export default class Example extends React.Component {
+  render() {
+    return (
+      <button type="button" className="link link-purple">
+        <MbxIconText iconAfter="chevron-right">Take me there</MbxIconText>
+      </button>
+    );
+  }
+}
diff --git a/src/components/mbx-icon-text/index.js b/src/components/mbx-icon-text/index.js
new file mode 100644
index 0000000..7fef300
--- /dev/null
+++ b/src/components/mbx-icon-text/index.js
@@ -0,0 +1,3 @@
+import main from './mbx-icon-text';
+
+export default main;
diff --git a/src/components/mbx-icon-text/mbx-icon-text.js b/src/components/mbx-icon-text/mbx-icon-text.js
new file mode 100644
index 0000000..b5f932e
--- /dev/null
+++ b/src/components/mbx-icon-text/mbx-icon-text.js
@@ -0,0 +1,69 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Icon from '../icon';
+
+/**
+ * Put an icon next to some text.
+ *
+ * The icon and text will be vertically centered, with standard spacing between.
+ */
+class MbxIconText extends React.Component {
+  renderIcon(icon) {
+    if (typeof icon === 'string') {
+      return <Icon name={icon} />;
+    }
+    return icon;
+  }
+
+  render() {
+    const { props } = this;
+    const spacer = props.spacing === 'small' ? '3' : '6';
+
+    const before = !props.iconBefore ? null : (
+      <span className={`flex-child mr${spacer}`}>
+        {this.renderIcon(props.iconBefore)}
+      </span>
+    );
+
+    const after = !props.iconAfter ? null : (
+      <span className={`flex-child ml${spacer}`}>
+        {this.renderIcon(props.iconAfter)}
+      </span>
+    );
+
+    return (
+      <span className="flex-parent flex-parent--center-cross">
+        {before}
+        <span className="flex-child">{props.children}</span>
+        {after}
+      </span>
+    );
+  }
+}
+
+MbxIconText.propTypes = {
+  /**
+   * The text. A string is recommended, but you can put an element in here if you think it's right.
+   */
+  children: PropTypes.node.isRequired,
+  /**
+   * The spacing size: `"small"` or `"large"`.
+   */
+  spacing: PropTypes.oneOf(['small', 'large']),
+  /**
+   * An icon to place before the text. If the value is a string, it should name an
+   * Assembly icon. If you bring your own SVG or want finer-grained control over
+   * how the Icon component is used, pass an element.
+   */
+  iconBefore: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
+  /**
+   * An icon to place after the text. See details documented for `iconBefore`.
+   */
+  iconAfter: PropTypes.oneOfType([PropTypes.node, PropTypes.string])
+};
+
+MbxIconText.defaultProps = {
+  spacing: 'small'
+};
+
+export default MbxIconText;


Test patch:
diff --git a/src/components/mbx-icon-text/__tests__/__snapshots__/mbx-icon-text.test.js.snap b/src/components/mbx-icon-text/__tests__/__snapshots__/mbx-icon-text.test.js.snap
new file mode 100644
index 0000000..fcfad5f
--- /dev/null
+++ b/src/components/mbx-icon-text/__tests__/__snapshots__/mbx-icon-text.test.js.snap
@@ -0,0 +1,89 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`icon after renders as expected 1`] = `
+<span
+  className="flex-parent flex-parent--center-cross"
+>
+  <span
+    className="flex-child"
+  >
+    Next
+  </span>
+  <span
+    className="flex-child ml3"
+  >
+    <Icon
+      inline={false}
+      name="chevron-right"
+    />
+  </span>
+</span>
+`;
+
+exports[`icon before renders as expected 1`] = `
+<span
+  className="flex-parent flex-parent--center-cross"
+>
+  <span
+    className="flex-child mr3"
+  >
+    <Icon
+      inline={false}
+      name="clipboard"
+    />
+  </span>
+  <span
+    className="flex-child"
+  >
+    Copy
+  </span>
+</span>
+`;
+
+exports[`icons on both sides, including a non-string one renders as expected 1`] = `
+<span
+  className="flex-parent flex-parent--center-cross"
+>
+  <span
+    className="flex-child mr3"
+  >
+    <Icon
+      inline={false}
+      name="check"
+    />
+  </span>
+  <span
+    className="flex-child"
+  >
+    What
+  </span>
+  <span
+    className="flex-child ml3"
+  >
+    <Icon
+      inline={false}
+      name="github"
+    />
+  </span>
+</span>
+`;
+
+exports[`large spacing renders as expected 1`] = `
+<span
+  className="flex-parent flex-parent--center-cross"
+>
+  <span
+    className="flex-child mr6"
+  >
+    <Icon
+      inline={false}
+      name="check"
+    />
+  </span>
+  <span
+    className="flex-child"
+  >
+    Done
+  </span>
+</span>
+`;
diff --git a/src/components/mbx-icon-text/__tests__/mbx-icon-text-test-cases.js b/src/components/mbx-icon-text/__tests__/mbx-icon-text-test-cases.js
new file mode 100644
index 0000000..986cb41
--- /dev/null
+++ b/src/components/mbx-icon-text/__tests__/mbx-icon-text-test-cases.js
@@ -0,0 +1,54 @@
+import React from 'react';
+import MbxIconText from '../mbx-icon-text';
+import Icon from '../../icon';
+
+const testCases = {};
+
+testCases.iconBefore = {
+  description: 'icon before',
+  component: MbxIconText,
+  props: {
+    children: 'Copy',
+    iconBefore: 'clipboard'
+  }
+};
+
+testCases.iconAfter = {
+  description: 'icon after',
+  component: MbxIconText,
+  props: {
+    children: 'Next',
+    iconAfter: 'chevron-right'
+  }
+};
+
+testCases.largeSpacing = {
+  description: 'large spacing',
+  component: MbxIconText,
+  props: {
+    children: 'Done',
+    iconBefore: 'check',
+    spacing: 'large'
+  }
+};
+
+testCases.bothIcons = {
+  description: 'icons on both sides, including a non-string one',
+  component: MbxIconText,
+  props: {
+    children: 'What',
+    iconBefore: 'check',
+    iconAfter: <Icon name="github" />
+  }
+};
+
+testCases.buttonyDisplay = {
+  description: 'a buttony look',
+  element: (
+    <button type="button" className="link txt-bold txt-s block">
+      <MbxIconText iconBefore="duplicate">Duplicate</MbxIconText>
+    </button>
+  )
+};
+
+export { testCases };
diff --git a/src/components/mbx-icon-text/__tests__/mbx-icon-text.test.js b/src/components/mbx-icon-text/__tests__/mbx-icon-text.test.js
new file mode 100644
index 0000000..4f8234f
--- /dev/null
+++ b/src/components/mbx-icon-text/__tests__/mbx-icon-text.test.js
@@ -0,0 +1,50 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { testCases } from './mbx-icon-text-test-cases';
+
+let testCase;
+let wrapper;
+
+describe(testCases.iconBefore.description, () => {
+  beforeEach(() => {
+    testCase = testCases.iconBefore;
+    wrapper = shallow(React.createElement(testCase.component, testCase.props));
+  });
+
+  test('renders as expected', () => {
+    expect(wrapper).toMatchSnapshot();
+  });
+});
+
+describe(testCases.iconAfter.description, () => {
+  beforeEach(() => {
+    testCase = testCases.iconAfter;
+    wrapper = shallow(React.createElement(testCase.component, testCase.props));
+  });
+
+  test('renders as expected', () => {
+    expect(wrapper).toMatchSnapshot();
+  });
+});
+
+describe(testCases.largeSpacing.description, () => {
+  beforeEach(() => {
+    testCase = testCases.largeSpacing;
+    wrapper = shallow(React.createElement(testCase.component, testCase.props));
+  });
+
+  test('renders as expected', () => {
+    expect(wrapper).toMatchSnapshot();
+  });
+});
+
+describe(testCases.bothIcons.description, () => {
+  beforeEach(() => {
+    testCase = testCases.bothIcons;
+    wrapper = shallow(React.createElement(testCase.component, testCase.props));
+  });
+
+  test('renders as expected', () => {
+    expect(wrapper).toMatchSnapshot();
+  });
+});