diff --git a/src/Hyperlink/Hyperlink.test.jsx b/src/Hyperlink/Hyperlink.test.tsx
similarity index 60%
rename from src/Hyperlink/Hyperlink.test.jsx
rename to src/Hyperlink/Hyperlink.test.tsx
index 2d5ffd3c5e..597ba8e5c4 100644
--- a/src/Hyperlink/Hyperlink.test.jsx
+++ b/src/Hyperlink/Hyperlink.test.tsx
@@ -4,30 +4,48 @@ import userEvent from '@testing-library/user-event';
import Hyperlink from '.';
-const content = 'content';
const destination = 'destination';
+const content = 'content';
const onClick = jest.fn();
const props = {
- content,
destination,
onClick,
};
const externalLinkAlternativeText = 'externalLinkAlternativeText';
const externalLinkTitle = 'externalLinkTitle';
const externalLinkProps = {
- target: '_blank',
+ target: '_blank' as const,
externalLinkAlternativeText,
externalLinkTitle,
...props,
};
describe('correct rendering', () => {
+ beforeEach(() => {
+ onClick.mockClear();
+ });
+ it('renders with deprecated "content" prop', async () => {
+ // @ts-expect-error - we don't include 'content' in the types since it's deprecated. Change to 'children'.
+ const { getByRole } = render();
+ const wrapper = getByRole('link');
+ expect(wrapper).toBeInTheDocument();
+
+ expect(wrapper).toHaveClass('pgn__hyperlink');
+ expect(wrapper).toHaveTextContent('content');
+ expect(wrapper).toHaveAttribute('href', destination);
+ expect(wrapper).toHaveAttribute('target', '_self');
+
+ await userEvent.click(wrapper);
+ expect(onClick).toHaveBeenCalledTimes(1);
+ });
+
it('renders Hyperlink', async () => {
- const { getByRole } = render();
+ const { getByRole } = render({content});
const wrapper = getByRole('link');
expect(wrapper).toBeInTheDocument();
expect(wrapper).toHaveClass('pgn__hyperlink');
+ expect(wrapper).toHaveClass('standalone-link');
expect(wrapper).toHaveTextContent(content);
expect(wrapper).toHaveAttribute('href', destination);
expect(wrapper).toHaveAttribute('target', '_self');
@@ -36,8 +54,17 @@ describe('correct rendering', () => {
expect(onClick).toHaveBeenCalledTimes(1);
});
+ it('renders an underlined Hyperlink', async () => {
+ const { getByRole } = render({content});
+ const wrapper = getByRole('link');
+ expect(wrapper).toBeInTheDocument();
+ expect(wrapper).toHaveClass('pgn__hyperlink');
+ expect(wrapper).not.toHaveClass('standalone-link');
+ expect(wrapper).toHaveClass('inline-link');
+ });
+
it('renders external Hyperlink', () => {
- const { getByRole, getByTestId } = render();
+ const { getByRole, getByTestId } = render({content});
const wrapper = getByRole('link');
const icon = getByTestId('hyperlink-icon');
const iconSvg = icon.querySelector('svg');
@@ -53,18 +80,16 @@ describe('correct rendering', () => {
describe('security', () => {
it('prevents reverse tabnabbing for links with target="_blank"', () => {
- const { getByRole } = render();
+ const { getByRole } = render({content});
const wrapper = getByRole('link');
expect(wrapper).toHaveAttribute('rel', 'noopener noreferrer');
});
});
describe('event handlers are triggered correctly', () => {
- let spy;
- beforeEach(() => { spy = jest.fn(); });
-
it('should fire onClick', async () => {
- const { getByRole } = render();
+ const spy = jest.fn();
+ const { getByRole } = render({content});
const wrapper = getByRole('link');
expect(spy).toHaveBeenCalledTimes(0);
await userEvent.click(wrapper);
diff --git a/src/Hyperlink/index.jsx b/src/Hyperlink/index.tsx
similarity index 59%
rename from src/Hyperlink/index.jsx
rename to src/Hyperlink/index.tsx
index 7c4a61f882..efe7fd3b92 100644
--- a/src/Hyperlink/index.jsx
+++ b/src/Hyperlink/index.tsx
@@ -1,7 +1,7 @@
+/* eslint-disable react/require-default-props */
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import isRequiredIf from 'react-proptype-conditional-require';
import { Launch } from '../../icons';
import Icon from '../Icon';
@@ -10,20 +10,39 @@ import withDeprecatedProps, { DeprTypes } from '../withDeprecatedProps';
export const HYPER_LINK_EXTERNAL_LINK_ALT_TEXT = 'in a new tab';
export const HYPER_LINK_EXTERNAL_LINK_TITLE = 'Opens in a new tab';
-const Hyperlink = React.forwardRef((props, ref) => {
- const {
- className,
- destination,
- children,
- target,
- onClick,
- externalLinkAlternativeText,
- externalLinkTitle,
- variant,
- isInline,
- showLaunchIcon,
- ...attrs
- } = props;
+interface Props extends Omit, 'href' | 'target'> {
+ /** specifies the URL */
+ destination: string;
+ /** Content of the hyperlink */
+ children: React.ReactNode;
+ /** Custom class names for the hyperlink */
+ className?: string;
+ /** Alt text for the icon indicating that this link opens in a new tab, if target="_blank". e.g. _("in a new tab") */
+ externalLinkAlternativeText?: string;
+ /** Tooltip text for the "opens in new tab" icon, if target="_blank". e.g. _("Opens in a new tab"). */
+ externalLinkTitle?: string;
+ /** type of hyperlink */
+ variant?: 'default' | 'muted' | 'brand';
+ /** Display the link with an underline. By default, it is only underlined on hover. */
+ isInline?: boolean;
+ /** specify if we need to show launch Icon. By default, it will be visible. */
+ showLaunchIcon?: boolean;
+ target?: '_blank' | '_self';
+}
+
+const Hyperlink = React.forwardRef(({
+ className,
+ destination,
+ children,
+ target = '_self',
+ onClick = () => {},
+ externalLinkAlternativeText = HYPER_LINK_EXTERNAL_LINK_ALT_TEXT,
+ externalLinkTitle = HYPER_LINK_EXTERNAL_LINK_TITLE,
+ variant = 'default',
+ isInline = false,
+ showLaunchIcon = true,
+ ...attrs
+}, ref) => {
let externalLinkIcon;
if (target === '_blank') {
@@ -83,17 +102,6 @@ const Hyperlink = React.forwardRef((props, ref) => {
);
});
-Hyperlink.defaultProps = {
- className: undefined,
- target: '_self',
- onClick: () => {},
- externalLinkAlternativeText: HYPER_LINK_EXTERNAL_LINK_ALT_TEXT,
- externalLinkTitle: HYPER_LINK_EXTERNAL_LINK_TITLE,
- variant: 'default',
- isInline: false,
- showLaunchIcon: true,
-};
-
Hyperlink.propTypes = {
/** specifies the URL */
destination: PropTypes.string.isRequired,
@@ -105,23 +113,18 @@ Hyperlink.propTypes = {
* loaded into the same browsing context as the current one.
* If the target is `_blank` (opening a new window) `rel='noopener'` will be added to the anchor tag to prevent
* any potential [reverse tabnabbing attack](https://www.owasp.org/index.php/Reverse_Tabnabbing).
- */
- target: PropTypes.string,
+ */
+ // @ts-ignore - some magic 'propTypes' decoding is clashing with our other types above, and won't accept '_blank' here
+ target: PropTypes.oneOf(['_blank', '_self']),
/** specifies the callback function when the link is clicked */
onClick: PropTypes.func,
- /** specifies the text for links with a `_blank` target (which loads the URL in a new browsing context). */
- externalLinkAlternativeText: isRequiredIf(
- PropTypes.string,
- props => props.target === '_blank',
- ),
- /** specifies the title for links with a `_blank` target (which loads the URL in a new browsing context). */
- externalLinkTitle: isRequiredIf(
- PropTypes.string,
- props => props.target === '_blank',
- ),
+ /** Alt text for the icon indicating that this link opens in a new tab, if target="_blank". e.g. _("in a new tab") */
+ externalLinkAlternativeText: PropTypes.string,
+ /** Tooltip text for the "opens in new tab" icon, if target="_blank". e.g. _("Opens in a new tab"). */
+ externalLinkTitle: PropTypes.string,
/** type of hyperlink */
variant: PropTypes.oneOf(['default', 'muted', 'brand']),
- /** specify the link style. By default, it will be underlined. */
+ /** Display the link with an underline. By default, it is only underlined on hover. */
isInline: PropTypes.bool,
/** specify if we need to show launch Icon. By default, it will be visible. */
showLaunchIcon: PropTypes.bool,
diff --git a/src/index.d.ts b/src/index.d.ts
index 9d71f85477..e4050de325 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -7,6 +7,7 @@
export { default as Bubble } from './Bubble';
export { default as Chip, CHIP_PGN_CLASS } from './Chip';
export { default as ChipCarousel } from './ChipCarousel';
+export { default as Hyperlink, HYPER_LINK_EXTERNAL_LINK_ALT_TEXT, HYPER_LINK_EXTERNAL_LINK_TITLE } from './Hyperlink';
export { default as Icon } from './Icon';
// // // // // // // // // // // // // // // // // // // // // // // // // // //
@@ -72,7 +73,6 @@ export const
FormAutosuggestOption: any,
InputGroup: any;
// from './Form';
-export const Hyperlink: any, HYPER_LINK_EXTERNAL_LINK_ALT_TEXT: string, HYPER_LINK_EXTERNAL_LINK_TITLE: string; // from './Hyperlink';
export const IconButton: any, IconButtonWithTooltip: any; // from './IconButton';
export const IconButtonToggle: any; // from './IconButtonToggle';
export const Input: any; // from './Input';
diff --git a/src/index.js b/src/index.js
index 6e8b9294c5..2f1b794be4 100644
--- a/src/index.js
+++ b/src/index.js
@@ -7,6 +7,7 @@
export { default as Bubble } from './Bubble';
export { default as Chip, CHIP_PGN_CLASS } from './Chip';
export { default as ChipCarousel } from './ChipCarousel';
+export { default as Hyperlink, HYPER_LINK_EXTERNAL_LINK_ALT_TEXT, HYPER_LINK_EXTERNAL_LINK_TITLE } from './Hyperlink';
export { default as Icon } from './Icon';
// // // // // // // // // // // // // // // // // // // // // // // // // // //
@@ -72,7 +73,6 @@ export {
FormAutosuggestOption,
InputGroup,
} from './Form';
-export { default as Hyperlink, HYPER_LINK_EXTERNAL_LINK_ALT_TEXT, HYPER_LINK_EXTERNAL_LINK_TITLE } from './Hyperlink';
export { default as IconButton, IconButtonWithTooltip } from './IconButton';
export { default as IconButtonToggle } from './IconButtonToggle';
export { default as Input } from './Input';