-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: APP-2733 - Implement Link component (#68)
- Loading branch information
1 parent
863706b
commit 46b8f26
Showing
6 changed files
with
161 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { Link } from './link'; | ||
export { ILinkProps, LinkVariant } from './link.api'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import type { AnchorHTMLAttributes } from 'react'; | ||
import { type IconType } from '../icon'; | ||
|
||
export type LinkVariant = 'primary' | 'neutral'; | ||
|
||
export interface ILinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> { | ||
/** | ||
* Variant of the link. | ||
* @default 'primary' | ||
*/ | ||
variant?: LinkVariant; | ||
/** | ||
* Icon displayed on the right side of the link. Accepts any icon from src/components/icon/iconList.ts. | ||
*/ | ||
iconRight?: IconType; | ||
/** | ||
* Whether the link is disabled. | ||
*/ | ||
disabled?: boolean; | ||
/** | ||
* Optional description text. | ||
*/ | ||
description?: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { Link } from '.'; | ||
|
||
const meta: Meta<typeof Link> = { | ||
title: 'components/Link', | ||
component: Link, | ||
tags: ['autodocs'], | ||
argTypes: { | ||
href: { control: 'text' }, | ||
}, | ||
parameters: { | ||
design: { | ||
type: 'figma', | ||
url: 'https://www.figma.com/file/ISSDryshtEpB7SUSdNqAcw/branch/jfKRr1V9evJUp1uBeyP3Zz/Aragon-ODS?type=design&node-id=7958%3A15661&mode=design&t=chS7QmnJNo46KjlP-1', | ||
}, | ||
docs: { | ||
description: { | ||
component: 'Component will auto size text from **14px to 16px** above `md` breakpoint.', | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
type Story = StoryObj<typeof Link>; | ||
|
||
export const Default: Story = { | ||
args: { | ||
children: 'Label', | ||
}, | ||
}; | ||
|
||
export default meta; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { fireEvent, render, screen } from '@testing-library/react'; | ||
import { Link, type ILinkProps } from '.'; | ||
|
||
describe('<Link /> component', () => { | ||
const createTestComponent = (props?: Partial<ILinkProps>) => { | ||
const completeProps: ILinkProps = { children: 'Default children', href: 'http://default.com', ...props }; | ||
|
||
return <Link {...completeProps} />; | ||
}; | ||
|
||
it('renders correctly with minimum props', () => { | ||
render(createTestComponent({ children: 'Example', href: 'http://example.com' })); | ||
const linkElement = screen.getByRole('link', { name: 'Example' }); | ||
expect(linkElement).toBeInTheDocument(); | ||
expect(linkElement).toHaveAttribute('href', 'http://example.com'); | ||
}); | ||
|
||
it('applies correct classes based on disabled prop', () => { | ||
const handleClick = jest.fn(); | ||
render(createTestComponent({ children: 'TEST', disabled: true, onClick: handleClick })); | ||
// eslint-disable-next-line testing-library/no-node-access | ||
const linkElement = screen.getByText('TEST').closest('a')!; | ||
fireEvent.click(linkElement); | ||
expect(linkElement).toHaveClass('truncate text-neutral-300 cursor-not-allowed'); | ||
expect(linkElement).toHaveAttribute('aria-disabled', 'true'); | ||
expect(linkElement).not.toHaveAttribute('href'); | ||
expect(handleClick).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('renders correctly with no icon', () => { | ||
const children = 'Link without icon'; | ||
render(createTestComponent({ children, href: 'http://example.com' })); | ||
const linkElement = screen.getByRole('link', { name: 'Link without icon' }); | ||
expect(linkElement).toHaveTextContent(children); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import classNames from 'classnames'; | ||
import React from 'react'; | ||
import { Icon } from '../icon'; | ||
import type { ILinkProps, LinkVariant } from './link.api'; | ||
|
||
export const variantToLabelClassNames: Record<LinkVariant, string[]> = { | ||
primary: [ | ||
'text-primary-400 cursor-pointer', // Default | ||
'hover:text-primary-600', // Hover state | ||
'active:text-primary-800', // Active state | ||
], | ||
neutral: [ | ||
'text-neutral-500 cursor-pointer', // Default | ||
'hover:text-neutral-800', // Hover state | ||
'active:text-neutral-800', // Active state | ||
], | ||
}; | ||
|
||
const disabledStyle = 'truncate text-neutral-300 cursor-not-allowed'; | ||
|
||
export const Link = React.forwardRef<HTMLAnchorElement, ILinkProps>( | ||
( | ||
{ | ||
children, | ||
disabled = false, | ||
variant = 'primary', | ||
description, | ||
href, | ||
iconRight, | ||
onClick, | ||
className, | ||
target, | ||
...props | ||
}, | ||
ref, | ||
) => { | ||
const linkClassName = classNames( | ||
'inline-flex max-w-fit flex-col gap-y-0.5 truncate rounded text-sm leading-tight focus:outline-none focus-visible:ring focus-visible:ring-primary focus-visible:ring-offset md:gap-y-1 md:text-base', | ||
className, | ||
disabled ? disabledStyle : variantToLabelClassNames[variant], | ||
); | ||
const descriptionClassName = classNames('truncate', disabled ? disabledStyle : 'text-neutral-500'); | ||
const linkRel = target === '_blank' ? 'noopener noreferrer' : ''; | ||
|
||
return ( | ||
<a | ||
ref={ref} | ||
onClick={!disabled ? onClick : undefined} | ||
href={disabled ? undefined : href} | ||
className={linkClassName} | ||
{...(disabled && { tabIndex: -1, 'aria-disabled': 'true' })} | ||
target={target} | ||
rel={linkRel} | ||
{...props} | ||
> | ||
<div className="flex items-center gap-x-2 truncate"> | ||
{children} | ||
{iconRight && <Icon icon={iconRight} size="sm" />} | ||
</div> | ||
{description && <p className={descriptionClassName}>{description}</p>} | ||
</a> | ||
); | ||
}, | ||
); | ||
|
||
Link.displayName = 'Link'; |