Skip to content

Commit

Permalink
Merge pull request #677 from Orfium/feat/NDS-536_link_component_imple…
Browse files Browse the repository at this point in the history
…mentation_v5

[NDS-536] feat: Link Component
  • Loading branch information
kostasdano authored Oct 13, 2023
2 parents 6e23946 + f6e6182 commit 8aa6da0
Show file tree
Hide file tree
Showing 10 changed files with 1,430 additions and 2 deletions.
151 changes: 151 additions & 0 deletions src/components/Link/Link.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { Meta, Preview, Props, Story } from '@storybook/addon-docs';
import { boolean, select, withKnobs } from '@storybook/addon-knobs';
import Link from './Link';
import Stack from '../storyUtils/Stack';
import { BASE_SHADE } from '../../theme/palette';
import { FIGMA_URL } from '../../utils/common';
import { getIconSelectorKnob } from '../../utils/stories';
import SectionHeader from '../../storybook/SectionHeader';

<Meta
title="Design System/Link"
component={Link}
parameters={{
design: [
{
type: 'figma',
name: 'Avatar',
url: `${FIGMA_URL}?node-id=3325%3A58246`,
},
],
}}
/>

<SectionHeader title={'Link'} />

- [Overview](#overview)
- [Props](#props)
- [Usage](#usage)
- [Variants](#variants)

## Overview

A link is a navigational component that takes a user to another URL, an element within the page, or a file.

## Props

<Props of={Link} />

## Usage

<UsageGuidelines
guidelines={[
'Use as navigation to a new URL',
'Use to navigate to an element within a page (anchor link)',
'Use to download a file',
'Use to show a secondary action (e.g. "Cancel")',
]}
/>

<Tip>To show a primary action (like “Save”), use a Button instead. </Tip>

## Props

<Props of={Link} />

## Variants

### Style

<Preview>
<Story name="Style">
<Stack isVertical>
<Link href="#">Primary</Link>
</Stack>
<Stack isInverted>
<Link href="#" type="inverted">
Inverted
</Link>
</Stack>
</Story>
</Preview>

### Placement

Inline: Used within a body of copy; always with underline text decoration for better discoverability <br/>
Block: Used without a body of copy; without underline text decoration

<Preview>
<Story name="Placement">
<Stack isVertical>
<div>
This is an{' '}
<Link href="#" placement="inline" size={1}>
inline Link
</Link>
.
</div>
<div>
This is a
<Link href="#" size={1}>
block Link
</Link>
.
</div>
</Stack>
</Story>
</Preview>

### Sizes

<Preview>
<Story name="Sizes">
<Stack isVertical>
<Link href="#" size={1}>
Size 1
</Link>
<Link href="#" size={2}>
Size 2
</Link>
<Link href="#" size={3}>
Size 3
</Link>
</Stack>
</Story>
</Preview>

### Link with Icon

<Preview>
<Story name="Link with Icon" parameters={{ decorators: [withKnobs] }}>
<Stack isVertical>
<Link href="#" size={1} iconName={getIconSelectorKnob('iconName', 'externalLink')}>
Link
</Link>
<Link href="#" size={2} iconName={getIconSelectorKnob('iconName', 'externalLink')}>
Link
</Link>
<Link href="#" size={3} iconName={getIconSelectorKnob('iconName', 'externalLink')}>
Link
</Link>
</Stack>
</Story>
</Preview>

### Playground

<Preview>
<Story name="Playground" parameters={{ decorators: [withKnobs] }}>
<Stack isVertical>
<Link
href="#"
size={select('size', [1, 2, 3], 1)}
type={select('type', ['primary', 'inverted'], 'primary')}
placement={select('placement', ['block', 'inline'], 'block')}
isDisabled={boolean('isDisabled', false)}
>
Link
</Link>
</Stack>
</Story>
</Preview>
52 changes: 52 additions & 0 deletions src/components/Link/Link.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { css, SerializedStyles } from '@emotion/react';
import { Theme } from 'theme';

import { getLinkTokens, LinkTokens } from './Link.tokens';
import { LinkProps } from './Link.types';
import { generateStylesFromTokens } from 'components/Typography/utils';

export const linkContainer =
({
placement,
type,
size,
isDisabled,
}: Pick<LinkProps, 'placement' | 'type' | 'size' | 'isDisabled'>) =>
(theme: Theme): SerializedStyles => {
const tokens = getLinkTokens(theme);

return css`
display: ${placement === 'inline' ? 'inline-flex' : 'flex'};
gap: ${tokens('padding')};
color: ${tokens(`textColor.${type}.default` as LinkTokens)};
text-decoration: none;
border: ${tokens('borderWidth.1')} solid ${tokens('borderColor.default')};
&:hover {
color: ${tokens(`textColor.${type}.hover` as LinkTokens)};
path {
fill: ${tokens(`textColor.${type}.hover` as LinkTokens)};
}
}
&:visited {
color: ${tokens(`textColor.${type}.visited` as LinkTokens)};
path {
fill: ${tokens(`textColor.${type}.visited` as LinkTokens)};
}
}
&:focus-visible {
border: ${tokens('borderWidth.2')} solid ${tokens('borderColor.focused')};
}
opacity: ${isDisabled ? theme.tokens.disabledState.get('default') : 1};
width: fit-content;
align-items: center;
cursor: ${isDisabled ? 'default' : 'pointer'};
pointer-events: ${isDisabled ? 'none' : 'default'};
${generateStylesFromTokens(tokens(`${placement}.${size}` as LinkTokens))}
`;
};
12 changes: 12 additions & 0 deletions src/components/Link/Link.tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import link from 'theme/tokens/components/variables/link';
import { getComponentTokens, DotKeys } from 'theme/tokens/utils';

import { Theme } from '../../theme';

export type LinkTokens = DotKeys<typeof link>;

export const getLinkTokens = (
theme: Theme
): ((path: LinkTokens, fn?: (val: string) => any) => any) => {
return getComponentTokens(link, theme);
};
51 changes: 51 additions & 0 deletions src/components/Link/Link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import useTheme from 'hooks/useTheme';
import React, { AnchorHTMLAttributes } from 'react';

import { linkContainer } from './Link.style';
import { LinkTokens, getLinkTokens } from './Link.tokens';
import { LinkProps } from './Link.types';
import Icon from 'components/Icon';

const Link = React.forwardRef<
HTMLAnchorElement,
AnchorHTMLAttributes<HTMLAnchorElement> & LinkProps
>((props, ref) => {
const {
type = 'primary',
placement = 'block',
size = 1,
iconName,
isDisabled,
dataTestPrefixId = '',
children,
...rest
} = props;

const theme = useTheme();

const tokens = getLinkTokens(theme);

return (
<a
css={linkContainer({ placement, type, size, isDisabled })}
ref={ref}
data-testid={`${dataTestPrefixId}_link`}
{...rest}
>
<span>{children}</span>
{/** @TODO Set the right size for the icon based on tokens once Icon component is refactored */}
{iconName && (
<Icon
name={iconName}
color={tokens(`textColor.${type}.default` as LinkTokens)}
size={12}
dataTestId={`${dataTestPrefixId}_link_icon`}
/>
)}
</a>
);
});

Link.displayName = 'Link';

export default Link;
18 changes: 18 additions & 0 deletions src/components/Link/Link.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { AcceptedIconNames } from 'components/Icon';

type LinkSizes = 1 | 2 | 3;

export type LinkProps = {
/** The type of the Link in terms of style */
type?: 'primary' | 'inverted';
/** The placement of the link */
placement?: 'block' | 'inline';
/** The size of the Link */
size?: LinkSizes;
/** Optional icon to add at the right of the Link */
iconName?: AcceptedIconNames;
/** Whether the link is disabled*/
isDisabled?: boolean;
/** Data Test Id prefix **/
dataTestPrefixId?: string;
};
Loading

0 comments on commit 8aa6da0

Please sign in to comment.