diff --git a/.storybook/__snapshots__/Storyshots.test.js.snap b/.storybook/__snapshots__/Storyshots.test.js.snap
index d9d58e9bcb..19cb5ac69b 100644
--- a/.storybook/__snapshots__/Storyshots.test.js.snap
+++ b/.storybook/__snapshots__/Storyshots.test.js.snap
@@ -133,6 +133,54 @@ exports[`Storyshots Dropdown basic usage 1`] = `
`;
+exports[`Storyshots HyperLink minimal usage 1`] = `
+
+ edX.org
+
+`;
+
+exports[`Storyshots HyperLink with blank target 1`] = `
+
+ edX.org
+
+
+
+
+
+`;
+
+exports[`Storyshots HyperLink with onClick 1`] = `
+
+ edX.org
+
+
+
+
+
+`;
+
exports[`Storyshots InputSelect basic usage 1`] = `
{
+ console.log(`onClick fired for ${event.target}`);
+
+ action('HyperLink Click');
+};
+
+storiesOf('HyperLink', module)
+ .add('minimal usage', () => (
+
+ ))
+ .add('with blank target', () => (
+
+ ))
+ .add('with onClick', () => (
+
+ ));
diff --git a/src/Hyperlink/Hyperlink.test.jsx b/src/Hyperlink/Hyperlink.test.jsx
new file mode 100644
index 0000000000..09c416ddbb
--- /dev/null
+++ b/src/Hyperlink/Hyperlink.test.jsx
@@ -0,0 +1,67 @@
+/* eslint-disable import/no-extraneous-dependencies */
+import React from 'react';
+import { shallow, mount } from 'enzyme';
+import classNames from 'classnames';
+import FontAwesomeStyles from 'font-awesome/css/font-awesome.min.css';
+
+import Hyperlink from './index';
+
+const content = 'content';
+const destination = 'destination';
+const onClick = () => {};
+const props = {
+ content,
+ destination,
+ onClick,
+};
+const externalLinkAlternativeText = 'externalLinkAlternativeText';
+const externalLinkTitle = 'externalLinktTitle';
+const externalLinkProps = {
+ target: '_blank',
+ externalLinkAlternativeText,
+ externalLinkTitle,
+ ...props,
+};
+
+describe('correct rendering', () => {
+ it('renders Hyperlink', () => {
+ const wrapper = shallow(
);
+ expect(wrapper.type()).toEqual('a');
+ expect(wrapper).toHaveLength(1);
+
+ expect(wrapper.prop('children')).toEqual([content, undefined]);
+ expect(wrapper.prop('href')).toEqual(destination);
+ expect(wrapper.prop('target')).toEqual('_self');
+ expect(wrapper.prop('onClick')).toEqual(onClick);
+
+ expect(wrapper.find('span')).toHaveLength(0);
+ expect(wrapper.find('i')).toHaveLength(0);
+ });
+
+ it('renders external Hyperlink', () => {
+ const wrapper = mount(
);
+
+ expect(wrapper.find('span')).toHaveLength(2);
+
+ const icon = wrapper.find('span').at(1);
+
+ expect(icon.prop('aria-hidden')).toEqual(false);
+ expect(icon.prop('className'))
+ .toEqual(classNames(FontAwesomeStyles.fa, FontAwesomeStyles['fa-external-link']));
+ expect(icon.prop('aria-label')).toEqual(externalLinkAlternativeText);
+ expect(icon.prop('title')).toEqual(externalLinkTitle);
+ });
+});
+
+describe('event handlers are triggered correctly', () => {
+ let spy;
+
+ beforeEach(() => { spy = jest.fn(); });
+
+ it('should fire onClick', () => {
+ const wrapper = mount(
);
+ expect(spy).toHaveBeenCalledTimes(0);
+ wrapper.simulate('click');
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/Hyperlink/index.jsx b/src/Hyperlink/index.jsx
new file mode 100644
index 0000000000..9da42a4286
--- /dev/null
+++ b/src/Hyperlink/index.jsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import classNames from 'classnames';
+import FontAwesomeStyles from 'font-awesome/css/font-awesome.min.css';
+import PropTypes from 'prop-types';
+import isRequiredIf from 'react-proptype-conditional-require';
+
+function Hyperlink(props) {
+ const {
+ destination,
+ content,
+ target,
+ onClick,
+ externalLinkAlternativeText,
+ externalLinkTitle,
+ ...other
+ } = props;
+
+ let externalLinkIcon;
+
+ if (target === '_blank') {
+ externalLinkIcon = (
+ // Space between content and icon
+
{' '}
+
+
+ );
+ }
+
+ return (
+
{content}{externalLinkIcon}
+
+ );
+}
+
+Hyperlink.defaultProps = {
+ target: '_self',
+ onClick: () => {},
+ externalLinkAlternativeText: 'Opens in a new window',
+ externalLinkTitle: 'Opens in a new window',
+};
+
+Hyperlink.propTypes = {
+ destination: PropTypes.string.isRequired,
+ content: PropTypes.string.isRequired,
+ target: PropTypes.string,
+ onClick: PropTypes.func,
+ externalLinkAlternativeText: isRequiredIf(PropTypes.string, props => props.target === '_blank'),
+ externalLinkTitle: isRequiredIf(PropTypes.string, props => props.target === '_blank'),
+};
+
+export default Hyperlink;