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 new Button component #23765

Merged
merged 3 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
2 changes: 1 addition & 1 deletion code/ui/components/package.json
Expand Up @@ -71,7 +71,7 @@
"@storybook/client-logger": "workspace:*",
"@storybook/csf": "^0.1.0",
"@storybook/global": "^5.0.0",
"@storybook/icons": "^1.1.5",
"@storybook/icons": "^1.1.6",
"@storybook/theming": "workspace:*",
"@storybook/types": "workspace:*",
"memoizerific": "^1.11.3",
Expand Down
100 changes: 37 additions & 63 deletions code/ui/components/src/new/Button/Button.stories.tsx
@@ -1,7 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';

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

const meta: Meta<typeof Button> = {
Expand All @@ -17,110 +15,86 @@ export const Base = {
args: { children: 'Button' },
};

export const Types: Story = {
export const Variants: Story = {
args: {
...Base.args,
},
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="tertiary">Tertiary</Button>
<Button variant="solid">Solid</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</div>
),
};

export const Active: Story = {
render: () => (
args: {
...Base.args,
},
render: (args) => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Button variant="primary" active>
Primary
<Button variant="solid" active {...args}>
Solid
</Button>
<Button variant="secondary" active>
Secondary
<Button variant="outline" active {...args}>
Outline
</Button>
<Button variant="tertiary" active>
Tertiary
<Button variant="ghost" active {...args}>
Ghost
</Button>
</div>
),
};

export const WithIcon: Story = {
render: () => (
args: {
...Base.args,
icon: 'FaceHappy',
},
render: ({ icon, children }) => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Button variant="primary" icon={<Icon.FaceHappy />}>
Primary
<Button variant="solid" icon={icon}>
{children}
</Button>
<Button variant="secondary" icon={<Icon.FaceHappy />}>
Secondary
<Button variant="outline" icon={icon}>
{children}
</Button>
<Button variant="tertiary" icon={<Icon.FaceHappy />}>
Tertiary
<Button variant="ghost" icon={icon}>
{children}
</Button>
</div>
),
};

export const Sizes: Story = {
args: {
...Base.args,
},
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Button size="small" icon={<Icon.FaceHappy />}>
<Button size="small" icon="FaceHappy">
Small Button
</Button>
<Button size="small" icon={<Icon.FaceHappy />} iconOnly />
<Button size="medium" icon={<Icon.FaceHappy />}>
<Button size="medium" icon="FaceHappy">
Medium Button
</Button>
<Button size="medium" icon={<Icon.FaceHappy />} iconOnly />
</div>
),
};

export const IconOnly: Story = {
parameters: {
docs: {
description: {
story: 'This is a story that shows how to use the `iconOnly` prop.',
},
source: {
type: 'dynamic',
},
},
},
render: () => (
<>
<Button size="small" variant="primary" iconOnly icon={<Icon.FaceHappy />} />
<Button size="small" variant="secondary" iconOnly icon={<Icon.FaceHappy />} />
<Button size="small" variant="tertiary" iconOnly icon={<Icon.FaceHappy />} />
<Button size="medium" variant="primary" iconOnly icon={<Icon.FaceHappy />} />
<Button size="medium" variant="secondary" iconOnly icon={<Icon.FaceHappy />} />
<Button size="medium" variant="tertiary" iconOnly icon={<Icon.FaceHappy />} />
</>
),
decorators: [
(Story) => <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>{Story()}</div>,
],
};

export const IconOnlyActive: Story = {
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Button size="small" variant="primary" iconOnly icon={<Icon.FaceHappy />} active />
<Button size="small" variant="secondary" iconOnly icon={<Icon.FaceHappy />} active />
<Button size="small" variant="tertiary" iconOnly icon={<Icon.FaceHappy />} active />
<Button size="medium" variant="primary" iconOnly icon={<Icon.FaceHappy />} active />
<Button size="medium" variant="secondary" iconOnly icon={<Icon.FaceHappy />} active />
<Button size="medium" variant="tertiary" iconOnly icon={<Icon.FaceHappy />} active />
</div>
),
};

export const Disabled: Story = {
args: {
...Base.args,
disabled: true,
children: 'Disabled Button',
},
};

export const WithHref: Story = {
args: {
...Base.args,
},
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<Button onClick={() => console.log('Hello')}>I am a button using onClick</Button>
Expand Down
80 changes: 29 additions & 51 deletions code/ui/components/src/new/Button/Button.tsx
@@ -1,44 +1,34 @@
import type { ReactNode } from 'react';
import React, { forwardRef } from 'react';
import { styled } from '@storybook/theming';
import { darken, lighten, rgba, transparentize } from 'polished';
import type { Icons } from '@storybook/icons';
import type { PropsOf } from '../utils/types';
import { Icon } from '../Icon/Icon';

interface CommonProps<T extends React.ElementType = React.ElementType> {
interface ButtonProps<T extends React.ElementType = React.ElementType> {
children: string;
as?: T;
size?: 'small' | 'medium';
variant?: 'primary' | 'secondary' | 'tertiary';
variant?: 'solid' | 'outline' | 'ghost';
onClick?: () => void;
disabled?: boolean;
active?: boolean;
icon?: Icons;
}

type ButtonIconOnlyProps = {
iconOnly: true;
icon: ReactNode;
children?: never;
};

type ButtonWithTextProps = {
iconOnly?: false;
icon?: ReactNode;
children: string;
};

type ButtonProps<T extends React.ElementType = React.ElementType> = CommonProps<T> &
(ButtonIconOnlyProps | ButtonWithTextProps);

export const Button: {
<E extends React.ElementType = 'button'>(
props: ButtonProps<E> & Omit<PropsOf<E>, keyof ButtonProps>
): JSX.Element;
displayName?: string;
} = forwardRef(
({ as, children, icon, ...props }: ButtonProps, ref: React.Ref<HTMLButtonElement>) => {
const LocalIcon = Icon[icon];

return (
<StyledButton as={as} ref={ref} {...props}>
{icon}
{!props.iconOnly && children}
{icon && <LocalIcon />}
kasperpeulen marked this conversation as resolved.
Show resolved Hide resolved
{children}
</StyledButton>
);
}
Expand All @@ -47,14 +37,7 @@ export const Button: {
Button.displayName = 'Button';

const StyledButton = styled.button<Omit<ButtonProps, 'children'>>(
({
theme,
variant = 'primary',
size = 'medium',
disabled = false,
active = false,
iconOnly = false,
}) => ({
({ theme, variant = 'solid', size = 'medium', disabled = false, active = false }) => ({
border: 0,
cursor: disabled ? 'not-allowed' : 'pointer',
display: 'inline-flex',
Expand All @@ -63,15 +46,10 @@ const StyledButton = styled.button<Omit<ButtonProps, 'children'>>(
justifyContent: 'center',
overflow: 'hidden',
padding: `${(() => {
if (!iconOnly && size === 'small') return '0 10px';
if (!iconOnly && size === 'medium') return '0 12px';
if (size === 'small') return '0 10px';
if (size === 'medium') return '0 12px';
return 0;
})()}`,
width: `${(() => {
if (iconOnly && size === 'small') return '28px';
if (iconOnly && size === 'medium') return '32px';
return 'auto';
})()}`,
height: size === 'small' ? '28px' : '32px',
position: 'relative',
textAlign: 'center',
Expand All @@ -88,41 +66,41 @@ const StyledButton = styled.button<Omit<ButtonProps, 'children'>>(
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
62 changes: 62 additions & 0 deletions code/ui/components/src/new/IconButton/IconButton.stories.tsx
@@ -0,0 +1,62 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';
import { IconButton } from './IconButton';

const meta: Meta<typeof IconButton> = {
title: 'IconButton',
component: IconButton,
tags: ['autodocs'],
};

export default meta;
type Story = StoryObj<typeof IconButton>;

export const Base = {
args: { icon: 'FaceHappy' },
};

export const Types: Story = {
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<IconButton variant="solid" icon="FaceHappy" />
<IconButton variant="outline" icon="FaceHappy" />
<IconButton variant="ghost" icon="FaceHappy" />
</div>
),
};

export const Active: Story = {
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<IconButton variant="solid" icon="FaceHappy" active />
<IconButton variant="outline" icon="FaceHappy" active />
<IconButton variant="ghost" icon="FaceHappy" active />
</div>
),
};

export const Sizes: Story = {
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<IconButton size="small" icon="FaceHappy" />
<IconButton size="medium" icon="FaceHappy" />
</div>
),
};

export const Disabled: Story = {
args: {
...Base.args,
icon: 'FaceHappy',
disabled: true,
},
};

export const WithHref: Story = {
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<IconButton icon="FaceHappy" onClick={() => console.log('Hello')} />
<IconButton as="a" href="https://storybook.js.org/" icon="FaceHappy" />
</div>
),
};