refactor: improve IconButtonGroup and FloatingIconButtonGroup (#1518)
Closes #1411
This commit is contained in:
@ -68,17 +68,24 @@ const StyledButton = styled.button<
|
|||||||
font-family: ${({ theme }) => theme.font.family};
|
font-family: ${({ theme }) => theme.font.family};
|
||||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: background 0.1s ease;
|
transition: background 0.1s ease;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
width: ${({ size }) => (size === 'small' ? '24px' : '32px')};
|
${({ position, size }) => {
|
||||||
|
const sizeInPx =
|
||||||
|
(size === 'small' ? 24 : 32) - (position === 'standalone' ? 0 : 4);
|
||||||
|
|
||||||
&:hover .floating-icon-button-hovered {
|
return `
|
||||||
display: flex;
|
height: ${sizeInPx}px;
|
||||||
|
width: ${sizeInPx}px;
|
||||||
|
`;
|
||||||
|
}}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
@ -91,18 +98,6 @@ const StyledButton = styled.button<
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledHover = styled.div`
|
|
||||||
background: ${({ theme }) => theme.background.transparent.lighter};
|
|
||||||
border-radius: calc(${({ theme }) => theme.border.radius.sm} - 2px);
|
|
||||||
bottom: 2px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: none;
|
|
||||||
left: 2px;
|
|
||||||
position: absolute;
|
|
||||||
right: 2px;
|
|
||||||
top: 2px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function FloatingIconButton({
|
export function FloatingIconButton({
|
||||||
className,
|
className,
|
||||||
icon: initialIcon,
|
icon: initialIcon,
|
||||||
@ -135,7 +130,6 @@ export function FloatingIconButton({
|
|||||||
position={position}
|
position={position}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{!disabled && <StyledHover className="floating-icon-button-hovered" />}
|
|
||||||
{icon}
|
{icon}
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,59 +1,61 @@
|
|||||||
import React from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import type { IconComponent } from '@/ui/icon/types/IconComponent';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
FloatingIconButton,
|
||||||
FloatingIconButtonPosition,
|
FloatingIconButtonPosition,
|
||||||
FloatingIconButtonProps,
|
type FloatingIconButtonProps,
|
||||||
} from './FloatingIconButton';
|
} from './FloatingIconButton';
|
||||||
|
|
||||||
const StyledFloatingIconButtonGroupContainer = styled.div`
|
const StyledFloatingIconButtonGroupContainer = styled.div`
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
box-shadow: ${({ theme }) =>
|
box-shadow: ${({ theme }) =>
|
||||||
`0px 2px 4px 0px ${theme.background.transparent.light}, 0px 0px 4px 0px ${theme.background.transparent.medium}`};
|
`0px 2px 4px 0px ${theme.background.transparent.light}, 0px 0px 4px 0px ${theme.background.transparent.medium}`};
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 2px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type FloatingIconButtonGroupProps = Pick<
|
export type FloatingIconButtonGroupProps = Pick<
|
||||||
FloatingIconButtonProps,
|
FloatingIconButtonProps,
|
||||||
'size' | 'className'
|
'className' | 'size'
|
||||||
> & {
|
> & {
|
||||||
children: React.ReactNode[];
|
iconButtons: {
|
||||||
|
Icon: IconComponent;
|
||||||
|
onClick?: (event: MouseEvent<any>) => void;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FloatingIconButtonGroup({
|
export function FloatingIconButtonGroup({
|
||||||
children,
|
iconButtons,
|
||||||
size,
|
size,
|
||||||
}: FloatingIconButtonGroupProps) {
|
}: FloatingIconButtonGroupProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledFloatingIconButtonGroupContainer>
|
<StyledFloatingIconButtonGroupContainer>
|
||||||
{React.Children.map(children, (child, index) => {
|
{iconButtons.map(({ Icon, onClick }, index) => {
|
||||||
let position: FloatingIconButtonPosition;
|
const position: FloatingIconButtonPosition =
|
||||||
|
index === 0
|
||||||
|
? 'left'
|
||||||
|
: index === iconButtons.length - 1
|
||||||
|
? 'right'
|
||||||
|
: 'middle';
|
||||||
|
|
||||||
if (index === 0) {
|
return (
|
||||||
position = 'left';
|
<FloatingIconButton
|
||||||
} else if (index === children.length - 1) {
|
applyBlur={false}
|
||||||
position = 'right';
|
applyShadow={false}
|
||||||
} else {
|
icon={<Icon size={theme.icon.size.sm} />}
|
||||||
position = 'middle';
|
onClick={onClick}
|
||||||
}
|
position={position}
|
||||||
|
size={size}
|
||||||
const additionalProps: any = {
|
/>
|
||||||
position,
|
);
|
||||||
size,
|
|
||||||
applyShadow: false,
|
|
||||||
applyBlur: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (size) {
|
|
||||||
additionalProps.size = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!React.isValidElement(child)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return React.cloneElement(child, additionalProps);
|
|
||||||
})}
|
})}
|
||||||
</StyledFloatingIconButtonGroupContainer>
|
</StyledFloatingIconButtonGroupContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
import React from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { IconButtonPosition, IconButtonProps } from './IconButton';
|
import type { IconComponent } from '@/ui/icon/types/IconComponent';
|
||||||
|
|
||||||
|
import { Button } from './Button';
|
||||||
|
import { IconButtonPosition, type IconButtonProps } from './IconButton';
|
||||||
|
|
||||||
const StyledIconButtonGroupContainer = styled.div`
|
const StyledIconButtonGroupContainer = styled.div`
|
||||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
@ -10,45 +14,42 @@ const StyledIconButtonGroupContainer = styled.div`
|
|||||||
|
|
||||||
export type IconButtonGroupProps = Pick<
|
export type IconButtonGroupProps = Pick<
|
||||||
IconButtonProps,
|
IconButtonProps,
|
||||||
'variant' | 'size' | 'accent'
|
'accent' | 'size' | 'variant'
|
||||||
> & {
|
> & {
|
||||||
children: React.ReactElement[];
|
iconButtons: {
|
||||||
|
Icon: IconComponent;
|
||||||
|
onClick?: (event: MouseEvent<any>) => void;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function IconButtonGroup({
|
export function IconButtonGroup({
|
||||||
children,
|
|
||||||
variant,
|
|
||||||
size,
|
|
||||||
accent,
|
accent,
|
||||||
|
iconButtons,
|
||||||
|
size,
|
||||||
|
variant,
|
||||||
}: IconButtonGroupProps) {
|
}: IconButtonGroupProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledIconButtonGroupContainer>
|
<StyledIconButtonGroupContainer>
|
||||||
{React.Children.map(children, (child, index) => {
|
{iconButtons.map(({ Icon, onClick }, index) => {
|
||||||
let position: IconButtonPosition;
|
const position: IconButtonPosition =
|
||||||
|
index === 0
|
||||||
|
? 'left'
|
||||||
|
: index === iconButtons.length - 1
|
||||||
|
? 'right'
|
||||||
|
: 'middle';
|
||||||
|
|
||||||
if (index === 0) {
|
return (
|
||||||
position = 'left';
|
<Button
|
||||||
} else if (index === children.length - 1) {
|
accent={accent}
|
||||||
position = 'right';
|
icon={<Icon size={theme.icon.size.sm} />}
|
||||||
} else {
|
onClick={onClick}
|
||||||
position = 'middle';
|
position={position}
|
||||||
}
|
size={size}
|
||||||
|
variant={variant}
|
||||||
const additionalProps: any = { position };
|
/>
|
||||||
|
);
|
||||||
if (variant) {
|
|
||||||
additionalProps.variant = variant;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accent) {
|
|
||||||
additionalProps.accent = accent;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size) {
|
|
||||||
additionalProps.size = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
return React.cloneElement(child, additionalProps);
|
|
||||||
})}
|
})}
|
||||||
</StyledIconButtonGroupContainer>
|
</StyledIconButtonGroupContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,15 +4,22 @@ import { IconCheckbox, IconNotes, IconTimelineEvent } from '@/ui/icon';
|
|||||||
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
|
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
|
||||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||||
|
|
||||||
import {
|
import { FloatingIconButtonSize } from '../FloatingIconButton';
|
||||||
FloatingIconButton,
|
|
||||||
FloatingIconButtonSize,
|
|
||||||
} from '../FloatingIconButton';
|
|
||||||
import { FloatingIconButtonGroup } from '../FloatingIconButtonGroup';
|
import { FloatingIconButtonGroup } from '../FloatingIconButtonGroup';
|
||||||
|
|
||||||
const meta: Meta<typeof FloatingIconButtonGroup> = {
|
const meta: Meta<typeof FloatingIconButtonGroup> = {
|
||||||
title: 'UI/Button/FloatingIconButtonGroup',
|
title: 'UI/Button/FloatingIconButtonGroup',
|
||||||
component: FloatingIconButtonGroup,
|
component: FloatingIconButtonGroup,
|
||||||
|
args: {
|
||||||
|
iconButtons: [
|
||||||
|
{ Icon: IconNotes },
|
||||||
|
{ Icon: IconCheckbox },
|
||||||
|
{ Icon: IconTimelineEvent },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
iconButtons: { control: false },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -21,29 +28,13 @@ type Story = StoryObj<typeof FloatingIconButtonGroup>;
|
|||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: {
|
args: {
|
||||||
size: 'small',
|
size: 'small',
|
||||||
children: [
|
|
||||||
<FloatingIconButton icon={<IconNotes />} />,
|
|
||||||
<FloatingIconButton icon={<IconCheckbox />} />,
|
|
||||||
<FloatingIconButton icon={<IconTimelineEvent />} />,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
argTypes: {
|
|
||||||
children: { control: false },
|
|
||||||
},
|
},
|
||||||
decorators: [ComponentDecorator],
|
decorators: [ComponentDecorator],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Catalog: Story = {
|
export const Catalog: Story = {
|
||||||
args: {
|
|
||||||
children: [
|
|
||||||
<FloatingIconButton icon={<IconNotes />} />,
|
|
||||||
<FloatingIconButton icon={<IconCheckbox />} />,
|
|
||||||
<FloatingIconButton icon={<IconTimelineEvent />} />,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
argTypes: {
|
argTypes: {
|
||||||
size: { control: false },
|
size: { control: false },
|
||||||
children: { control: false },
|
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
|
pseudo: { hover: ['.hover'], active: ['.pressed'], focus: ['.focus'] },
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { IconCheckbox, IconNotes, IconTimelineEvent } from '@/ui/icon';
|
|||||||
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
|
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
|
||||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||||
|
|
||||||
import { Button } from '../Button';
|
|
||||||
import {
|
import {
|
||||||
IconButtonAccent,
|
IconButtonAccent,
|
||||||
IconButtonSize,
|
IconButtonSize,
|
||||||
@ -15,6 +14,16 @@ import { IconButtonGroup } from '../IconButtonGroup';
|
|||||||
const meta: Meta<typeof IconButtonGroup> = {
|
const meta: Meta<typeof IconButtonGroup> = {
|
||||||
title: 'UI/Button/IconButtonGroup',
|
title: 'UI/Button/IconButtonGroup',
|
||||||
component: IconButtonGroup,
|
component: IconButtonGroup,
|
||||||
|
args: {
|
||||||
|
iconButtons: [
|
||||||
|
{ Icon: IconNotes },
|
||||||
|
{ Icon: IconCheckbox },
|
||||||
|
{ Icon: IconTimelineEvent },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
iconButtons: { control: false },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -25,31 +34,15 @@ export const Default: Story = {
|
|||||||
size: 'small',
|
size: 'small',
|
||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
accent: 'danger',
|
accent: 'danger',
|
||||||
children: [
|
|
||||||
<Button icon={<IconNotes />} />,
|
|
||||||
<Button icon={<IconCheckbox />} />,
|
|
||||||
<Button icon={<IconTimelineEvent />} />,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
argTypes: {
|
|
||||||
children: { control: false },
|
|
||||||
},
|
},
|
||||||
decorators: [ComponentDecorator],
|
decorators: [ComponentDecorator],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Catalog: Story = {
|
export const Catalog: Story = {
|
||||||
args: {
|
|
||||||
children: [
|
|
||||||
<Button icon={<IconNotes />} />,
|
|
||||||
<Button icon={<IconCheckbox />} />,
|
|
||||||
<Button icon={<IconTimelineEvent />} />,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
argTypes: {
|
argTypes: {
|
||||||
size: { control: false },
|
size: { control: false },
|
||||||
variant: { control: false },
|
variant: { control: false },
|
||||||
accent: { control: false },
|
accent: { control: false },
|
||||||
children: { control: false },
|
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
catalog: {
|
catalog: {
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
|
|
||||||
import { FloatingIconButton } from '@/ui/button/components/FloatingIconButton';
|
|
||||||
import { FloatingIconButtonGroup } from '@/ui/button/components/FloatingIconButtonGroup';
|
import { FloatingIconButtonGroup } from '@/ui/button/components/FloatingIconButtonGroup';
|
||||||
import { IconComponent } from '@/ui/icon/types/IconComponent';
|
import { IconComponent } from '@/ui/icon/types/IconComponent';
|
||||||
|
|
||||||
@ -33,8 +31,6 @@ export function MenuItem({
|
|||||||
testId,
|
testId,
|
||||||
onClick,
|
onClick,
|
||||||
}: MenuItemProps) {
|
}: MenuItemProps) {
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0;
|
const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -45,17 +41,7 @@ export function MenuItem({
|
|||||||
accent={accent}
|
accent={accent}
|
||||||
>
|
>
|
||||||
<MenuItemLeftContent LeftIcon={LeftIcon ?? undefined} text={text} />
|
<MenuItemLeftContent LeftIcon={LeftIcon ?? undefined} text={text} />
|
||||||
{showIconButtons && (
|
{showIconButtons && <FloatingIconButtonGroup iconButtons={iconButtons} />}
|
||||||
<FloatingIconButtonGroup>
|
|
||||||
{iconButtons?.map(({ Icon, onClick }, index) => (
|
|
||||||
<FloatingIconButton
|
|
||||||
icon={<Icon size={theme.icon.size.sm} />}
|
|
||||||
key={index}
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</FloatingIconButtonGroup>
|
|
||||||
)}
|
|
||||||
</StyledMenuItemBase>
|
</StyledMenuItemBase>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user