From 2313f307d401689215eca0521a39eacf5ae5d175 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Wed, 22 May 2024 06:00:15 -0700 Subject: [PATCH] feat: Add TypeScript types for (#3077) --- .eslintrc.js | 2 +- package.json | 4 +- ...{Hyperlink.test.jsx => Hyperlink.test.tsx} | 31 +++++--- src/Hyperlink/{index.jsx => index.tsx} | 78 ++++++++++--------- src/index.d.ts | 2 +- src/index.js | 2 +- src/{setupTest.js => setupTest.ts} | 5 ++ 7 files changed, 72 insertions(+), 52 deletions(-) rename src/Hyperlink/{Hyperlink.test.jsx => Hyperlink.test.tsx} (74%) rename src/Hyperlink/{index.jsx => index.tsx} (63%) rename src/{setupTest.js => setupTest.ts} (62%) diff --git a/.eslintrc.js b/.eslintrc.js index cc55b4443c..aaf4750116 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -28,7 +28,7 @@ module.exports = { { devDependencies: [ '**/*.stories.jsx', - 'src/setupTest.js', + 'src/setupTest.ts', '**/*.test.jsx', '**/*.test.js', 'config/*.js', diff --git a/package.json b/package.json index d85f25b581..d5054c9267 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,7 @@ "^.+\\.tsx?$": "ts-jest" }, "setupFilesAfterEnv": [ - "./src/setupTest.js" + "./src/setupTest.ts" ], "moduleNameMapper": { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", @@ -180,7 +180,7 @@ ], "coveragePathIgnorePatterns": [ "/node_modules/", - "src/setupTest.js", + "src/setupTest.ts", "src/index.js", "/tests/", "/www/", diff --git a/src/Hyperlink/Hyperlink.test.jsx b/src/Hyperlink/Hyperlink.test.tsx similarity index 74% rename from src/Hyperlink/Hyperlink.test.jsx rename to src/Hyperlink/Hyperlink.test.tsx index 2d5ffd3c5e..3982cc6fa6 100644 --- a/src/Hyperlink/Hyperlink.test.jsx +++ b/src/Hyperlink/Hyperlink.test.tsx @@ -4,30 +4,34 @@ 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 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 +40,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 +66,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 63% rename from src/Hyperlink/index.jsx rename to src/Hyperlink/index.tsx index 7c4a61f882..5229f73f8f 100644 --- a/src/Hyperlink/index.jsx +++ b/src/Hyperlink/index.tsx @@ -1,29 +1,45 @@ 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'; -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, + onClick, + externalLinkAlternativeText, + externalLinkTitle, + variant, + isInline, + showLaunchIcon, + ...attrs +}, ref) => { let externalLinkIcon; if (target === '_blank') { @@ -105,32 +121,20 @@ 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, + */ + 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, }; -export default withDeprecatedProps(Hyperlink, 'Hyperlink', { - /** specifies the text or element that a URL should be associated with */ - content: { - deprType: DeprTypes.MOVED, - newName: 'children', - }, -}); +export default Hyperlink; 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 00bb9b6458..1b28bb4c94 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'; // // // // // // // // // // // // // // // // // // // // // // // // // // // @@ -69,7 +70,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 Image, Figure } from './Image'; diff --git a/src/setupTest.js b/src/setupTest.ts similarity index 62% rename from src/setupTest.js rename to src/setupTest.ts index 5d7527d861..acd5ba24da 100644 --- a/src/setupTest.js +++ b/src/setupTest.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-extraneous-dependencies */ import 'regenerator-runtime/runtime'; import '@testing-library/jest-dom'; @@ -17,3 +18,7 @@ class ResizeObserver { } window.ResizeObserver = ResizeObserver; + +(window as any).crypto = { + getRandomValues: (arr: any) => crypto.randomBytes(arr.length), +};