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,