diff --git a/code/ui/components/package.json b/code/ui/components/package.json index 44cc62e34df9..86e7eae05e97 100644 --- a/code/ui/components/package.json +++ b/code/ui/components/package.json @@ -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", diff --git a/code/ui/components/src/new/Button/Button.stories.tsx b/code/ui/components/src/new/Button/Button.stories.tsx index e318d2e26bac..0bae84bdf8ec 100644 --- a/code/ui/components/src/new/Button/Button.stories.tsx +++ b/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 = { @@ -17,110 +15,86 @@ export const Base = { args: { children: 'Button' }, }; -export const Types: Story = { +export const Variants: Story = { + args: { + ...Base.args, + }, render: () => (
- - - + + +
), }; export const Active: Story = { - render: () => ( + args: { + ...Base.args, + }, + render: (args) => (
- - -
), }; export const WithIcon: Story = { - render: () => ( + args: { + ...Base.args, + icon: 'FaceHappy', + }, + render: ({ icon, children }) => (
- - -
), }; export const Sizes: Story = { + args: { + ...Base.args, + }, render: () => (
- - -
- ), -}; - -export const IconOnly: Story = { - parameters: { - docs: { - description: { - story: 'This is a story that shows how to use the `iconOnly` prop.', - }, - source: { - type: 'dynamic', - }, - }, - }, - render: () => ( - <> - diff --git a/code/ui/components/src/new/Button/Button.tsx b/code/ui/components/src/new/Button/Button.tsx index 204d99180652..f36ea73379d4 100644 --- a/code/ui/components/src/new/Button/Button.tsx +++ b/code/ui/components/src/new/Button/Button.tsx @@ -1,33 +1,21 @@ -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 { +interface ButtonProps { + 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 = CommonProps & - (ButtonIconOnlyProps | ButtonWithTextProps); - export const Button: { ( props: ButtonProps & Omit, keyof ButtonProps> @@ -35,10 +23,12 @@ export const Button: { displayName?: string; } = forwardRef( ({ as, children, icon, ...props }: ButtonProps, ref: React.Ref) => { + const LocalIcon = Icon[icon]; + return ( - {icon} - {!props.iconOnly && children} + {icon && } + {children} ); } @@ -47,14 +37,7 @@ export const Button: { Button.displayName = 'Button'; const StyledButton = styled.button>( - ({ - 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', @@ -63,15 +46,10 @@ const StyledButton = styled.button>( 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', @@ -88,41 +66,41 @@ const StyledButton = styled.button>( 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); })()}`, }, diff --git a/code/ui/components/src/new/IconButton/IconButton.stories.tsx b/code/ui/components/src/new/IconButton/IconButton.stories.tsx new file mode 100644 index 000000000000..4d4b82cf7e22 --- /dev/null +++ b/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 = { + title: 'IconButton', + component: IconButton, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Base = { + args: { icon: 'FaceHappy' }, +}; + +export const Types: Story = { + render: () => ( +
+ + + +
+ ), +}; + +export const Active: Story = { + render: () => ( +
+ + + +
+ ), +}; + +export const Sizes: Story = { + render: () => ( +
+ + +
+ ), +}; + +export const Disabled: Story = { + args: { + ...Base.args, + icon: 'FaceHappy', + disabled: true, + }, +}; + +export const WithHref: Story = { + render: () => ( +
+ console.log('Hello')} /> + +
+ ), +}; diff --git a/code/ui/components/src/new/IconButton/IconButton.tsx b/code/ui/components/src/new/IconButton/IconButton.tsx new file mode 100644 index 000000000000..9a0e1ad98ea9 --- /dev/null +++ b/code/ui/components/src/new/IconButton/IconButton.tsx @@ -0,0 +1,111 @@ +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 ButtonProps { + icon: Icons; + as?: T; + size?: 'small' | 'medium'; + variant?: 'solid' | 'outline' | 'ghost'; + onClick?: () => void; + disabled?: boolean; + active?: boolean; +} + +export const IconButton: { + ( + props: ButtonProps & Omit, keyof ButtonProps> + ): JSX.Element; + displayName?: string; +} = forwardRef( + ({ as, icon = 'FaceHappy', ...props }: ButtonProps, ref: React.Ref) => { + const LocalIcon = Icon[icon]; + + return ( + + {icon && } + + ); + } +); + +IconButton.displayName = 'IconButton'; + +const StyledButton = styled.button>( + ({ theme, variant = 'primary', size = 'medium', disabled = false, active = false }) => ({ + border: 0, + cursor: disabled ? 'not-allowed' : 'pointer', + display: 'inline-flex', + gap: '6px', + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + width: `${(() => { + if (size === 'small') return '28px'; + if (size === 'medium') return '32px'; + return 'auto'; + })()}`, + height: size === 'small' ? '28px' : '32px', + position: 'relative', + textAlign: 'center', + textDecoration: 'none', + transitionProperty: 'background, box-shadow', + transitionDuration: '150ms', + transitionTimingFunction: 'ease-out', + verticalAlign: 'top', + whiteSpace: 'nowrap', + userSelect: 'none', + opacity: disabled ? 0.5 : 1, + margin: 0, + fontSize: `${theme.typography.size.s1}px`, + 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; + 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; + return theme.input.color; + })()}`, + boxShadow: variant === 'secondary' ? `${theme.button.border} 0 0 0 1px inset` : 'none', + borderRadius: theme.input.borderRadius, + + '&:hover': { + color: variant === 'tertiary' ? 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 === 'tertiary') 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, + background: `${(() => { + let bgColor = theme.color.secondary; + if (variant === 'primary') bgColor = theme.color.secondary; + if (variant === 'secondary') bgColor = theme.button.background; + + if (variant === 'tertiary') return theme.background.hoverable; + return theme.base === 'light' ? darken(0.02, bgColor) : lighten(0.03, bgColor); + })()}`, + }, + + '&:focus': { + boxShadow: `${rgba(theme.color.secondary, 1)} 0 0 0 1px inset`, + outline: 'none', + }, + }) +); diff --git a/code/yarn.lock b/code/yarn.lock index a6ce5bb8a7bb..b05b56f86671 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6548,7 +6548,7 @@ __metadata: "@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:*" "@types/react-syntax-highlighter": 11.0.5 @@ -6872,13 +6872,13 @@ __metadata: languageName: unknown linkType: soft -"@storybook/icons@npm:^1.1.5": - version: 1.1.5 - resolution: "@storybook/icons@npm:1.1.5" +"@storybook/icons@npm:^1.1.6": + version: 1.1.6 + resolution: "@storybook/icons@npm:1.1.6" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 5c138fd53510d7eaaaede6ae6dc1a7242d1953563449367742733757f35731dd5ed5c6f1ccc9c989cf5d17dffa0eccd4468643d613c66f0017133c2bdc92c8ae + checksum: c8c2fb8f91c5c93b1cd6951c06a25f5b8203943cb95d7b6ef40015896596ed7444911fa556726c880902110f04e013f3def3fbae0eb6f7dbfcc96ec93fce9cb9 languageName: node linkType: hard