Skip to content

Commit

Permalink
Merge pull request #23795 from storybookjs/charles-ui-updates
Browse files Browse the repository at this point in the history
UI: Update IconButton and add new Toolbar component
  • Loading branch information
cdedreuille authored Aug 11, 2023
2 parents 3ba0d68 + d964d59 commit ae80dc4
Show file tree
Hide file tree
Showing 8 changed files with 386 additions and 9 deletions.
1 change: 1 addition & 0 deletions code/ui/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
},
"dependencies": {
"@radix-ui/react-select": "^1.2.2",
"@radix-ui/react-toolbar": "^1.0.4",
"@storybook/client-logger": "workspace:*",
"@storybook/csf": "^0.1.0",
"@storybook/global": "^5.0.0",
Expand Down
1 change: 1 addition & 0 deletions code/ui/components/src/experimental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export { Select } from './new/Select/Select';
export { Link } from './new/Link/Link';
export { Icon } from './new/Icon/Icon';
export { IconButton } from './new/IconButton/IconButton';
export { Toolbar } from './new/Toolbar/Toolbar';
14 changes: 14 additions & 0 deletions code/ui/components/src/new/IconButton/IconButton.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ export const Disabled: Story = {
},
};

export const Animated: Story = {
args: {
...Base.args,
icon: 'FaceHappy',
},
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<IconButton icon="FaceHappy" onClickAnimation="glow" />
<IconButton icon="FaceHappy" onClickAnimation="rotate360" />
<IconButton icon="FaceHappy" onClickAnimation="jiggle" />
</div>
),
};

export const WithHref: Story = {
render: () => (
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
Expand Down
46 changes: 38 additions & 8 deletions code/ui/components/src/new/IconButton/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,61 @@
import React, { forwardRef } from 'react';
import type { SyntheticEvent } from 'react';
import React, { forwardRef, useEffect, useState } 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<T extends React.ElementType = React.ElementType> {
interface IconButtonProps<T extends React.ElementType = React.ElementType> {
icon: Icons;
as?: T;
size?: 'small' | 'medium';
variant?: 'solid' | 'outline' | 'ghost';
onClick?: () => void;
onClick?: (event: SyntheticEvent) => void;
disabled?: boolean;
active?: boolean;
onClickAnimation?: 'none' | 'rotate360' | 'glow' | 'jiggle';
}

export const IconButton: {
<E extends React.ElementType = 'button'>(
props: ButtonProps<E> & Omit<PropsOf<E>, keyof ButtonProps>
props: IconButtonProps<E> & Omit<PropsOf<E>, keyof IconButtonProps>
): JSX.Element;
displayName?: string;
} = forwardRef(
({ as, icon = 'FaceHappy', ...props }: ButtonProps, ref: React.Ref<HTMLButtonElement>) => {
(
{ as, icon = 'FaceHappy', onClickAnimation = 'none', onClick, ...props }: IconButtonProps,
ref: React.Ref<HTMLButtonElement>
) => {
const LocalIcon = Icon[icon];
const [isAnimating, setIsAnimating] = useState(false);

const handleClick = (event: SyntheticEvent) => {
if (onClick) onClick(event);
if (onClickAnimation === 'none') return;
setIsAnimating(true);
};

useEffect(() => {
const timer = setTimeout(() => {
if (isAnimating) setIsAnimating(false);
}, 1000);
return () => clearTimeout(timer);
}, [isAnimating]);

return (
<StyledButton as={as} ref={ref} {...props}>
{icon && <LocalIcon />}
<StyledButton as={as} ref={ref} {...props} onClick={handleClick}>
<IconWrapper isAnimating={isAnimating} animation={onClickAnimation}>
<LocalIcon />
</IconWrapper>
</StyledButton>
);
}
);

IconButton.displayName = 'IconButton';

const StyledButton = styled.button<Omit<ButtonProps, 'icon'>>(
const StyledButton = styled.button<Omit<IconButtonProps, 'icon'>>(
({ theme, variant = 'solid', size = 'medium', disabled = false, active = false }) => ({
border: 0,
cursor: disabled ? 'not-allowed' : 'pointer',
Expand Down Expand Up @@ -109,3 +130,12 @@ const StyledButton = styled.button<Omit<ButtonProps, 'icon'>>(
},
})
);

const IconWrapper = styled.div<{
isAnimating: boolean;
animation: IconButtonProps['onClickAnimation'];
}>(({ theme, isAnimating, animation }) => ({
width: 14,
height: 14,
animation: isAnimating && animation !== 'none' && `${theme.animation[animation]} 1000ms ease-out`,
}));
117 changes: 117 additions & 0 deletions code/ui/components/src/new/Toolbar/Toolbar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';

import { Toolbar } from './Toolbar';
import { IconButton } from '../IconButton/IconButton';
import { Button } from '../Button/Button';

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

export default meta;
type Story = StoryObj<typeof Toolbar.Root>;

export const Base: Story = {
args: {
hasPadding: true,
borderTop: false,
borderBottom: true,
},
render: (_, { args }) => (
<Toolbar.Root {...args}>
<Toolbar.Left>
<Toolbar.ToogleGroup type="single">
<Toolbar.ToggleItem value="item1">
<IconButton icon="Sync" size="small" variant="ghost" onClickAnimation="rotate360" />
</Toolbar.ToggleItem>
<Toolbar.ToggleItem value="item2">
<IconButton icon="Zoom" size="small" variant="ghost" />
</Toolbar.ToggleItem>
<Toolbar.ToggleItem value="item2">
<IconButton icon="ZoomOut" size="small" variant="ghost" />
</Toolbar.ToggleItem>
<Toolbar.ToggleItem value="item2">
<IconButton icon="ZoomReset" size="small" variant="ghost" />
</Toolbar.ToggleItem>
</Toolbar.ToogleGroup>
<Toolbar.Separator />
<Toolbar.ToogleGroup type="single">
<Toolbar.ToggleItem value="item1">
<IconButton icon="Photo" size="small" variant="ghost" />
</Toolbar.ToggleItem>
<Toolbar.ToggleItem value="item2">
<IconButton icon="Grid" size="small" variant="ghost" />
</Toolbar.ToggleItem>
<Toolbar.ToggleItem value="item2">
<IconButton icon="Grow" size="small" variant="ghost" />
</Toolbar.ToggleItem>
</Toolbar.ToogleGroup>
<Toolbar.Separator />
<Toolbar.ToogleGroup type="single">
<Toolbar.ToggleItem value="item1">
<Button icon="CircleHollow" size="small" variant="ghost">
Theme
</Button>
</Toolbar.ToggleItem>
<Toolbar.ToggleItem value="item2">
<IconButton icon="Ruler" size="small" variant="ghost" />
</Toolbar.ToggleItem>
<Toolbar.ToggleItem value="item2">
<IconButton icon="Outline" size="small" variant="ghost" />
</Toolbar.ToggleItem>
</Toolbar.ToogleGroup>
</Toolbar.Left>
<Toolbar.Right>
<Toolbar.ToogleGroup type="single">
<Toolbar.ToggleItem value="item2">
<IconButton icon="Expand" size="small" variant="ghost" />
</Toolbar.ToggleItem>
<Toolbar.ToggleItem value="item2">
<IconButton icon="ShareAlt" size="small" variant="ghost" />
</Toolbar.ToggleItem>
<Toolbar.ToggleItem value="item2">
<IconButton icon="Link" size="small" variant="ghost" />
</Toolbar.ToggleItem>
</Toolbar.ToogleGroup>
</Toolbar.Right>
</Toolbar.Root>
),
};

export const NoMargin: Story = {
args: {
...Base.args,
hasPadding: false,
},
render: Base.render,
};

export const BorderTop: Story = {
args: {
...Base.args,
borderTop: true,
borderBottom: false,
},
render: Base.render,
};

export const BorderBottom: Story = {
args: {
...Base.args,
borderTop: false,
borderBottom: true,
},
render: Base.render,
};

export const BorderTopBottom: Story = {
args: {
...Base.args,
borderTop: true,
borderBottom: true,
},
render: Base.render,
};
83 changes: 83 additions & 0 deletions code/ui/components/src/new/Toolbar/Toolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { ComponentPropsWithoutRef, ElementRef } from 'react';
import React, { forwardRef } from 'react';
import * as ToolbarPrimitive from '@radix-ui/react-toolbar';
import { styled } from '@storybook/theming';

interface RootProps extends ComponentPropsWithoutRef<typeof ToolbarPrimitive.Root> {
hasPadding?: boolean;
borderBottom?: boolean;
borderTop?: boolean;
}

const ToolbarRoot = forwardRef<ElementRef<typeof ToolbarPrimitive.Root>, RootProps>(
({ className, children, ...props }, ref) => (
<StyledRoot ref={ref} {...props}>
{children}
</StyledRoot>
)
);
ToolbarRoot.displayName = ToolbarPrimitive.Root.displayName;

const ToolbarSeparator = React.forwardRef<
ElementRef<typeof ToolbarPrimitive.Separator>,
ComponentPropsWithoutRef<typeof ToolbarPrimitive.Separator>
>(({ className, ...props }, ref) => <StyledSeparator ref={ref} {...props} />);
ToolbarSeparator.displayName = ToolbarPrimitive.Separator.displayName;

const ToolbarToggleGroup = React.forwardRef<
ElementRef<typeof ToolbarPrimitive.ToggleGroup>,
ToolbarPrimitive.ToolbarToggleGroupSingleProps | ToolbarPrimitive.ToolbarToggleGroupMultipleProps
>(({ className, ...props }, ref) => <StyledToggleGroup ref={ref} {...props} />);
ToolbarToggleGroup.displayName = ToolbarPrimitive.ToggleGroup.displayName;

const ToolbarToggleItem = React.forwardRef<
ElementRef<typeof ToolbarPrimitive.ToggleItem>,
ComponentPropsWithoutRef<typeof ToolbarPrimitive.ToggleItem>
>(({ className, ...props }, ref) => <ToolbarPrimitive.ToggleItem ref={ref} {...props} asChild />);
ToolbarToggleItem.displayName = ToolbarPrimitive.ToggleItem.displayName;

const StyledRoot = styled(ToolbarPrimitive.Root)<RootProps>(
({ theme, hasPadding = true, borderBottom = true, borderTop = false }) => ({
display: 'flex',
padding: hasPadding ? '0 10px' : 0,
justifyContent: 'space-between',
height: 40,
borderBottom: borderBottom ? `1px solid ${theme.appBorderColor}` : 'none',
borderTop: borderTop ? `1px solid ${theme.appBorderColor}` : 'none',
boxSizing: 'border-box',
backgroundColor: theme.barBg,
})
);

const StyledSeparator = styled(ToolbarPrimitive.Separator)(({ theme }) => ({
width: 1,
height: 20,
backgroundColor: theme.appBorderColor,
}));

const StyledToggleGroup = styled(ToolbarPrimitive.ToggleGroup)({
display: 'flex',
gap: 5,
alignItems: 'center',
});

const Left = styled.div({
display: 'flex',
gap: 5,
alignItems: 'center',
});

const Right = styled.div({
display: 'flex',
gap: 5,
alignItems: 'center',
});

export const Toolbar = {
Root: ToolbarRoot,
Left,
Right,
ToogleGroup: ToolbarToggleGroup,
ToggleItem: ToolbarToggleItem,
Separator: ToolbarSeparator,
};
10 changes: 9 additions & 1 deletion code/ui/manager/src/globals/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,15 @@ export default {
'resetComponents',
'withReset',
],
'@storybook/components/experimental': ['Button', 'Icon', 'IconButton', 'Input', 'Link', 'Select'],
'@storybook/components/experimental': [
'Button',
'Icon',
'IconButton',
'Input',
'Link',
'Select',
'Toolbar',
],
'@storybook/channels': [
'Channel',
'PostMessageTransport',
Expand Down
Loading

0 comments on commit ae80dc4

Please sign in to comment.