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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: Improve Link component #23767

Merged
merged 5 commits into from
Aug 9, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 2 additions & 21 deletions code/ui/blocks/src/components/ArgsTable/Empty.tsx
@@ -1,7 +1,7 @@
import type { FC } from 'react';
import React, { useEffect, useState } from 'react';
import { styled } from '@storybook/theming';
import { Icon, Link } from '@storybook/components/experimental';
import { Link } from '@storybook/components/experimental';

interface EmptyProps {
inAddonPanel?: boolean;
Expand Down Expand Up @@ -53,16 +53,6 @@ const Divider = styled.div(({ theme }) => ({
backgroundColor: theme.appBorderColor,
}));

const VideoIcon = styled.div(({ theme }) => ({
width: 22,
height: 16,
borderRadius: theme.appBorderRadius,
border: `1px solid ${theme.color.secondary}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}));

export const Empty: FC<EmptyProps> = ({ inAddonPanel }) => {
const [isLoading, setIsLoading] = useState(true);

Expand Down Expand Up @@ -95,16 +85,7 @@ export const Empty: FC<EmptyProps> = ({ inAddonPanel }) => {
<Links>
{inAddonPanel && (
<>
<Link
href="https://youtu.be/0gOfS6K0x0E"
target="_blank"
icon={
<VideoIcon>
<Icon.Play size={8} />
</VideoIcon>
}
withArrow
>
<Link href="https://youtu.be/0gOfS6K0x0E" target="_blank" icon="Video" withArrow>
Watch 5m video
</Link>
<Divider />
Expand Down
1 change: 1 addition & 0 deletions code/ui/components/src/experimental.ts
Expand Up @@ -12,3 +12,4 @@ export { Input } from './new/Input/Input';
export { Select } from './new/Select/Select';
export { Link } from './new/Link/Link';
export { Icon } from './new/Icon/Icon';
export { IconButton } from './new/IconButton/IconButton';
34 changes: 17 additions & 17 deletions code/ui/components/src/new/IconButton/IconButton.tsx
Expand Up @@ -35,7 +35,7 @@ export const IconButton: {
IconButton.displayName = 'IconButton';

const StyledButton = styled.button<Omit<ButtonProps, 'icon'>>(
({ theme, variant = 'primary', size = 'medium', disabled = false, active = false }) => ({
({ theme, variant = 'solid', size = 'medium', disabled = false, active = false }) => ({
border: 0,
cursor: disabled ? 'not-allowed' : 'pointer',
display: 'inline-flex',
Expand Down Expand Up @@ -64,41 +64,41 @@ const StyledButton = styled.button<Omit<ButtonProps, 'icon'>>(
fontWeight: theme.typography.weight.bold,
lineHeight: '1',
background: `${(() => {
if (variant === 'primary') return theme.color.secondary;
if (variant === 'secondary') return theme.button.background;
if (variant === 'tertiary' && active) return theme.background.hoverable;
if (variant === 'solid') return theme.color.secondary;
if (variant === 'outline') return theme.button.background;
if (variant === 'ghost' && active) return theme.background.hoverable;
return 'transparent';
})()}`,
color: `${(() => {
if (variant === 'primary') return theme.color.lightest;
if (variant === 'secondary') return theme.input.color;
if (variant === 'tertiary' && active) return theme.color.secondary;
if (variant === 'tertiary') return theme.color.mediumdark;
if (variant === 'solid') return theme.color.lightest;
if (variant === 'outline') return theme.input.color;
if (variant === 'ghost' && active) return theme.color.secondary;
if (variant === 'ghost') return theme.color.mediumdark;
return theme.input.color;
})()}`,
boxShadow: variant === 'secondary' ? `${theme.button.border} 0 0 0 1px inset` : 'none',
boxShadow: variant === 'outline' ? `${theme.button.border} 0 0 0 1px inset` : 'none',
borderRadius: theme.input.borderRadius,

'&:hover': {
color: variant === 'tertiary' ? theme.color.secondary : null,
color: variant === 'ghost' ? theme.color.secondary : null,
background: `${(() => {
let bgColor = theme.color.secondary;
if (variant === 'primary') bgColor = theme.color.secondary;
if (variant === 'secondary') bgColor = theme.button.background;
if (variant === 'solid') bgColor = theme.color.secondary;
if (variant === 'outline') bgColor = theme.button.background;

if (variant === 'tertiary') return transparentize(0.86, theme.color.secondary);
if (variant === 'ghost') return transparentize(0.86, theme.color.secondary);
return theme.base === 'light' ? darken(0.02, bgColor) : lighten(0.03, bgColor);
})()}`,
},

'&:active': {
color: variant === 'tertiary' ? theme.color.secondary : null,
color: variant === 'ghost' ? theme.color.secondary : null,
background: `${(() => {
let bgColor = theme.color.secondary;
if (variant === 'primary') bgColor = theme.color.secondary;
if (variant === 'secondary') bgColor = theme.button.background;
if (variant === 'solid') bgColor = theme.color.secondary;
if (variant === 'outline') bgColor = theme.button.background;

if (variant === 'tertiary') return theme.background.hoverable;
if (variant === 'ghost') return theme.background.hoverable;
return theme.base === 'light' ? darken(0.02, bgColor) : lighten(0.03, bgColor);
})()}`,
},
Expand Down
73 changes: 41 additions & 32 deletions code/ui/components/src/new/Link/Link.stories.tsx
@@ -1,7 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';

import { Icon } from '@storybook/components/experimental';
import { Link } from './Link';

const meta: Meta<typeof Link> = {
Expand All @@ -26,24 +24,56 @@ export const Variants: Story = {
<Link href="https://storybook.js.org/" variant="secondary">
Secondary
</Link>
<Link href="https://storybook.js.org/" variant="tertiary">
Tertiary
</div>
),
};

export const Underline: Story = {
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Link href="https://storybook.js.org/" variant="primary" underline="hover">
Primary
</Link>
<Link href="https://storybook.js.org/" variant="primary" underline="always">
Secondary
</Link>
<Link href="https://storybook.js.org/" variant="secondary" underline="hover">
Secondary
</Link>
<Link href="https://storybook.js.org/" variant="secondary" underline="always">
Secondary
</Link>
</div>
),
};

export const WithIcon: Story = {
export const Weight: Story = {
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Link href="https://storybook.js.org/" variant="primary" icon={<Icon.FaceHappy />}>
<Link href="https://storybook.js.org/" variant="primary" weight="regular">
Primary
</Link>
<Link href="https://storybook.js.org/" variant="secondary" icon={<Icon.FaceHappy />}>
<Link href="https://storybook.js.org/" variant="primary" weight="bold">
Secondary
</Link>
<Link href="https://storybook.js.org/" variant="tertiary" icon={<Icon.FaceHappy />}>
Tertiary
<Link href="https://storybook.js.org/" variant="secondary" weight="regular">
Secondary
</Link>
<Link href="https://storybook.js.org/" variant="secondary" weight="bold">
Secondary
</Link>
</div>
),
};

export const WithIcon: Story = {
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Link href="https://storybook.js.org/" variant="primary" icon="FaceHappy">
Primary
</Link>
<Link href="https://storybook.js.org/" variant="secondary" icon="FaceHappy">
Secondary
</Link>
</div>
),
Expand All @@ -59,35 +89,14 @@ export const WithArrow: Story = {
<Link href="https://storybook.js.org/" variant="secondary" withArrow>
Secondary
</Link>
<Link href="https://storybook.js.org/" variant="tertiary" withArrow>
Tertiary
</Link>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Link
href="https://storybook.js.org/"
variant="primary"
icon={<Icon.FaceHappy />}
withArrow
>
<Link href="https://storybook.js.org/" variant="primary" icon="FaceHappy" withArrow>
Primary
</Link>
<Link
href="https://storybook.js.org/"
variant="secondary"
icon={<Icon.FaceHappy />}
withArrow
>
<Link href="https://storybook.js.org/" variant="secondary" icon="FaceHappy" withArrow>
Secondary
</Link>
<Link
href="https://storybook.js.org/"
variant="tertiary"
icon={<Icon.FaceHappy />}
withArrow
>
Tertiary
</Link>
</div>
</div>
),
Expand Down
73 changes: 36 additions & 37 deletions code/ui/components/src/new/Link/Link.tsx
@@ -1,15 +1,17 @@
import type { MouseEvent, ReactNode } from 'react';
import type { MouseEvent } from 'react';
import React, { forwardRef } from 'react';
import { styled } from '@storybook/theming';
import { darken } from 'polished';
import type { Icons } from '@storybook/icons';
import { Icon } from '../Icon/Icon';
import type { PropsOf } from '../utils/types';

export interface LinkProps<T extends React.ElementType = React.ElementType> {
as?: T;
children: string;
variant?: 'primary' | 'secondary' | 'tertiary';
icon?: ReactNode;
variant?: 'primary' | 'secondary';
weight?: 'regular' | 'bold';
underline?: 'hover' | 'always';
icon?: Icons;
onClick?: (e: MouseEvent) => void;
withArrow?: boolean;
}
Expand All @@ -20,11 +22,12 @@ export const Link: {
): JSX.Element;
displayName?: string;
} = forwardRef(
({ as, children, icon, withArrow, ...props }: LinkProps, ref: React.Ref<HTMLAnchorElement>) => {
({ children, icon, withArrow, ...props }: LinkProps, ref: React.Ref<HTMLAnchorElement>) => {
const LocalIcon = Icon[icon];
return (
<StyledLink as={as} ref={ref} {...props}>
<StyledLink ref={ref} {...props}>
<StyledLeft>
{icon}
{icon && <LocalIcon />}
{children}
</StyledLeft>
{withArrow && <Icon.ChevronRight size={8} />}
Expand All @@ -35,39 +38,35 @@ export const Link: {

Link.displayName = 'Link';

const StyledLink = styled.a<Omit<LinkProps, 'children'>>(({ theme, variant = 'primary' }) => ({
display: 'inline-flex',
gap: 4,
alignItems: 'center',
transition: 'all 150ms ease-out',
textDecoration: 'none',
lineHeight: 1,
color: `${(() => {
if (variant === 'primary') return theme.color.secondary;
if (variant === 'secondary') return theme.textMutedColor;
if (variant === 'tertiary') return theme.color.dark;
return theme.color.secondary;
})()}`,

'&:hover, &:focus': {
cursor: 'pointer',
const StyledLink = styled.a<Omit<LinkProps, 'children'>>(
({ theme, variant = 'primary', underline = 'hover', weight = 'regular' }) => ({
display: 'inline-flex',
gap: 4,
alignItems: 'center',
transition: 'all 150ms ease-out',
textDecoration: 'none',
lineHeight: 1,
color: `${(() => {
if (variant === 'primary') return darken(0.07, theme.color.secondary);
if (variant === 'secondary') return theme.color.dark;
if (variant === 'tertiary') return theme.color.darkest;
return darken(0.07, theme.color.secondary);
if (variant === 'primary') return theme.color.secondary;
if (variant === 'secondary') return theme.color.defaultText;
return theme.color.secondary;
})()}`,
},

'&:active': {
color: `${(() => {
if (variant === 'primary') return darken(0.1, theme.color.secondary);
if (variant === 'secondary') return theme.color.darker;
if (variant === 'tertiary') return theme.textMutedColor;
return darken(0.1, theme.color.secondary);
fontWeight: `${(() => {
if (weight === 'regular') return theme.typography.weight.regular;
if (weight === 'bold') return theme.typography.weight.bold;
return theme.typography.weight.bold;
})()}`,
},
}));
textDecorationLine: `${underline === 'always' ? 'underline' : 'none'}`,
textDecorationStyle: 'solid',
textDecorationThickness: '1px',
textUnderlineOffset: '2px',

'&:hover, &:focus': {
cursor: 'pointer',
textDecorationLine: 'underline',
},
})
);

const StyledLeft = styled.span(({ theme }) => ({
display: 'inline-flex',
Expand Down
2 changes: 1 addition & 1 deletion code/ui/manager/src/globals/exports.ts
Expand Up @@ -114,7 +114,7 @@ export default {
'resetComponents',
'withReset',
],
'@storybook/components/experimental': ['Button', 'Icon', 'Input', 'Link', 'Select'],
'@storybook/components/experimental': ['Button', 'Icon', 'IconButton', 'Input', 'Link', 'Select'],
'@storybook/channels': [
'Channel',
'PostMessageTransport',
Expand Down