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

feat(Link)!: introduce v2.0 component #1890

Merged
merged 2 commits into from
Mar 19, 2024
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
93 changes: 93 additions & 0 deletions src/components/Link/Link-v2.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
@import '../../design-tokens/mixins.css';

/*------------------------------------*\
# LINK
\*------------------------------------*/

.link {
color: var(--eds-theme-color-text-utility-neutral-primary);
display: inline;

/* TODO-AH: verify the way to sync type tokens and one-off treatments */
font-weight: 500;
text-decoration-line: underline;
}

/**
* Sub-components
*/
.link__icon {
padding-left: 0.25rem;

/* Sub-component spacing */
&.link--size-xl,
&.link--size-lg { padding-left: 0.5rem; }

&.link--size-md,
&.link--size-sm,
&.link--size-xs { padding-left: 0.25rem; }
}

/**
* Contexts
*/
.link--context-standalone {
display: block;

/**
* Sizes - using the presets for type ramp matching body-*
*/
&.link--size-xl {
font: var(--eds-theme-typography-body-xl);
}

&.link--size-lg {
font: var(--eds-theme-typography-body-lg);
}

&.link--size-md {
font: var(--eds-theme-typography-body-md);
}

&.link--size-sm {
font: var(--eds-theme-typography-body-sm);
}

&.link--size-xs {
font: var(--eds-theme-typography-body-xs);
}
}

/**
* Emphasis
*/
.link--emphasis-high {
color: var(--eds-theme-color-text-utility-interactive-secondary);
}

.link--emphasis-low {
color: var(--eds-theme-color-text-utility-neutral-primary);
text-decoration: none;
}


/**
* States
*/

.link:hover {
color: var(--eds-theme-color-text-utility-interactive-secondary-hover);
}

.link:active {
color: var(--eds-theme-color-text-utility-interactive-secondary-active);
}

.link:visited {
color: var(--eds-theme-color-text-utility-interactive-visited);
}

.link:focus-visible {
outline: 0.125rem solid var(--eds-theme-color-border-utility-focus);
outline-offset: 0.125rem;
}
88 changes: 88 additions & 0 deletions src/components/Link/Link-v2.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { StoryObj, Meta } from '@storybook/react';
import React from 'react';
import { Link, type LinkProps } from './Link-v2';

export default {
title: 'Components/V2/Link',
component: Link,
parameters: {
badges: ['intro-1.0', 'current-2.0'],
},
args: {
children: 'Link',
size: 'lg',
href: 'https://go.czi.team/eds',
// stop link from navigating to another page so we can click the link for testing
onClick: (event: any) => event.preventDefault(),
},
} as Meta<Args>;

type Args = React.ComponentProps<typeof Link>;

export const Default: StoryObj<Args> = {};

export const LinkWithChevron: StoryObj<Args> = {
args: {
children: 'Default',
context: 'standalone',
icon: 'chevron-right',
},
};

export const LinkWithOpenIcon: StoryObj<Args> = {
args: {
children: 'Default',
context: 'standalone',
icon: 'open-in-new',
},
};

export const Emphasis: StoryObj<Args> = {
args: {
size: 'md',
context: 'standalone',
},
render: (args) => {
return (
<div>
<Link {...args} emphasis="default">
Default Emphasis
</Link>
<Link {...args} emphasis="high">
High Emphasis
</Link>
<Link {...args} emphasis="low">
Low Emphasis
</Link>
</div>
);
},
};

export const LinkInParagraphContext: StoryObj<Args> = {
render: (
args: React.JSX.IntrinsicAttributes &
(LinkProps & React.RefAttributes<HTMLAnchorElement>),
) => (
<div>
Lorem ipsum dolor sit amet,{' '}
<Link {...args} href="https://go.czi.team/eds">
consectetur adipiscing elit
</Link>
. Morbi porta at ante quis molestie. Nam scelerisque id diam at iaculis.
Nullam sit amet iaculis erat. Nulla id tellus ante.{' '}
<Link {...args} href="https://go.czi.team/eds">
Aliquam pellentesque ipsum sagittis, commodo neque at, ornare est.
Maecenas a malesuada sem, vitae euismod erat. Nullam molestie nunc non
dui dignissim fermentum.
</Link>{' '}
Aliquam id volutpat nulla, sed auctor orci. Fusce cursus leo nisi. Fusce
vehicula vitae nisl nec ultricies. Cras ut enim nec magna semper egestas.
Sed sed quam id nisl bibendum convallis. Proin suscipit, odio{' '}
<Link {...args} href="https://go.czi.team/eds">
vel pulvinar
</Link>{' '}
euismod, risus eros ullamcorper lectus, non blandit nulla dui eget massa.
</div>
),
};
92 changes: 92 additions & 0 deletions src/components/Link/Link-v2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import clsx from 'clsx';
import React, { forwardRef } from 'react';
import type { Size } from '../../util/variant-types';
import type { IconName } from '../Icon';
import Icon from '../Icon';

import styles from './Link-v2.module.css';

export type LinkProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
// Component API
/**
* Component used to render the element. Meant to support interaction with framework navigation libraries.
* TODO-AH: support frameworks that may not use (or replace) `href` for some other prop
*
* **Default is `"a"`**.
*/
as: string | React.ElementType;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this to support links link in Remix. Will likely need some work to support fully, but will be able to test once i get thru the first wave of component implementations

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Challenge I see: Remix links currently use to instead of href

So to enable passing in a Remix link to here, we'd probably need to figure out how to support passing the EDS Link's href prop to the Remix Link's to somehow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to replicate what we do using ClickableStyle and also get rid of that component in the next version :).

I didn't want to copy how ClickableStyle works in here, b/c it seems to lose all the typing information and converts everything to any :( What does make sense over there is that it uses a type param to combine the types of Remix's Link with the component props.

I'm guessing this was part of why you needed to export the types earlier?

Copy link
Contributor

@ahuth ahuth Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree on that goal, and not wanting to do ClickableStyle.

My point is that to achieve:

<EdsLink
  as={RemixLink}
  href={...} // or maybe to={...}?
/>

we'll need to figure out how to handle href vs to.

Copy link
Contributor

@ahuth ahuth Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One way to do that would be for the Eds Link to take href and always pass both it as both href and to on to the as component...

Kind of annoying, but that's the simplest thing I can see (at least until Remix updates its Link to use the standard href).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree on that goal, and not wanting to do ClickableStyle.

My point is that to achieve:

<EdsLink
  as={RemixLink}
  href={...} // or maybe to={...}?
/>

we'll need to figure out how to handle href vs to.

Yah, I think you misunderstood. The way ClickableStyle supports to/href is weird, but "works". So we are saying the same thing basically.

I want to avoid polluting EDS with anything specific to Remix or some other framework, as well. I like the TypeParam approach (e.g., combine EDSLink with Framework type, with option or to Omit). Once I get thru the re-theming, I'll try a few things.

/**
* The link contents or label.
*/
children: string;
// Design API
/**
* Where `Link` sits alongside other text and content:
*
* * **inline** - Inline link inherits the text size established within the `<p>` paragraph they are embedded in.
* * **standalone** - Users can choose from the available sizes.
*/
context?: 'inline' | 'standalone';
/**
* (trailing) icon to use with the link
*/
icon?: Extract<IconName, 'chevron-right' | 'open-in-new'>;
/**
* Extra or lowered colors added to a link
*/
emphasis?: 'default' | 'high' | 'low';

/**
* Link size inherits from the surrounding text.
*/
size?: Extract<Size, 'xs' | 'sm' | 'md' | 'lg' | 'xl'>;
};

/**
* `import {Link} from "@chanzuckerberg/eds";`
*
* Component for making styled anchor tags. Links allow users to navigate within or between a web page(s) or app(s).
*
*/
export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't wait until React removes forwardRef in React 19

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh, what will happen instead? I never read the upcoming docs for things besides HTML/CSS/JS :P

(
{
as: Component = 'a',
children,
className,
context,
emphasis = 'default',
icon,
size = 'md',
...other
},
ref,
) => {
const componentClassName = clsx(
className,
styles['link'],
context && styles[`link--context-${context}`],
emphasis && styles[`link--emphasis-${emphasis}`],
icon && styles['link--has-right-icon'],
size && styles[`link--size-${size}`],
);

const iconSize = size && (['xl', 'lg'].includes(size) ? '1.5rem' : '1rem');

return (
<Component className={componentClassName} ref={ref} {...other}>
{children}
{icon && context === 'standalone' && (
<Icon
className={styles['link__icon']}
name={icon}
purpose="decorative"
size={iconSize}
/>
)}
</Component>
);
},
);

Link.displayName = 'Link';
1 change: 1 addition & 0 deletions src/components/Link/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { Link as default } from './Link';
export type { LinkProps } from './Link';
export { Link as LinkV2 } from './Link-v2';
Loading