diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f14a5d2f..65ad2d34e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/components/icons/interface/icon_update.tsx b/src/components/icons/interface/icon_update.tsx new file mode 100644 index 000000000..be3a6149c --- /dev/null +++ b/src/components/icons/interface/icon_update.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { type IconType } from '..'; + +export const IconUpdate: IconType = ({ height = 16, width = 16, ...props }) => { + return ( + + + + + + ); +}; diff --git a/src/components/icons/interface/index.ts b/src/components/icons/interface/index.ts index d2f540c97..11b7b9914 100644 --- a/src/components/icons/interface/index.ts +++ b/src/components/icons/interface/index.ts @@ -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'; diff --git a/src/components/link/link.stories.tsx b/src/components/link/link.stories.tsx index 2a3a35b27..120e0eb19 100644 --- a/src/components/link/link.stories.tsx +++ b/src/components/link/link.stories.tsx @@ -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 = { 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 = (args) => ; +export default meta; -export const Default = Template.bind({}); -Default.args = { - label: 'Link text', - href: 'https://aragon.org/', - type: 'primary', -}; +type Story = StoryObj; -export const IconRight = Template.bind({}); -IconRight.args = { - iconRight: , - label: 'Link text', - href: 'https://aragon.org/', - type: 'secondary', +export const Primary: Story = { + render: (args) => , + args: { + label: 'Aragon', + description: 'Association Website', + href: 'https://aragon.org/', + type: 'primary', + iconRight: , + }, }; -export const IconLeft = Template.bind({}); -IconLeft.args = { - iconLeft: , - label: 'Link text', - href: 'https://aragon.org/', - disabled: true, - type: 'neutral', +export const Neutral: Story = { + render: (args) => , + args: { + label: 'Aragon', + description: 'Association Website', + href: 'https://aragon.org/', + type: 'neutral', + iconRight: , + }, }; diff --git a/src/components/link/link.test.tsx b/src/components/link/link.test.tsx index 3fac52476..cb96136b4 100644 --- a/src/components/link/link.test.tsx +++ b/src/components/link/link.test.tsx @@ -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(); return screen.getByTestId('link'); } test('should render without crashing', () => { - const element = setup({}); + const element = setup(); expect(element).toBeInTheDocument; }); }); diff --git a/src/components/link/link.tsx b/src/components/link/link.tsx index f4525a52f..2bb932b43 100644 --- a/src/components/link/link.tsx +++ b/src/components/link/link.tsx @@ -5,78 +5,86 @@ import { type IconType } from '../icons'; export type LinkProps = React.AnchorHTMLAttributes & { 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; - iconLeft?: React.FunctionComponentElement; - /** 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 = ({ - disabled = false, - external = true, - type = 'primary', - iconLeft, - iconRight, - label, - href, - ...props -}) => { - return ( - - {iconLeft &&
{iconLeft}
} - - {!iconLeft && iconRight &&
{iconRight}
} -
- ); -}; +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( + ({ disabled = false, external = true, type = 'primary', iconRight, description, label, href, ...props }, ref) => { + return ( + +
+ + {iconRight &&
{iconRight}
} +
+ {description && {description}} +
+ ); + }, +); + +Link.displayName = 'Link'; type StyledLinkProps = Pick & { type: NonNullable; }; 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 }; })` - 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 ', };