Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: APP-2463 - Update Link component #25

Merged
merged 5 commits into from
Sep 12, 2023
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- `ActionItemAddress` component
- `IconUpdate` component

### Changed

- `Link` component to include description

## [0.2.13] - 2023-08-31

Expand Down
25 changes: 25 additions & 0 deletions src/components/icons/interface/icon_update.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { type IconType } from '..';

export const IconUpdate: IconType = ({ height = 16, width = 16, ...props }) => {
return (
<svg
width={width}
height={height}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g id="_update">
<path
id="Icon"
fillRule="evenodd"
clipRule="evenodd"
d="M8.99993 1.00098C8.73475 1.00098 8.48043 1.10632 8.29292 1.29383C8.1054 1.48134 8.00006 1.73566 8.00006 2.00085C8.00006 2.26603 8.1054 2.52035 8.29292 2.70786C8.48043 2.89537 8.73475 3.00072 8.99993 3.00072H10.4997C10.6323 3.00072 10.7595 3.05339 10.8532 3.14714C10.947 3.2409 10.9997 3.36806 10.9997 3.50065V9.49987H9.49987C9.40086 9.4997 9.30403 9.52892 9.22165 9.58384C9.13927 9.63876 9.07506 9.71691 9.03714 9.80837C8.99923 9.89983 8.98933 10.0005 9.00869 10.0976C9.02806 10.1947 9.07582 10.2838 9.14591 10.3538L11.6456 12.8534C11.692 12.9 11.7472 12.9369 11.8079 12.9621C11.8687 12.9873 11.9338 13.0003 11.9995 13.0003C12.0653 13.0003 12.1304 12.9873 12.1912 12.9621C12.2519 12.9369 12.3071 12.9 12.3535 12.8534L14.8532 10.3538C14.9233 10.2838 14.971 10.1947 14.9904 10.0976C15.0098 10.0005 14.9999 9.89983 14.962 9.80837C14.924 9.71691 14.8598 9.63876 14.7774 9.58384C14.6951 9.52892 14.5982 9.4997 14.4992 9.49987H12.9994V3.50065C12.9994 2.8377 12.7361 2.20189 12.2673 1.73311C11.7985 1.26433 11.1627 1.00098 10.4997 1.00098L8.99993 1.00098ZM4.35453 3.1467C4.30809 3.10014 4.25292 3.0632 4.19218 3.038C4.13144 3.0128 4.06633 2.99982 4.00057 2.99982C3.93481 2.99982 3.8697 3.0128 3.80896 3.038C3.74823 3.0632 3.69306 3.10014 3.64662 3.1467L1.14694 5.64637C1.07684 5.71629 1.02908 5.80545 1.00972 5.90255C0.990352 5.99965 1.00025 6.10031 1.03817 6.19177C1.07608 6.28323 1.1403 6.36137 1.22267 6.41629C1.30505 6.47122 1.40188 6.50044 1.50089 6.50026H3.0007V12.4995C3.0007 13.1624 3.26406 13.7982 3.73284 14.267C4.20162 14.7358 4.83742 14.9992 5.50038 14.9992H7.00019C7.26537 14.9992 7.51969 14.8938 7.70721 14.7063C7.89472 14.5188 8.00006 14.2645 8.00006 13.9993C8.00006 13.7341 7.89472 13.4798 7.70721 13.2923C7.51969 13.1048 7.26537 12.9994 7.00019 12.9994H5.50038C5.36779 12.9994 5.24063 12.9467 5.14687 12.853C5.05312 12.7592 5.00044 12.6321 5.00044 12.4995V6.50026H6.50025C6.59926 6.50044 6.69609 6.47122 6.77847 6.41629C6.86085 6.36137 6.92507 6.28323 6.96298 6.19177C7.00089 6.10031 7.01079 5.99965 6.99143 5.90255C6.97206 5.80545 6.92431 5.71629 6.85421 5.64637L4.35453 3.1467Z"
fill="white"
/>
</g>
</svg>
);
};
1 change: 1 addition & 0 deletions src/components/icons/interface/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export { IconStorage } from './icon_storage';
export { IconSuccess } from './icon_success';
export { IconSwitch } from './icon_switch';
export { IconTurnOff } from './icon_turn_off';
export { IconUpdate } from './icon_update';
export { IconWarning } from './icon_warning';
export { IconWithdraw } from './icon_withdraw';
export { IconRadioCancel } from './radio';
63 changes: 37 additions & 26 deletions src/components/link/link.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,46 @@
import { type Meta, type Story } from '@storybook/react';
import { type Meta, type StoryObj } from '@storybook/react';
import React from 'react';
import { IconChevronDown } from '../icons';
import { Link, type LinkProps } from './link';

export default {
title: 'Components/Link',
import { IconLinkExternal } from '../icons';
import { LINK_VARIANTS, Link } from './link';

const meta: Meta<typeof Link> = {
component: Link,
} as Meta;
title: 'Components/Link',
tags: ['autodocs'],
argTypes: {
label: { control: { type: 'text' } },
description: { control: { type: 'text' } },
type: {
options: LINK_VARIANTS,
control: { type: 'select' },
defaultValue: 'primary',
},
},
};

const Template: Story<LinkProps> = (args) => <Link {...args} />;
export default meta;

export const Default = Template.bind({});
Default.args = {
label: 'Link text',
href: 'https://aragon.org/',
type: 'primary',
};
type Story = StoryObj<typeof Link>;

export const IconRight = Template.bind({});
IconRight.args = {
iconRight: <IconChevronDown />,
label: 'Link text',
href: 'https://aragon.org/',
type: 'secondary',
export const Primary: Story = {
render: (args) => <Link {...args} />,
args: {
label: 'Aragon',
description: 'Association Website',
href: 'https://aragon.org/',
type: 'primary',
iconRight: <IconLinkExternal />,
},
};

export const IconLeft = Template.bind({});
IconLeft.args = {
iconLeft: <IconChevronDown />,
label: 'Link text',
href: 'https://aragon.org/',
disabled: true,
type: 'neutral',
export const Neutral: Story = {
render: (args) => <Link {...args} />,
args: {
label: 'Aragon',
description: 'Association Website',
href: 'https://aragon.org/',
type: 'neutral',
iconRight: <IconLinkExternal />,
},
};
7 changes: 3 additions & 4 deletions src/components/link/link.test.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Link } from './link';
import { Link, type LinkProps } from './link';

describe('Link', () => {
// eslint-disable-next-line
function setup(args: any) {
function setup(args: LinkProps = {} as LinkProps) {
render(<Link {...args} />);
return screen.getByTestId('link');
}

test('should render without crashing', () => {
const element = setup({});
const element = setup();
expect(element).toBeInTheDocument;
});
});
98 changes: 53 additions & 45 deletions src/components/link/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,78 +5,86 @@ import { type IconType } from '../icons';

export type LinkProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
disabled?: boolean;
/** whether link should open new tab to external location */
/** Indicates whether the link should open in a new tab */
external?: boolean;
iconRight?: React.FunctionComponentElement<IconType>;
iconLeft?: React.FunctionComponentElement<IconType>;
/** optional label for the link, defaults to the href if value not provided */
label?: string;
/** link variants */
type?: 'primary' | 'secondary' | 'neutral';
/** Label for the link */
label: string;
/** Optional description */
description?: string;
/** Variants for link appearance */
type?: LinkType;
};

/** Default link component */
export const Link: React.FC<LinkProps> = ({
disabled = false,
external = true,
type = 'primary',
iconLeft,
iconRight,
label,
href,
...props
}) => {
return (
<StyledLink
href={disabled ? undefined : href}
rel="noopener noreferrer"
type={type}
disabled={disabled}
{...(external ? { target: '_blank' } : {})}
{...props}
data-testid="link"
>
{iconLeft && <div>{iconLeft}</div>}
<Label>{label ?? href}</Label>
{!iconLeft && iconRight && <div>{iconRight}</div>}
</StyledLink>
);
};
export const LINK_VARIANTS = ['primary', 'neutral'] as const;
type LinkType = (typeof LINK_VARIANTS)[number];

/**
* The Link component creates a styled anchor element with optional icon and description.
*
* @param {LinkProps} props - The properties of the link.
*/
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
({ disabled = false, external = true, type = 'primary', iconRight, description, label, href, ...props }, ref) => {
return (
<StyledLink
ref={ref}
href={disabled ? undefined : href}
rel="noopener noreferrer"
type={type}
disabled={disabled}
{...(external && { target: '_blank' })}
{...props}
data-testid="link"
>
<div className="flex gap-x-1 items-center mr-0.5">
<Label>{label}</Label>
{iconRight && <div>{iconRight}</div>}
</div>
{description && <Description>{description}</Description>}
</StyledLink>
);
},
);

Link.displayName = 'Link';

type StyledLinkProps = Pick<LinkProps, 'disabled'> & {
type: NonNullable<LinkProps['type']>;
};

const StyledLink = styled.a.attrs(({ disabled, type }: StyledLinkProps) => {
let className = 'inline-flex items-center space-x-1.5 max-w-full rounded cursor-pointer ';

let className = 'inline-flex flex-col gap-y-0.25 tablet:gap-y-0.5 max-w-full rounded cursor-pointer ';
className += variants[type];

className += disabled ? disabledColors[type] : defaultColors[type];

return { className };
})<StyledLinkProps>`
outline: 0; // forcefully setting to remove default Chrome black outline
outline: 0; // Remove default Chrome black outline
`;

const Label = styled.span.attrs({
className: 'font-bold truncate',
className: 'ft-text-base font-semibold truncate',
})``;

const Description = styled.p.attrs({
className: 'ft-text-sm text-ui-600 truncate',
})``;

const variants = {
primary: 'hover:text-primary-700 active:text-primary-800 focus-visible:ring-2 focus-visible:ring-primary-500 ',
secondary: 'hover:text-primary-100 active:text-primary-900 focus-visible:ring-2 focus-visible:ring-ui-0 ',
neutral: 'hover:text-primary-700 active:text-primary-800 focus-visible:ring-2 focus-visible:ring-primary-500 ',
primary: `hover:text-primary-600 active:text-primary-800
focus-visible:ring focus-visible:ring-primary-200 focus-visible:bg-ui-50 `,

neutral: `hover:text-ui-800 active:text-ui-800
focus-visible:ring focus-visible:ring-primary-200 focus-visible:bg-ui-50 `,
};

const disabledColors = {
primary: 'text-ui-300 pointer-events-none ',
secondary: 'text-primary-300 pointer-events-none ',
neutral: 'text-ui-300 pointer-events-none ',
};

const defaultColors = {
primary: 'text-primary-500 ',
secondary: 'text-ui-0 ',
neutral: 'text-ui-500 ',
primary: 'text-primary-400 ',
neutral: 'text-ui-600 ',
};