diff --git a/MIGRATION.md b/MIGRATION.md index ceb73feaba6a..fea774ce9279 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -3,6 +3,7 @@ - [From version 7.x to 8.0.0](#from-version-7x-to-800) - [Core changes](#core-changes) - [UI layout state has changed shape](#ui-layout-state-has-changed-shape) + - [New UI and props for Button and IconButton components](#new-ui-and-props-for-button-and-iconbutton-components) - [From version 7.4.0 to 7.5.0](#from-version-740-to-750) - [`storyStoreV6` and `storiesOf` is deprecated](#storystorev6-and-storiesof-is-deprecated) - [`storyIndexers` is replaced with `experimental_indexers`](#storyindexers-is-replaced-with-experimental_indexers) @@ -320,6 +321,24 @@ In Storybook 7 it was possible to use `addons.setConfig({...});` to configure St - `showPanel: boolean` is now split into `bottomPanelHeight: number` and `rightPanelWidth: number`, where the numbers represents the size of the panel in pixels. - `isFullscreen: boolean` is no longer supported, but can be achieved by setting a combination of the above. +#### New UI and props for Button and IconButton components + +We used to have a lot of different buttons in `@storybook/components` that were not used anywhere. In Storybook 8.0 we are deprecating `Form.Button` and added a new `Button` component that can be used in all places. The `IconButton` component has also been updated to use the new `Button` component under the hood. Going forward addon creators and Storybook maintainers should use the new `Button` component instead of `Form.Button`. + +For the `Button` component, the following props are now deprecated: + +- `isLink` - Please use the `asChild` prop instead like this: `Link` +- `primary` - Please use the `variant` prop instead. +- `secondary` - Please use the `variant` prop instead. +- `tertiary` - Please use the `variant` prop instead. +- `gray` - Please use the `variant` prop instead. +- `inForm` - Please use the `variant` prop instead. +- `small` - Please use the `size` prop instead. +- `outline` - Please use the `variant` prop instead. +- `containsIcon`. Please add your icon as a child directly. No need for this prop anymore. + +The `IconButton` doesn't have any deprecated props but it now uses the new `Button` component under the hood so all props for `IconButton` will be the same as `Button`. + ## From version 7.4.0 to 7.5.0 #### `storyStoreV6` and `storiesOf` is deprecated diff --git a/code/ui/blocks/src/controls/Object.tsx b/code/ui/blocks/src/controls/Object.tsx index 11c51f8f6392..e3cf71bef394 100644 --- a/code/ui/blocks/src/controls/Object.tsx +++ b/code/ui/blocks/src/controls/Object.tsx @@ -288,7 +288,6 @@ export const ObjectControl: FC = ({ name, value, onChange }) => { {['Object', 'Array'].includes(getObjectType(data)) && ( { e.preventDefault(); setShowRaw((v) => !v); diff --git a/code/ui/components/package.json b/code/ui/components/package.json index 85c425ea05bf..4ae6bbe3e2f7 100644 --- a/code/ui/components/package.json +++ b/code/ui/components/package.json @@ -59,8 +59,7 @@ "prep": "../../../scripts/prepare/bundle.ts" }, "dependencies": { - "@radix-ui/react-select": "^1.2.2", - "@radix-ui/react-toolbar": "^1.0.4", + "@radix-ui/react-slot": "^1.0.2", "@storybook/client-logger": "workspace:*", "@storybook/csf": "^0.1.0", "@storybook/global": "^5.0.0", diff --git a/code/ui/components/src/components/Button/Button.deprecated.stories.tsx b/code/ui/components/src/components/Button/Button.deprecated.stories.tsx new file mode 100644 index 000000000000..7aaadc6140b4 --- /dev/null +++ b/code/ui/components/src/components/Button/Button.deprecated.stories.tsx @@ -0,0 +1,90 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { Button } from './Button'; +import { Icons } from '../icon/icon'; +import { Form } from '../form'; + +const meta: Meta = { + title: 'Button/Deprecated', + component: Button, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default = { args: { children: 'Default' } }; + +export const FormButton: Story = { + render: (args) => , + args: { children: 'Form.Button' }, +}; + +export const Primary: Story = { args: { primary: true, children: 'Primary' } }; +export const Secondary: Story = { args: { secondary: true, children: 'Secondary' } }; +export const Tertiary: Story = { args: { tertiary: true, children: 'Tertiary' } }; +export const Gray: Story = { args: { gray: true, children: 'Gray' } }; + +export const Outline: Story = { args: { outline: true, children: 'Outline' } }; +export const OutlinePrimary: Story = { + args: { outline: true, primary: true, children: 'Outline Primary' }, +}; +export const OutlineSecondary: Story = { + args: { outline: true, secondary: true, children: 'Outline Secondary' }, +}; +export const OutlineTertiary: Story = { + args: { outline: true, tertiary: true, children: 'Outline Tertiary' }, +}; + +export const Disabled: Story = { args: { disabled: true, children: 'Disabled' } }; +export const DisabledPrimary: Story = { + args: { disabled: true, primary: true, children: 'Disabled Priary' }, +}; +export const DisabledSecondary: Story = { + args: { disabled: true, secondary: true, children: 'Disabled Secondary' }, +}; +export const DisabledTertiary: Story = { + args: { disabled: true, tertiary: true, children: 'Disabled Tertiary' }, +}; +export const DisabledGray: Story = { + args: { disabled: true, gray: true, children: 'Disabled Gray' }, +}; + +export const Small: Story = { args: { small: true, children: 'Small' } }; +export const SmallPrimary: Story = { + args: { small: true, primary: true, children: 'Small Priary' }, +}; +export const SmallSecondary: Story = { + args: { small: true, secondary: true, children: 'Small Secondary' }, +}; +export const SmallTertiary: Story = { + args: { small: true, tertiary: true, children: 'Small Tertiary' }, +}; +export const SmallGray: Story = { + args: { small: true, gray: true, children: 'Small Gray' }, +}; + +export const IsLink: Story = { + args: { isLink: true, children: 'Button as a link' }, +}; + +export const IconPrimary: Story = { + args: { + primary: true, + containsIcon: true, + title: 'link', + children: , + }, +}; +export const IconOutline: Story = { + args: { outline: true, containsIcon: true, title: 'link', children: }, +}; +export const IconOutlineSmall: Story = { + args: { + outline: true, + containsIcon: true, + small: true, + title: 'link', + children: , + }, +}; diff --git a/code/ui/components/src/components/Button/Button.stories.tsx b/code/ui/components/src/components/Button/Button.stories.tsx index e1fc021087b8..bdf87aeab2e7 100644 --- a/code/ui/components/src/components/Button/Button.stories.tsx +++ b/code/ui/components/src/components/Button/Button.stories.tsx @@ -1,82 +1,185 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import type { ReactNode } from 'react'; import React from 'react'; -import type { Args } from '@storybook/types'; - import { Button } from './Button'; import { Icons } from '../icon/icon'; -import { Form } from '../form/index'; -export default { +const meta = { + title: 'Button', component: Button, -}; + args: { children: 'Button' }, +} satisfies Meta; -export const Default = { args: { children: 'Default' } }; +export default meta; +type Story = StoryObj; -export const FormButton = { - render: (args: Args) => , - args: { children: 'Form.Button' }, -}; +const Stack = ({ children }: { children: ReactNode }) => ( + {children} +); -export const Primary = { args: { primary: true, children: 'Primary' } }; -export const Secondary = { args: { secondary: true, children: 'Secondary' } }; -export const Tertiary = { args: { tertiary: true, children: 'Tertiary' } }; -export const Gray = { args: { gray: true, children: 'Gray' } }; +const Row = ({ children }: { children: ReactNode }) => ( + {children} +); -export const Outline = { args: { outline: true, children: 'Outline' } }; -export const OutlinePrimary = { - args: { outline: true, primary: true, children: 'Outline Primary' }, -}; -export const OutlineSecondary = { - args: { outline: true, secondary: true, children: 'Outline Secondary' }, -}; -export const OutlineTertiary = { - args: { outline: true, tertiary: true, children: 'Outline Tertiary' }, -}; +export const Base: Story = {}; -export const Disabled = { args: { disabled: true, children: 'Disabled' } }; -export const DisabledPrimary = { - args: { disabled: true, primary: true, children: 'Disabled Priary' }, -}; -export const DisabledSecondary = { - args: { disabled: true, secondary: true, children: 'Disabled Secondary' }, -}; -export const DisabledTertiary = { - args: { disabled: true, tertiary: true, children: 'Disabled Tertiary' }, -}; -export const DisabledGray = { - args: { disabled: true, gray: true, children: 'Disabled Gray' }, +export const Variants: Story = { + render: (args) => ( + + + + Solid + + + Outline + + + Ghost + + + + + Solid + + + Outline + + + Ghost + + + + + + + + + + + + + + + ), }; -export const Small = { args: { small: true, children: 'Small' } }; -export const SmallPrimary = { - args: { small: true, primary: true, children: 'Small Priary' }, +export const Active: Story = { + args: { + active: true, + children: ( + <> + + Button + > + ), + }, + render: (args) => ( + + + + + + ), }; -export const SmallSecondary = { - args: { small: true, secondary: true, children: 'Small Secondary' }, + +export const WithIcon: Story = { + args: { + children: ( + <> + + Button + > + ), + }, + render: (args) => ( + + + + + + ), }; -export const SmallTertiary = { - args: { small: true, tertiary: true, children: 'Small Tertiary' }, + +export const IconOnly: Story = { + args: { + children: , + padding: 'small', + }, + render: (args) => ( + + + + + + ), }; -export const SmallGray = { - args: { small: true, gray: true, children: 'Small Gray' }, + +export const Sizes: Story = { + render: () => ( + + Small Button + Medium Button + + ), }; -export const IconPrimary = { +export const Disabled: Story = { args: { - primary: true, - containsIcon: true, - title: 'link', - children: , + disabled: true, + children: 'Disabled Button', }, }; -export const IconOutline = { - args: { outline: true, containsIcon: true, title: 'link', children: }, + +export const WithHref: Story = { + render: () => ( + + console.log('Hello')}>I am a button using onClick + + I am an anchor using Href + + + ), }; -export const IconOutlineSmall = { + +export const Animated: Story = { args: { - outline: true, - containsIcon: true, - small: true, - title: 'link', - children: , + variant: 'outline', }, + render: (args) => ( + + + + Button + + + Button + + + Button + + + + + Button + + + Button + + + Button + + + + + + + + + + + + + + + ), }; diff --git a/code/ui/components/src/components/Button/Button.tsx b/code/ui/components/src/components/Button/Button.tsx index 72598c214ec7..16744abb3472 100644 --- a/code/ui/components/src/components/Button/Button.tsx +++ b/code/ui/components/src/components/Button/Button.tsx @@ -1,272 +1,218 @@ -import type { FC, ComponentProps, ReactNode } from 'react'; -import React, { forwardRef } from 'react'; -import { styled } from '@storybook/theming'; +import type { ButtonHTMLAttributes, SyntheticEvent } from 'react'; +import React, { forwardRef, useEffect, useState } from 'react'; +import { isPropValid, styled } from '@storybook/theming'; import { darken, lighten, rgba, transparentize } from 'polished'; +import { Slot } from '@radix-ui/react-slot'; +import { deprecate } from '@storybook/client-logger'; + +export interface ButtonProps extends ButtonHTMLAttributes { + asChild?: boolean; + size?: 'small' | 'medium'; + padding?: 'small' | 'medium'; + variant?: 'outline' | 'solid' | 'ghost'; + onClick?: (event: SyntheticEvent) => void; + disabled?: boolean; + active?: boolean; + animation?: 'none' | 'rotate360' | 'glow' | 'jiggle'; -const ButtonWrapper = styled.button<{ + /** @deprecated Use {@link asChild} instead */ isLink?: boolean; + /** @deprecated Use {@link variant} instead */ primary?: boolean; + /** @deprecated Use {@link variant} instead */ secondary?: boolean; + /** @deprecated Use {@link variant} instead */ tertiary?: boolean; + /** @deprecated Use {@link variant} instead */ gray?: boolean; + /** @deprecated Use {@link variant} instead */ inForm?: boolean; - disabled?: boolean; + /** @deprecated Use {@link size} instead */ small?: boolean; + /** @deprecated Use {@link variant} instead */ outline?: boolean; + /** @deprecated Add your icon as a child directly */ containsIcon?: boolean; - children?: ReactNode; - href?: string; -}>( - ({ small, theme }) => ({ - border: 0, - borderRadius: '3em', - cursor: 'pointer', - display: 'inline-block', - overflow: 'hidden', - padding: small ? '8px 16px' : '13px 20px', - position: 'relative', - textAlign: 'center', - textDecoration: 'none', - transitionProperty: 'background, box-shadow', - transitionDuration: '150ms', - transitionTimingFunction: 'ease-out', - verticalAlign: 'top', - whiteSpace: 'nowrap', - userSelect: 'none', - opacity: 1, - margin: 0, - background: 'transparent', - - fontSize: `${small ? theme.typography.size.s1 : theme.typography.size.s2 - 1}px`, - fontWeight: theme.typography.weight.bold, - lineHeight: '1', - - svg: { - display: 'inline-block', - height: small ? 12 : 14, - width: small ? 12 : 14, - - verticalAlign: 'top', - marginRight: small ? 4 : 6, - marginTop: small ? 0 : -1, - marginBottom: small ? 0 : -1, +} - /* Necessary for js mouse events to not glitch out when hovering on svgs */ - pointerEvents: 'none', - - path: { - fill: 'currentColor', - }, +export const Button = forwardRef( + ( + { + asChild = false, + animation = 'none', + size = 'small', + variant = 'outline', + padding = 'medium', + disabled = false, + active = false, + onClick, + ...props }, - }), - ({ disabled }) => - disabled - ? { - cursor: 'not-allowed !important', - opacity: 0.5, - '&:hover': { - transform: 'none', - }, - } - : {}, - ({ containsIcon, small }) => - containsIcon - ? { - svg: { - display: 'block', - margin: 0, - }, - ...(small ? { padding: 10 } : { padding: 13 }), - } - : {}, - ({ theme, primary, secondary, gray }) => { - let color; - - if (gray) { - color = theme.color.mediumlight; - } else if (secondary) { - color = theme.color.secondary; - } else if (primary) { - color = theme.color.primary; + ref + ) => { + let Comp: 'button' | 'a' | typeof Slot = 'button'; + if (props.isLink) Comp = 'a'; + if (asChild) Comp = Slot; + let localVariant = variant; + let localSize = size; + + const [isAnimating, setIsAnimating] = useState(false); + + const handleClick = (event: SyntheticEvent) => { + if (onClick) onClick(event); + if (animation === 'none') return; + setIsAnimating(true); + }; + + useEffect(() => { + const timer = setTimeout(() => { + if (isAnimating) setIsAnimating(false); + }, 1000); + return () => clearTimeout(timer); + }, [isAnimating]); + + // Match the old API with the new API + // TODO: Remove this after 9.0 + if (props.primary) { + localVariant = 'solid'; + localSize = 'medium'; } - return color - ? { - background: color, - color: gray ? theme.color.darkest : theme.color.lightest, - - '&:hover': { - background: darken(0.05, color), - }, - '&:active': { - boxShadow: 'rgba(0, 0, 0, 0.1) 0 0 0 3em inset', - }, - '&:focus': { - boxShadow: `${rgba(color, 1)} 0 1px 9px 2px`, - outline: 'none', - }, - '&:focus:hover': { - boxShadow: `${rgba(color, 0.2)} 0 8px 18px 0px`, - }, - } - : {}; - }, - ({ theme, tertiary, inForm, small }) => - tertiary - ? { - background: theme.button.background, - color: theme.input.color, - boxShadow: `${theme.button.border} 0 0 0 1px inset`, - borderRadius: theme.input.borderRadius, - - ...(inForm && small ? { padding: '10px 16px' } : {}), - - '&:hover': { - background: - theme.base === 'light' - ? darken(0.02, theme.button.background) - : lighten(0.03, theme.button.background), - ...(inForm - ? {} - : { - boxShadow: 'rgba(0,0,0,.2) 0 2px 6px 0, rgba(0,0,0,.1) 0 0 0 1px inset', - }), - }, - '&:active': { - background: theme.button.background, - }, - '&:focus': { - boxShadow: `${rgba(theme.color.secondary, 1)} 0 0 0 1px inset`, - outline: 'none', - }, - } - : {}, - ({ theme, outline }) => - outline - ? { - boxShadow: `${transparentize(0.8, theme.color.defaultText)} 0 0 0 1px inset`, - color: transparentize(0.3, theme.color.defaultText), - background: 'transparent', - - '&:hover, &:focus': { - boxShadow: `${transparentize(0.5, theme.color.defaultText)} 0 0 0 1px inset`, - outline: 'none', - }, + // Match the old API with the new API + // TODO: Remove this after 9.0 + if (props.secondary || props.tertiary || props.gray || props.outline || props.inForm) { + localVariant = 'outline'; + localSize = 'medium'; + } - '&:active': { - boxShadow: `${transparentize(0.5, theme.color.defaultText)} 0 0 0 2px inset`, - color: transparentize(0, theme.color.defaultText), - }, - } - : {}, - ({ theme, outline, primary }) => { - const color = theme.color.primary; + if ( + props.small || + props.isLink || + props.primary || + props.secondary || + props.tertiary || + props.gray || + props.outline || + props.inForm || + props.containsIcon + ) { + const buttonContent = React.Children.toArray(props.children).filter( + (e) => typeof e === 'string' && e !== '' + ); - return outline && primary - ? { - boxShadow: `${color} 0 0 0 1px inset`, - color, + deprecate( + `Use of deprecated props in the button ${ + buttonContent.length > 0 ? `"${buttonContent.join(' ')}"` : 'component' + } detected, see the migration notes at https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#new-ui-and-props-for-button-and-iconbutton-components` + ); + } - 'svg path:not([fill])': { - fill: color, - }, + return ( + + ); + } +); - '&:hover': { - boxShadow: `${color} 0 0 0 1px inset`, - background: 'transparent', - }, +Button.displayName = 'Button'; - '&:active': { - background: color, - boxShadow: `${color} 0 0 0 1px inset`, - color: theme.color.tertiary, - }, - '&:focus': { - boxShadow: `${color} 0 0 0 1px inset, ${rgba(color, 0.4)} 0 1px 9px 2px`, - outline: 'none', - }, - '&:focus:hover': { - boxShadow: `${color} 0 0 0 1px inset, ${rgba(color, 0.2)} 0 8px 18px 0px`, - }, - } - : {}; +const StyledButton = styled('button', { + shouldForwardProp: (prop) => isPropValid(prop), +})< + ButtonProps & { + animating: boolean; + animation: ButtonProps['animation']; + } +>(({ theme, variant, size, disabled, active, animating, animation, padding }) => ({ + border: 0, + cursor: disabled ? 'not-allowed' : 'pointer', + display: 'inline-flex', + gap: '6px', + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + padding: (() => { + if (padding === 'small' && size === 'small') return '0 7px'; + if (padding === 'small' && size === 'medium') return '0 9px'; + if (size === 'small') return '0 10px'; + if (size === 'medium') return '0 12px'; + return 0; + })(), + 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 === '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 === '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 === 'outline' ? `${theme.button.border} 0 0 0 1px inset` : 'none', + borderRadius: theme.input.borderRadius, + // Making sure that the button never shrinks below its minimum size + flexShrink: 0, + + '&:hover': { + color: variant === 'ghost' ? theme.color.secondary : null, + background: (() => { + let bgColor = theme.color.secondary; + if (variant === 'solid') bgColor = theme.color.secondary; + if (variant === 'outline') bgColor = theme.button.background; + + if (variant === 'ghost') return transparentize(0.86, theme.color.secondary); + return theme.base === 'light' ? darken(0.02, bgColor) : lighten(0.03, bgColor); + })(), }, - ({ theme, outline, primary, secondary }) => { - let color; - - if (secondary) { - color = theme.color.secondary; - } else if (primary) { - color = theme.color.primary; - } - - return outline && color - ? { - boxShadow: `${color} 0 0 0 1px inset`, - color, - 'svg path:not([fill])': { - fill: color, - }, + '&:active': { + color: variant === 'ghost' ? theme.color.secondary : null, + background: (() => { + let bgColor = theme.color.secondary; + if (variant === 'solid') bgColor = theme.color.secondary; + if (variant === 'outline') bgColor = theme.button.background; - '&:hover': { - boxShadow: `${color} 0 0 0 1px inset`, - background: 'transparent', - }, - - '&:active': { - background: color, - boxShadow: `${color} 0 0 0 1px inset`, - color: theme.color.tertiary, - }, - '&:focus': { - boxShadow: `${color} 0 0 0 1px inset, ${rgba(color, 0.4)} 0 1px 9px 2px`, - outline: 'none', - }, - '&:focus:hover': { - boxShadow: `${color} 0 0 0 1px inset, ${rgba(color, 0.2)} 0 8px 18px 0px`, - }, - } - : {}; - } -); + if (variant === 'ghost') return theme.background.hoverable; + return theme.base === 'light' ? darken(0.02, bgColor) : lighten(0.03, bgColor); + })(), + }, -const ButtonLink = ButtonWrapper.withComponent('a'); + '&:focus': { + boxShadow: `${rgba(theme.color.secondary, 1)} 0 0 0 1px inset`, + outline: 'none', + }, -export const Button: FC> = Object.assign( - forwardRef< - any, - { - isLink?: boolean; - primary?: boolean; - secondary?: boolean; - tertiary?: boolean; - gray?: boolean; - inForm?: boolean; - disabled?: boolean; - small?: boolean; - outline?: boolean; - containsIcon?: boolean; - children?: ReactNode; - href?: string; - } - >(function Button({ isLink, children, ...props }, ref) { - if (isLink) { - return ( - - {children} - - ); - } - return ( - - {children} - - ); - }), - { - defaultProps: { - isLink: false, - }, - } -); + '> svg': { + animation: + animating && animation !== 'none' ? `${theme.animation[animation]} 1000ms ease-out` : '', + }, +})); diff --git a/code/ui/components/src/components/Button/Docs.mdx b/code/ui/components/src/components/Button/Docs.mdx new file mode 100644 index 000000000000..a3259c4667c3 --- /dev/null +++ b/code/ui/components/src/components/Button/Docs.mdx @@ -0,0 +1,159 @@ +import { Canvas, Meta, Controls, Source } from '@storybook/blocks'; + +import * as ButtonStories from './Button.stories'; + + + +# Button + +Button component is used to trigger an action or event, such as submitting a form, opening a Dialog, canceling an action, or performing a delete operation. + +## Import + + + +## Usage +Hello world! + +// Using the asChild prop to render a custom child + + Hello world! + +`} /> + + + +### Button sizes + +Use the `size` prop to change the size of the button. You can set the value to `small` or `medium`. + + +Small Button +Medium Button +`} /> + +### Button variants + +Use the `variant` prop to change the visual style of the button. You can set the value to `outline`, `solid` or `ghost`. + + +Outline +Solid +Ghost +`} /> + +### Button with icon + +You can add an icon to the button by adding the icon on the left of the text. Please use any icon from the icon library `@storybook/icons`. + + + + Button + +`} /> + +### Icon only buttons + +You can also use the button as an icon only button by removing the text. to make sure the button is square, please set the padding prop to `small` + + + + + +`} /> + +### Button with custom wrapper + +If you want to use a custom wrapper to set the button as an external link or to use your custom router, you can use the `asChild` prop. This will render the button as a child of the wrapper. + + + + Hello world! + + + Hello world! + +`} /> + +### Button with animations + +You can use the `animate` prop to add animations to the button. You can set the value to `glow`, `jiggle` or `rotate360`. + + + + Button + + + Button + + + Button + +`} /> + +### Active button + +You can use the `active` prop to set the button as active. This will change the background color of the button. + + + + Button + + `} +/> + +### Disabled button + +You can use the `disabled` prop to set the button as disabled. + + + + Button + + `} +/> \ No newline at end of file diff --git a/code/ui/components/src/components/IconButton/IconButton.stories.tsx b/code/ui/components/src/components/IconButton/IconButton.stories.tsx new file mode 100644 index 000000000000..2f88a3e898a2 --- /dev/null +++ b/code/ui/components/src/components/IconButton/IconButton.stories.tsx @@ -0,0 +1,81 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { IconButton } from './IconButton'; +import { Icons } from '../icon/icon'; + +const meta = { + title: 'IconButton', + component: IconButton, + tags: ['autodocs'], + args: { children: }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Base = {}; + +export const Types: Story = { + render: ({ ...args }) => ( + + + + + + ), +}; + +export const Active: Story = { + args: { active: true }, + render: ({ ...args }) => ( + + + + + + ), +}; + +export const Sizes: Story = { + args: { variant: 'solid' }, + render: ({ ...args }) => ( + + + + + ), +}; + +export const Disabled: Story = { + args: { disabled: true }, + render: ({ ...args }) => ( + + + + + + ), +}; + +export const Animated: Story = { + render: ({ ...args }) => ( + + + + + + ), +}; + +export const WithHref: Story = { + render: ({ ...args }) => ( + + console.log('Hello')} /> + + + + + + + ), +}; diff --git a/code/ui/components/src/components/IconButton/IconButton.tsx b/code/ui/components/src/components/IconButton/IconButton.tsx new file mode 100644 index 000000000000..87088d342b52 --- /dev/null +++ b/code/ui/components/src/components/IconButton/IconButton.tsx @@ -0,0 +1,11 @@ +import React, { forwardRef } from 'react'; +import type { ButtonProps } from '../Button/Button'; +import { Button } from '../Button/Button'; + +export const IconButton = forwardRef( + ({ padding = 'small', variant = 'ghost', ...props }, ref) => { + return ; + } +); + +IconButton.displayName = 'IconButton'; diff --git a/code/ui/components/src/components/bar/button.stories.tsx b/code/ui/components/src/components/bar/button.stories.tsx deleted file mode 100644 index 6f2fb250168f..000000000000 --- a/code/ui/components/src/components/bar/button.stories.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; - -import { IconButton, IconButtonSkeleton } from './button'; -import { Icons } from '../icon/icon'; - -export default { - component: IconButton, -}; - -export const Loading = () => ; - -// eslint-disable-next-line no-underscore-dangle, @typescript-eslint/naming-convention -export const _IconButton = () => ( - - - -); - -export const Active = () => ( - - - -); - -export const Disabled = () => ( - - - -); - -export const WithText = () => ( - - - Howdy! - -); - -export const WithTextActive = () => ( - - - Howdy! - -); - -export const WithTextDisabled = () => ( - - - Howdy! - -); diff --git a/code/ui/components/src/components/bar/button.tsx b/code/ui/components/src/components/bar/button.tsx index ebc97f41fa50..6392b6710209 100644 --- a/code/ui/components/src/components/bar/button.tsx +++ b/code/ui/components/src/components/bar/button.tsx @@ -1,8 +1,6 @@ import type { AnchorHTMLAttributes, ButtonHTMLAttributes, DetailedHTMLProps } from 'react'; import React from 'react'; import { styled, isPropValid } from '@storybook/theming'; -import { transparentize } from 'polished'; -import { auto } from '@popperjs/core'; interface BarButtonProps extends DetailedHTMLProps, HTMLButtonElement> { @@ -95,54 +93,6 @@ export interface IconButtonProps { disabled?: boolean; } -export const IconButton = styled(ButtonOrLink, { shouldForwardProp: isPropValid })( - () => ({ - alignItems: 'center', - background: 'transparent', - border: 'none', - borderRadius: 4, - color: 'inherit', - cursor: 'pointer', - display: 'inline-flex', - fontSize: 13, - fontWeight: 'bold', - height: 28, - justifyContent: 'center', - marginTop: 6, - padding: '8px 7px', - - '& > svg': { - width: 14, - }, - }), - ({ active, theme }) => - active - ? { - backgroundColor: theme.background.hoverable, - color: theme.barSelectedColor, - } - : {}, - ({ disabled, theme }) => - disabled - ? { - opacity: 0.5, - cursor: 'not-allowed', - } - : { - '&:hover, &:focus-visible': { - background: transparentize(0.88, theme.color.secondary), - color: theme.barHoverColor, - }, - '&:focus-visible': { - outline: auto, // Ensures links have the same focus style - }, - '&:focus:not(:focus-visible)': { - outline: 'none', - }, - } -); -IconButton.displayName = 'IconButton'; - const IconPlaceholder = styled.div(({ theme }) => ({ width: 14, height: 14, diff --git a/code/ui/components/src/components/bar/separator.tsx b/code/ui/components/src/components/bar/separator.tsx index c43ebebad7b9..6a24a11d3386 100644 --- a/code/ui/components/src/components/bar/separator.tsx +++ b/code/ui/components/src/components/bar/separator.tsx @@ -6,8 +6,7 @@ export const Separator = styled.span( width: 1, height: 20, background: theme.appBorderColor, - marginTop: 10, - marginLeft: 6, + marginLeft: 2, marginRight: 2, }), ({ force }) => diff --git a/code/ui/components/src/components/tabs/tabs.stories.tsx b/code/ui/components/src/components/tabs/tabs.stories.tsx index cac0a76b4deb..ae766ca6f51f 100644 --- a/code/ui/components/src/components/tabs/tabs.stories.tsx +++ b/code/ui/components/src/components/tabs/tabs.stories.tsx @@ -12,8 +12,8 @@ import { } from '@storybook/testing-library'; import { Tabs, TabsState, TabWrapper } from './tabs'; import type { ChildrenList } from './tabs.helpers'; -import { IconButton } from '../bar/button'; import { Icons } from '../icon/icon'; +import { IconButton } from '../IconButton/IconButton'; const colours = Array.from(new Array(15), (val, index) => index).map((i) => Math.floor((1 / 15) * i * 16777215) diff --git a/code/ui/components/src/index.ts b/code/ui/components/src/index.ts index 44934fc3ad00..ccc7053f7b1f 100644 --- a/code/ui/components/src/index.ts +++ b/code/ui/components/src/index.ts @@ -50,6 +50,7 @@ export { ErrorFormatter } from './components/ErrorFormatter/ErrorFormatter'; // Forms export { Button } from './components/Button/Button'; +export { IconButton } from './components/IconButton/IconButton'; export { Form } from './components/form/index'; // Tooltips @@ -64,7 +65,7 @@ export { default as ListItem } from './components/tooltip/ListItem'; // Toolbar and subcomponents export { Tabs, TabsState, TabBar, TabWrapper } from './components/tabs/tabs'; -export { IconButton, IconButtonSkeleton, TabButton } from './components/bar/button'; +export { IconButtonSkeleton, TabButton } from './components/bar/button'; export { Separator, interleaveSeparators } from './components/bar/separator'; export { Bar, FlexBar } from './components/bar/bar'; export { AddonPanel } from './components/addon-panel/addon-panel'; diff --git a/code/ui/manager/src/components/panel/Panel.tsx b/code/ui/manager/src/components/panel/Panel.tsx index b153e83fe0c4..1ad1b563a303 100644 --- a/code/ui/manager/src/components/panel/Panel.tsx +++ b/code/ui/manager/src/components/panel/Panel.tsx @@ -1,8 +1,9 @@ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; import { Tabs, Icons, IconButton } from '@storybook/components'; import type { State } from '@storybook/manager-api'; import { shortcutToHumanString } from '@storybook/manager-api'; import type { Addon_BaseType } from '@storybook/types'; +import { styled } from '@storybook/theming'; import { useLayout } from '../layout/LayoutProvider'; export interface SafeTabProps { @@ -58,30 +59,32 @@ export const AddonPanel = React.memo<{ menuName="Addons" actions={actions} tools={ - isDesktop ? ( - - - - - + + {isDesktop ? ( + <> + + + + + + + > + ) : ( + setMobilePanelOpen(false)} title="Close addon panel"> - - ) : ( - setMobilePanelOpen(false)} title="Close addon panel"> - - - ) + )} + } id="storybook-panel-root" > @@ -96,3 +99,9 @@ export const AddonPanel = React.memo<{ ); AddonPanel.displayName = 'AddonPanel'; + +const Actions = styled.div({ + display: 'flex', + alignItems: 'center', + gap: 6, +}); diff --git a/code/ui/manager/src/components/preview/FramesRenderer.tsx b/code/ui/manager/src/components/preview/FramesRenderer.tsx index fbd7d4cf1867..a1a89a9e5041 100644 --- a/code/ui/manager/src/components/preview/FramesRenderer.tsx +++ b/code/ui/manager/src/components/preview/FramesRenderer.tsx @@ -96,8 +96,10 @@ export const FramesRenderer: FC = ({ return null; } return ( - - Skip to sidebar + + + Skip to sidebar + ); }} diff --git a/code/ui/manager/src/components/preview/Toolbar.tsx b/code/ui/manager/src/components/preview/Toolbar.tsx index 9e81c650e02d..f7a29c09985f 100644 --- a/code/ui/manager/src/components/preview/Toolbar.tsx +++ b/code/ui/manager/src/components/preview/Toolbar.tsx @@ -278,7 +278,8 @@ const ToolbarLeft = styled.div({ display: 'flex', whiteSpace: 'nowrap', flexBasis: 'auto', - gap: 4, + gap: 6, + alignItems: 'center', }); const ToolbarRight = styled(ToolbarLeft)({ diff --git a/code/ui/manager/src/components/preview/tools/eject.tsx b/code/ui/manager/src/components/preview/tools/eject.tsx index 65688038b859..4d0a903919f3 100644 --- a/code/ui/manager/src/components/preview/tools/eject.tsx +++ b/code/ui/manager/src/components/preview/tools/eject.tsx @@ -28,13 +28,15 @@ export const ejectTool: Addon_BaseType = { {({ baseUrl, storyId, queryParams }) => storyId ? ( - - + + + + ) : null } diff --git a/code/ui/manager/src/components/sidebar/Heading.tsx b/code/ui/manager/src/components/sidebar/Heading.tsx index f360cab313d0..1cdb6e5fa2aa 100644 --- a/code/ui/manager/src/components/sidebar/Heading.tsx +++ b/code/ui/manager/src/components/sidebar/Heading.tsx @@ -88,8 +88,10 @@ export const Heading: FC> = return ( {skipLinkHref && ( - - Skip to canvas + + + Skip to canvas + )} diff --git a/code/ui/manager/src/components/sidebar/Tree.tsx b/code/ui/manager/src/components/sidebar/Tree.tsx index a430061f5bf4..dd50c5085115 100644 --- a/code/ui/manager/src/components/sidebar/Tree.tsx +++ b/code/ui/manager/src/components/sidebar/Tree.tsx @@ -241,8 +241,8 @@ const Node = React.memo(function Node({ {(item.renderLabel as (i: typeof item) => React.ReactNode)?.(item) || item.name} {isSelected && ( - - Skip to canvas + + Skip to canvas )} {icon ? ( diff --git a/code/ui/manager/src/settings/About.tsx b/code/ui/manager/src/settings/About.tsx index 5a62c40e6667..bd65795a66b4 100644 --- a/code/ui/manager/src/settings/About.tsx +++ b/code/ui/manager/src/settings/About.tsx @@ -72,20 +72,18 @@ const AboutScreen: FC<{ onNavigateToWhatsNew?: () => void }> = ({ onNavigateToWh