diff --git a/.storybook/__snapshots__/Storyshots.test.js.snap b/.storybook/__snapshots__/Storyshots.test.js.snap
index c6b9056f48..7efc55493d 100644
--- a/.storybook/__snapshots__/Storyshots.test.js.snap
+++ b/.storybook/__snapshots__/Storyshots.test.js.snap
@@ -443,6 +443,18 @@ exports[`Storyshots HyperLink with blank target 1`] = `
`;
+exports[`Storyshots HyperLink with icon as content 1`] = `
+
+
+
+`;
+
exports[`Storyshots HyperLink with onClick 1`] = `
`;
+exports[`Storyshots MailtoLink minimal usage 1`] = `
+
+ edx@example.com
+
+`;
+
+exports[`Storyshots MailtoLink with blank target 1`] = `
+
+ edx@example.com
+
+
+
+
+
+`;
+
+exports[`Storyshots MailtoLink with cc and bcc 1`] = `
+
+ Moar mail, this time with cc and bcc
+
+`;
+
+exports[`Storyshots MailtoLink with multiple cc and bcc 1`] = `
+
+ edx@example.com
+
+`;
+
+exports[`Storyshots MailtoLink with multiple recipients and mail icon 1`] = `
+
+
+
+`;
+
+exports[`Storyshots MailtoLink with onClick 1`] = `
+
+ edx@example.com
+
+
+
+
+
+`;
+
+exports[`Storyshots MailtoLink with subject and body 1`] = `
+
+ email with subject and body
+
+`;
+
exports[`Storyshots Modal basic usage 1`] = `
+ ))
+ .add('with icon as content', () => (
+ )}
+ />
));
diff --git a/src/Hyperlink/README.md b/src/Hyperlink/README.md
new file mode 100644
index 0000000000..2dc24f42d4
--- /dev/null
+++ b/src/Hyperlink/README.md
@@ -0,0 +1,31 @@
+# Hyperlink
+
+## API
+
+### `content` (string or element; required)
+
+`content` specifies the text or element that a URL should be associated with
+
+### `destination` (string; required)
+
+`destination` specifies the URL
+
+### `target` (string; optional)
+
+`target` specifies where the link should open. The default behavior is `_self`, which means that the URL will be loaded into the same browsing context as the current one
+
+### `onClick` (function; optional)
+
+`onClick` specifies the callback function when the link is clicked
+
+### `externalLinkAlternativeText` (string; optional)
+
+`externalLinkAlternativeText` specifies the text for links with a `_blank` target (which loads the URL in a new browsing context).
+
+**This value is required if the `target` value is `_blank`**
+
+### `externalLinkTitle` (string; optional)
+
+`externalLinkTitle` specifies the title for links with a `_blank` target (which loads the URL in a new browsing context).
+
+**This value is required if the `target` value is `_blank`**
\ No newline at end of file
diff --git a/src/Hyperlink/index.jsx b/src/Hyperlink/index.jsx
index 9da42a4286..6742831a8c 100644
--- a/src/Hyperlink/index.jsx
+++ b/src/Hyperlink/index.jsx
@@ -51,7 +51,7 @@ Hyperlink.defaultProps = {
Hyperlink.propTypes = {
destination: PropTypes.string.isRequired,
- content: PropTypes.string.isRequired,
+ content: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
target: PropTypes.string,
onClick: PropTypes.func,
externalLinkAlternativeText: isRequiredIf(PropTypes.string, props => props.target === '_blank'),
diff --git a/src/MailtoLink/MailtoLink.stories.jsx b/src/MailtoLink/MailtoLink.stories.jsx
new file mode 100644
index 0000000000..98a2010b04
--- /dev/null
+++ b/src/MailtoLink/MailtoLink.stories.jsx
@@ -0,0 +1,68 @@
+/* eslint-disable import/no-extraneous-dependencies, no-console */
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { action } from '@storybook/addon-actions';
+import { setConsoleOptions } from '@storybook/addon-console';
+
+import MailtoLink from './index';
+
+setConsoleOptions({
+ panelExclude: ['warn', 'error'],
+});
+
+const onClick = (event) => {
+ console.log(`onClick fired for ${event.target}`);
+
+ action('MailtoLink Click');
+};
+
+storiesOf('MailtoLink', module)
+ .add('minimal usage', () => (
+
+ ))
+ .add('with blank target', () => (
+
+ ))
+ .add('with onClick', () => (
+
+ ))
+ .add('with multiple recipients and mail icon', () => (
+ )}
+ />
+ ))
+ .add('with subject and body', () => (
+
+ ))
+ .add('with cc and bcc', () => (
+
+ ))
+ .add('with multiple cc and bcc', () => (
+
+ ));
diff --git a/src/MailtoLink/MailtoLink.test.jsx b/src/MailtoLink/MailtoLink.test.jsx
new file mode 100644
index 0000000000..0fd34fa937
--- /dev/null
+++ b/src/MailtoLink/MailtoLink.test.jsx
@@ -0,0 +1,49 @@
+/* eslint-disable import/no-extraneous-dependencies */
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import MailtoLink from './index';
+
+const emailAddress = 'edx@example.com';
+const emailAddresses = ['foo@example.com', 'bar@example.com', 'baz@example.com'];
+const subject = 'subject';
+const body = 'body';
+const content = 'content';
+
+const baseProps = { subject, body, content };
+
+describe('correct rendering', () => {
+ it('renders MailtoLink with single to, cc, and bcc recipient', () => {
+ const singleRecipientLink = (
+
+ );
+ const wrapper = shallow(singleRecipientLink);
+
+ expect(wrapper.prop('href')).toEqual('mailto:edx@example.com?bcc=edx%40example.com&body=body&cc=edx%40example.com&subject=subject');
+ });
+
+ it('renders mailtoLink with many to, cc, and bcc recipients', () => {
+ const multiRecipientLink = (
+
+ );
+ const wrapper = shallow(multiRecipientLink);
+
+ expect(wrapper.prop('href')).toEqual('mailto:foo@example.com,bar@example.com,baz@example.com?bcc=foo%40example.com%2Cbar%40example.com%2Cbaz%40example.com&body=body&cc=foo%40example.com%2Cbar%40example.com%2Cbaz%40example.com&subject=subject');
+ });
+
+ it('renders empty mailtoLink', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.prop('href')).toEqual('mailto:');
+ });
+});
diff --git a/src/MailtoLink/README.md b/src/MailtoLink/README.md
new file mode 100644
index 0000000000..a0af6a67ea
--- /dev/null
+++ b/src/MailtoLink/README.md
@@ -0,0 +1,43 @@
+# MailtoLink
+
+A thin wrapper around the `Hyperlink` component that abstracts the creation of the `destination` value given a set of recipients, a subject, and a body.
+
+## API
+
+### `content` (string or element; required)
+
+`content` specifies the text or element that a URL should be associated with
+
+### `to` (string or string array; optional)
+
+`to` specifies the email's recipients
+
+### `cc` (string or string array; optional)
+
+`cc` specifies the email's carbon copy recipients
+
+### `bcc` (string or string array; optional)
+
+`bcc` specifies the email's blind carbon copy recipients
+
+### `subject` (string; optional)
+
+`subject` specifies the email's subject
+
+### `body` (string; optional)
+
+`body` specifies the email's body
+
+### `target` (string; optional)
+
+`target` specifies where the link should open. The default behavior is `_self`, which means that the URL will be loaded into the same browsing context as the current one
+
+### `onClick` (function; optional)
+
+`onClick` specifies the callback function when the link is clicked
+
+### `externalLink` (shape; optional)
+
+The `externalLink` object contains the `alternativeText` and `title` fields which specify the text and title for links with a `_blank` target (which loads the URL in a new browsing context).
+
+**This object is required if the `target` value is `_blank`**
diff --git a/src/MailtoLink/index.jsx b/src/MailtoLink/index.jsx
new file mode 100644
index 0000000000..56c62046b3
--- /dev/null
+++ b/src/MailtoLink/index.jsx
@@ -0,0 +1,69 @@
+import React from 'react'; // eslint-disable-line no-unused-vars
+import PropTypes from 'prop-types';
+import emailPropType from 'email-prop-type';
+import isRequiredIf from 'react-proptype-conditional-require';
+import mailtoLink from 'mailto-link';
+
+import Hyperlink from '../Hyperlink';
+
+const MailtoLink = (props) => {
+ const {
+ to,
+ cc,
+ bcc,
+ subject,
+ body,
+ content,
+ target,
+ onClick,
+ externalLink,
+ ...other
+ } = props;
+
+ const externalLinkAlternativeText = externalLink.alternativeLink;
+ const externalLinkTitle = externalLink.title;
+ const destination = mailtoLink({
+ to, cc, bcc, subject, body,
+ });
+
+ return Hyperlink({
+ destination,
+ content,
+ target,
+ onClick,
+ externalLinkAlternativeText,
+ externalLinkTitle,
+ ...other,
+ });
+};
+
+MailtoLink.defaultProps = {
+ to: [],
+ cc: [],
+ bcc: [],
+ subject: '',
+ body: '',
+ target: '_self',
+ onClick: null,
+ externalLink: {
+ alternativeText: 'Opens in a new window',
+ title: 'Opens in a new window',
+ },
+};
+
+MailtoLink.propTypes = {
+ content: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
+ to: PropTypes.oneOfType([PropTypes.arrayOf(emailPropType), emailPropType]),
+ cc: PropTypes.oneOfType([PropTypes.arrayOf(emailPropType), emailPropType]),
+ bcc: PropTypes.oneOfType([PropTypes.arrayOf(emailPropType), emailPropType]),
+ subject: PropTypes.string,
+ body: PropTypes.string,
+ target: PropTypes.string,
+ onClick: PropTypes.func,
+ externalLink: PropTypes.shape({
+ alternativeText: isRequiredIf(PropTypes.string, props => props.target === '_blank'),
+ title: isRequiredIf(PropTypes.string, props => props.target === '_blank'),
+ }),
+};
+
+export default MailtoLink;
diff --git a/src/index.js b/src/index.js
index a1d7f64051..73096b2fc3 100644
--- a/src/index.js
+++ b/src/index.js
@@ -6,6 +6,7 @@ import Dropdown from './Dropdown';
import Hyperlink from './Hyperlink';
import InputSelect from './InputSelect';
import InputText from './InputText';
+import MailtoLink from './MailtoLink';
import Modal from './Modal';
import RadioButtonGroup, { RadioButton } from './RadioButtonGroup';
import StatusAlert from './StatusAlert';
@@ -23,6 +24,7 @@ export {
Hyperlink,
InputSelect,
InputText,
+ MailtoLink,
Modal,
RadioButtonGroup,
RadioButton,