Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions src/components/Link/link.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ const meta: Meta<typeof Link> = {
title: 'Components (Verified)/Links',
tags: ['autodocs'],
component: Link,
argTypes: {
href: { control: 'text' },
label: { control: 'text' },
isButton: { control: 'boolean' },
isJump: { control: 'boolean' },
isRouterLink: { control: 'boolean' },
appearance: {
control: 'select',
options: ['primary', 'secondary', 'warning', 'destructive'],
},
iconLeft: { control: 'text' },
iconRight: { control: 'text' },
children: { control: 'text' },
},
};

export default meta;
Expand All @@ -15,8 +29,34 @@ type Story = StoryObj<typeof meta>;

const DefaultArguments = {
args: {
href: '#',
children: 'Link Text',
href: '/#',
label: 'Link Text',
isButton: false,
isJump: false,
isRouterLink: false,
appearance: 'primary',
iconLeft: undefined,
iconRight: undefined,
children: undefined,
},
};

export const Configurable: Story = {
args: {
...DefaultArguments.args,
},
render: (arguments_) =>
arguments_.isRouterLink ? (
<BrowserRouter>
<Link {...arguments_} />
</BrowserRouter>
) : (
<Link {...arguments_} />
),
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const link = canvas.getByRole('link');
await expect(link).toHaveAttribute('href', args.href);
},
};

Expand Down Expand Up @@ -117,7 +157,9 @@ export const Destructive: Story = {
args: {
...DefaultArguments.args,
},
render: () => <Link href='/#' type='destructive' label='Destructive link' />,
render: () => (
<Link href='/#' appearance='warning' label='Destructive link' />
),
};

export const LinkWithReactRouterLink: Story = {
Expand Down
29 changes: 26 additions & 3 deletions src/components/Link/link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ describe('<Link />', () => {
};
const testId = linkBaseProperties['data-testid'];

it('Type: "default"', () => {
it('Appearance: "primary" (default)', () => {
render(<Link {...linkBaseProperties} />);
const link = screen.getByTestId(testId);
expect(link).toHaveAttribute('href', '/foo/bar');
});

it('Type: "destructive"', () => {
render(<Link {...linkBaseProperties} type='destructive' />);
it('Appearance: "warning" (destructive)', () => {
render(<Link {...linkBaseProperties} appearance='warning' />);
const link = screen.getByTestId(testId);
expect(link).toHaveClass('a-btn a-btn--link a-btn--warning');
});
Expand Down Expand Up @@ -73,6 +73,29 @@ describe('<Link />', () => {
expect(await screen.findByTestId('link-icon-right')).toBeInTheDocument();
});

it('Option: appearance - it applies button appearance classes when isButton', () => {
render(
<Link {...linkBaseProperties} isButton appearance='secondary' />,
);
const link = screen.getByTestId(testId);
expect(link).toHaveClass('a-btn');
expect(link).toHaveClass('a-btn--secondary');
});

it('Option: appearance - it applies warning class when isButton', () => {
render(<Link {...linkBaseProperties} isButton appearance='warning' />);
const link = screen.getByTestId(testId);
expect(link).toHaveClass('a-btn');
expect(link).toHaveClass('a-btn--warning');
});

it('Option: appearance - it does not apply secondary class without isButton', () => {
render(<Link {...linkBaseProperties} appearance='secondary' />);
const link = screen.getByTestId(testId);
expect(link).not.toHaveClass('a-btn--secondary');
expect(link).not.toHaveClass('a-btn--warning');
});

it('Other: propagates other attributes', () => {
render(<Link {...linkBaseProperties} target='_blank' />);
const link = screen.getByTestId(testId);
Expand Down
18 changes: 10 additions & 8 deletions src/components/Link/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export interface LinkProperties extends HTMLProps<HTMLAnchorElement> {
* Whether the link should be rendered as a button.
*/
isButton?: boolean;
/**
* What is the link's appearance?
*/
appearance?: 'primary' | 'secondary' | 'destructive' | 'warning';
/**
* Any children to render within the link. Allows you to wrap any node with anchor tag
*/
Expand Down Expand Up @@ -42,10 +46,6 @@ export interface LinkProperties extends HTMLProps<HTMLAnchorElement> {
*/
label?: string;
ref?: Ref<HTMLAnchorElement>;
/**
* What type of link should be rendered
*/
type?: 'default' | 'destructive';
}

/**
Expand All @@ -56,14 +56,14 @@ export interface LinkProperties extends HTMLProps<HTMLAnchorElement> {
*/
export default function Link({
isButton = false,
appearance,
children,
href,
iconLeft,
iconRight,
isJump = false,
isRouterLink = false,
label,
type = 'default',
...others
}: LinkProperties): JSXElement {
const hasLeftIcon = Boolean(iconLeft);
Expand All @@ -72,10 +72,12 @@ export default function Link({
const shouldUseLinkStyles = !isButton && (hasIcons || isJump);
const shouldWrapLabel = isButton || shouldUseLinkStyles;
const labelNode = shouldWrapLabel ? <LinkText>{label}</LinkText> : label;
const isDestructive = ['destructive', 'warning'].includes(appearance);
const cname = classnames(others.className, {
'a-btn': isButton || type === 'destructive',
'a-btn--link': type === 'destructive',
'a-btn--warning': type === 'destructive',
'a-btn': isButton || isDestructive,
'a-btn--link': isDestructive && !isButton,
'a-btn--warning': isDestructive,
'a-btn--secondary': isButton && appearance === 'secondary',
'a-link--jump': isJump,
'a-link': shouldUseLinkStyles,
});
Expand Down
Loading