Skip to content

Commit

Permalink
chore: prop types checker for destination hyperlink prop
Browse files Browse the repository at this point in the history
  • Loading branch information
adamstankiewicz committed Jan 29, 2025
1 parent 765b95e commit cf38ed0
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 16 deletions.
37 changes: 34 additions & 3 deletions src/Hyperlink/Hyperlink.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import React from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import Hyperlink from '.';
import Hyperlink, { HyperlinkProps } from '.';

const destination = 'destination';
const destination = 'http://destination.example';
const content = 'content';
const onClick = jest.fn();
const props = {
Expand All @@ -20,9 +20,25 @@ const externalLinkProps = {
...props,
};

interface LinkProps extends HyperlinkProps {
to: string;
}

function Link({ to, children, ...rest }: LinkProps) {
return (
<a
data-testid="custom-hyperlink-element"
href={to}
{...rest}
>
{children}
</a>
);
}

describe('correct rendering', () => {
beforeEach(() => {
onClick.mockClear();
jest.clearAllMocks();
});

it('renders Hyperlink', async () => {
Expand All @@ -40,6 +56,21 @@ describe('correct rendering', () => {
expect(onClick).toHaveBeenCalledTimes(1);
});

it('renders with custom element type via "as" prop', () => {
const propsWithoutDestination = {
to: destination, // `to` simulates common `Link` components' prop
};
const { getByRole } = render(<Hyperlink as={Link} {...propsWithoutDestination}>{content}</Hyperlink>);
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');
});

it('renders an underlined Hyperlink', async () => {
const { getByRole } = render(<Hyperlink isInline {...props}>{content}</Hyperlink>);
const wrapper = getByRole('link');
Expand Down
4 changes: 1 addition & 3 deletions src/Hyperlink/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,13 @@ notes: |
</div>
```

## with custom link element
## with custom link element (e.g., using a router)

``Hyperlink`` typically relies on the standard HTML anchor tag (i.e., ``a``); however, this behavior may be overriden when the destination link is to an internal route where it should be using routing instead (e.g., ``Link`` from React Router).

```jsx live
<Hyperlink
as={GatsbyLink}
// `destination` is still a required prop even though the `to` takes precedence.
destination="/components/button"
to="/components/button"
>
Button
Expand Down
33 changes: 23 additions & 10 deletions src/Hyperlink/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { forwardRef } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {
Expand All @@ -7,13 +7,15 @@ import {
} from 'react-bootstrap/esm/helpers';
import { Launch } from '../../icons';
import Icon from '../Icon';
// @ts-ignore
import { customPropTypeRequirement } from '../utils/propTypes/utils';

export const HYPER_LINK_EXTERNAL_LINK_ALT_TEXT = 'in a new tab';
export const HYPER_LINK_EXTERNAL_LINK_TITLE = 'Opens in a new tab';

interface HyperlinkProps extends BsPrefixProps, Omit<React.ComponentPropsWithRef<'a'>, 'href' | 'target'> {
export interface HyperlinkProps extends BsPrefixProps, Omit<React.ComponentPropsWithRef<'a'>, 'href' | 'target'> {
/** specifies the URL */
destination: string;
destination?: string;
/** Content of the hyperlink */
children: React.ReactNode;
/** Custom class names for the hyperlink */
Expand All @@ -31,9 +33,9 @@ interface HyperlinkProps extends BsPrefixProps, Omit<React.ComponentPropsWithRef
target?: '_blank' | '_self';
}

type HyperlinkType = ComponentWithAsProp<'a', HyperlinkProps>;
export type HyperlinkType = ComponentWithAsProp<'a', HyperlinkProps>;

const Hyperlink: HyperlinkType = React.forwardRef<HTMLAnchorElement, HyperlinkProps>(({
const Hyperlink: HyperlinkType = forwardRef<HTMLAnchorElement, HyperlinkProps>(({

Check failure on line 38 in src/Hyperlink/index.tsx

View workflow job for this annotation

GitHub Actions / tests

Type 'ForwardRefExoticComponent<Omit<HyperlinkProps, "ref"> & RefAttributes<HTMLAnchorElement>>' is not assignable to type 'HyperlinkType'.
as: Component = 'a',
className,
destination,
Expand Down Expand Up @@ -83,6 +85,11 @@ const Hyperlink: HyperlinkType = React.forwardRef<HTMLAnchorElement, HyperlinkPr
}
}

const additionalProps: Record<string, any> = { ...attrs };
if (destination) {
additionalProps.href = destination;
}

return (
<Component
ref={ref}
Expand All @@ -95,10 +102,9 @@ const Hyperlink: HyperlinkType = React.forwardRef<HTMLAnchorElement, HyperlinkPr
},
className,
)}
href={destination}
target={target}
onClick={onClick}
{...attrs}
{...additionalProps}
>
{children}
{externalLinkIcon}
Expand All @@ -108,9 +114,10 @@ const Hyperlink: HyperlinkType = React.forwardRef<HTMLAnchorElement, HyperlinkPr

Hyperlink.defaultProps = {
as: 'a',
destination: undefined,
className: undefined,
target: '_self',
onClick: () => {},
onClick: undefined,
externalLinkAlternativeText: HYPER_LINK_EXTERNAL_LINK_ALT_TEXT,
externalLinkTitle: HYPER_LINK_EXTERNAL_LINK_TITLE,
variant: 'default',
Expand All @@ -121,8 +128,14 @@ Hyperlink.defaultProps = {
Hyperlink.propTypes = {
/** specifies the component element type to render for the hyperlink */
as: PropTypes.elementType,
/** specifies the URL */
destination: PropTypes.string.isRequired,
/** specifies the URL; required iff `as` prop is a standard anchor tag */
destination: customPropTypeRequirement(
PropTypes.string,
({ as }: Partial<HyperlinkProps>) => as && as === 'a',
// "[`destination` is required when]..."
'the `as` prop is a standard anchor element (i.e., "a")',
),
// destination: PropTypes.string.isRequired,
/** Content of the hyperlink */
children: PropTypes.node.isRequired,
/** Custom class names for the hyperlink */
Expand Down

0 comments on commit cf38ed0

Please sign in to comment.