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: Update IconButton and add new Toolbar component #23795

Merged
merged 2 commits into from
Aug 11, 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
1 change: 1 addition & 0 deletions code/ui/components/package.json
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
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
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
@@ -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
@@ -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
@@ -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
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