Skip to content

Commit

Permalink
feat!: add link atomic component
Browse files Browse the repository at this point in the history
- Added Link.tsx and Link.types.d.ts.
  - Wrapped MuiLink in new Link component.
- Added test suite for link, Link.test.tsx.
  • Loading branch information
ishaan bhalla authored and ishaan bhalla committed Oct 11, 2024
1 parent a605ffd commit 983150e
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 0 deletions.
61 changes: 61 additions & 0 deletions src/components/atoms/Link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use client';

import { LinkProps } from '@components/atoms/Link';
import { Link as MuiLink } from '@mui/material';
import React from 'react';

const Link: React.FC<LinkProps> = ({
href,
color = 'primary',
underline = 'always',
variant = 'inherit',
target,
rel,
onClick,
sx = {},
tabIndex = 0,
children,
ariaLabel,
role,
component,
...props
}) => {
if (!href && component !== 'button') {
throw new Error(
'The `href` prop is required unless `component="button"` is used.'
);
}

const computedRel =
target === '_blank'
? rel
? `${rel} noopener noreferrer`
: 'noopener noreferrer'
: rel;

const linkProps = {
...(component !== 'button' && { href }),
target,
rel: computedRel,
onClick,
'aria-label': ariaLabel,
role,
tabIndex,
component,
};

return (
<MuiLink
color={color}
sx={sx}
underline={underline}
variant={variant}
{...linkProps}
{...props}
>
{children}
</MuiLink>
);
};

export default Link;
49 changes: 49 additions & 0 deletions src/types/Link.types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
declare module '@components/atoms/Link' {
import { ElementType, FC, ReactNode } from 'react';
import { SxProps, Theme } from '@mui/system';
import { LinkProps as MuiLinkProps } from '@mui/material/Link';

export interface LinkProps
extends Omit<
MuiLinkProps,
'color' | 'variant' | 'underline' | 'href' | 'onClick'
> {
href?: string;
color?:
| 'initial'
| 'inherit'
| 'primary'
| 'secondary'
| 'textPrimary'
| 'textSecondary'
| 'error';
underline?: 'none' | 'hover' | 'always';
variant?:
| 'inherit'
| 'h1'
| 'h2'
| 'h3'
| 'h4'
| 'h5'
| 'h6'
| 'subtitle1'
| 'subtitle2'
| 'body1'
| 'body2'
| 'caption'
| 'button'
| 'overline';
target?: React.HTMLAttributeAnchorTarget;
rel?: string;
onClick?: React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement>;
sx?: SxProps<Theme>;
tabIndex?: number;
children: ReactNode;
ariaLabel?: string;
role?: string;
component?: ElementType;
}

declare const Link: FC<LinkProps>;
export default Link;
}
121 changes: 121 additions & 0 deletions tests/components/atoms/Link.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import '@testing-library/jest-dom';
import { fireEvent, render, screen } from '@testing-library/react';
import Link from '../../../src/components/atoms/Link';
import React from 'react';

describe('Link component', () => {
const defaultProps = {
href: 'https://www.example.com',
children: 'Example Link',
};

it('renders the link with correct href and text', () => {
render(<Link {...defaultProps} />);
const linkElement = screen.getByRole('link', { name: 'Example Link' });
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', defaultProps.href);
});

it('includes rel="noopener noreferrer" when target="_blank"', () => {
render(<Link {...defaultProps} target="_blank" />);
const linkElement = screen.getByRole('link', { name: 'Example Link' });
expect(linkElement).toHaveAttribute('target', '_blank');
expect(linkElement).toHaveAttribute('rel', 'noopener noreferrer');
});

it('includes custom rel attribute when provided with target="_blank"', () => {
render(<Link {...defaultProps} rel="nofollow" target="_blank" />);
const linkElement = screen.getByRole('link', { name: 'Example Link' });
expect(linkElement).toHaveAttribute('rel', 'nofollow noopener noreferrer');
});

it('does not include rel when target is not "_blank"', () => {
render(<Link {...defaultProps} rel="nofollow" />);
const linkElement = screen.getByRole('link', { name: 'Example Link' });
expect(linkElement).toHaveAttribute('rel', 'nofollow');
});

it('handles onClick event', () => {
const handleClick = jest.fn();
render(
<Link {...defaultProps} onClick={handleClick}>
Clickable Link
</Link>
);
const linkElement = screen.getByRole('link', { name: 'Clickable Link' });
fireEvent.click(linkElement);
expect(handleClick).toHaveBeenCalledTimes(1);
});

it('renders as a button when component="button"', () => {
const handleClick = jest.fn();
render(
<Link component="button" onClick={handleClick}>
Button Link
</Link>
);
const buttonElement = screen.getByRole('button', { name: 'Button Link' });
expect(buttonElement.tagName).toBe('BUTTON');
expect(buttonElement).not.toHaveAttribute('href');
fireEvent.click(buttonElement);
expect(handleClick).toHaveBeenCalledTimes(1);
});

it('applies aria-label when provided', () => {
render(<Link {...defaultProps} ariaLabel="Custom Aria Label" />);
const linkElement = screen.getByLabelText('Custom Aria Label');
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', defaultProps.href);
});

it('does not have aria-label when not provided', () => {
render(<Link {...defaultProps} />);
const linkElement = screen.getByRole('link', { name: 'Example Link' });
expect(linkElement).not.toHaveAttribute('aria-label');
});

it('passes additional props to the underlying component', () => {
render(<Link {...defaultProps} data-testid="custom-link" />);
const linkElement = screen.getByTestId('custom-link');
expect(linkElement).toBeInTheDocument();
});

it('supports custom components via the "component" prop', () => {
const CustomComponent = React.forwardRef<
HTMLAnchorElement,
React.AnchorHTMLAttributes<HTMLAnchorElement>
>((props, ref) => (
<a data-testid="custom-component" ref={ref} {...props} />
));

CustomComponent.displayName = 'CustomComponent';

render(
<Link component={CustomComponent} href="/custom">
Custom Component Link
</Link>
);

const customElement = screen.getByTestId('custom-component');
expect(customElement).toBeInTheDocument();
expect(customElement).toHaveAttribute('href', '/custom');
expect(customElement).toHaveTextContent('Custom Component Link');
});

it('handles missing href when component is "button"', () => {
render(<Link component="button">Button without href</Link>);
const buttonElement = screen.getByRole('button', {
name: 'Button without href',
});
expect(buttonElement).toBeInTheDocument();
expect(buttonElement.tagName).toBe('BUTTON');
expect(buttonElement).not.toHaveAttribute('href');
});

it('throws an error when href is missing and component is not "button"', () => {
console.error = jest.fn();
expect(() => {
render(<Link>Link without href</Link>);
}).toThrow();
});
});

0 comments on commit 983150e

Please sign in to comment.