@ -0,0 +1,58 @@
|
||||
import { ComponentProps, ReactElement } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledHeader = styled.li`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
|
||||
padding: calc(${({ theme }) => theme.spacing(2)})
|
||||
calc(${({ theme }) => theme.spacing(2)});
|
||||
|
||||
user-select: none;
|
||||
|
||||
${({ onClick, theme }) => {
|
||||
if (onClick) {
|
||||
return `
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: ${theme.background.transparent.light};
|
||||
}
|
||||
`;
|
||||
}
|
||||
}}
|
||||
`;
|
||||
|
||||
const StyledStartIconWrapper = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: inline-flex;
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledEndIconWrapper = styled(StyledStartIconWrapper)`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: inline-flex;
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
`;
|
||||
|
||||
type DropdownMenuHeaderProps = ComponentProps<'li'> & {
|
||||
startIcon?: ReactElement;
|
||||
endIcon?: ReactElement;
|
||||
};
|
||||
|
||||
export const DropdownMenuHeader = ({
|
||||
children,
|
||||
startIcon,
|
||||
endIcon,
|
||||
...props
|
||||
}: DropdownMenuHeaderProps) => (
|
||||
<StyledHeader {...props}>
|
||||
{startIcon && <StyledStartIconWrapper>{startIcon}</StyledStartIconWrapper>}
|
||||
{children}
|
||||
{endIcon && <StyledEndIconWrapper>{endIcon}</StyledEndIconWrapper>}
|
||||
</StyledHeader>
|
||||
);
|
||||
@ -1,8 +1,15 @@
|
||||
import { ComponentProps } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import {
|
||||
IconButtonGroup,
|
||||
type IconButtonGroupProps,
|
||||
} from '@/ui/button/components/IconButtonGroup';
|
||||
import { hoverBackground } from '@/ui/theme/constants/effects';
|
||||
|
||||
export const DropdownMenuItem = styled.li`
|
||||
const styledIconButtonGroupClassName = 'dropdown-menu-item-actions';
|
||||
|
||||
const StyledItem = styled.li`
|
||||
--horizontal-padding: ${({ theme }) => theme.spacing(1)};
|
||||
--vertical-padding: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
@ -25,6 +32,43 @@ export const DropdownMenuItem = styled.li`
|
||||
|
||||
${hoverBackground};
|
||||
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
||||
width: calc(100% - 2 * var(--horizontal-padding));
|
||||
|
||||
&:hover {
|
||||
.${styledIconButtonGroupClassName} {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledActions = styled(IconButtonGroup)`
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export type DropdownMenuItemProps = ComponentProps<'li'> & {
|
||||
actions?: IconButtonGroupProps['children'];
|
||||
};
|
||||
|
||||
export const DropdownMenuItem = ({
|
||||
actions,
|
||||
children,
|
||||
...props
|
||||
}: DropdownMenuItemProps) => (
|
||||
<StyledItem {...props}>
|
||||
{children}
|
||||
{actions && (
|
||||
<StyledActions
|
||||
className={styledIconButtonGroupClassName}
|
||||
variant="transparent"
|
||||
size="small"
|
||||
>
|
||||
{actions}
|
||||
</StyledActions>
|
||||
)}
|
||||
</StyledItem>
|
||||
);
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const DropdownMenuSubheader = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-size: ${({ theme }) => theme.font.size.xxs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
padding: ${({ theme }) => `${theme.spacing(1)} ${theme.spacing(2)}`};
|
||||
text-transform: uppercase;
|
||||
width: 100%;
|
||||
`;
|
||||
@ -2,18 +2,21 @@ import { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { IconPlus } from '@/ui/icon/index';
|
||||
import { IconButton } from '@/ui/button/components/IconButton';
|
||||
import { IconPlus, IconUser } from '@/ui/icon';
|
||||
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
|
||||
import { Avatar } from '@/users/components/Avatar';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { DropdownMenu } from '../DropdownMenu';
|
||||
import { DropdownMenuCheckableItem } from '../DropdownMenuCheckableItem';
|
||||
import { DropdownMenuHeader } from '../DropdownMenuHeader';
|
||||
import { DropdownMenuItem } from '../DropdownMenuItem';
|
||||
import { DropdownMenuItemsContainer } from '../DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearch } from '../DropdownMenuSearch';
|
||||
import { DropdownMenuSelectableItem } from '../DropdownMenuSelectableItem';
|
||||
import { DropdownMenuSeparator } from '../DropdownMenuSeparator';
|
||||
import { DropdownMenuSubheader } from '../DropdownMenuSubheader';
|
||||
|
||||
const meta: Meta<typeof DropdownMenu> = {
|
||||
title: 'UI/Dropdown/DropdownMenu',
|
||||
@ -59,47 +62,36 @@ const MenuAbsolutePositionWrapper = styled.div`
|
||||
width: fit-content;
|
||||
`;
|
||||
|
||||
const FakeMenuItemList = () => (
|
||||
<>
|
||||
<DropdownMenuItem>Company A</DropdownMenuItem>
|
||||
<DropdownMenuItem>Company B</DropdownMenuItem>
|
||||
<DropdownMenuItem>Company C</DropdownMenuItem>
|
||||
<DropdownMenuItem>Person 2</DropdownMenuItem>
|
||||
<DropdownMenuItem>Company D</DropdownMenuItem>
|
||||
<DropdownMenuItem>Person 1</DropdownMenuItem>
|
||||
</>
|
||||
);
|
||||
|
||||
const mockSelectArray = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Company A',
|
||||
avatarUrl: avatarUrl,
|
||||
avatarUrl,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Company B',
|
||||
avatarUrl: avatarUrl,
|
||||
avatarUrl,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Company C',
|
||||
avatarUrl: avatarUrl,
|
||||
avatarUrl,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Person 2',
|
||||
avatarUrl: avatarUrl,
|
||||
avatarUrl,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Company D',
|
||||
avatarUrl: avatarUrl,
|
||||
avatarUrl,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'Person 1',
|
||||
avatarUrl: avatarUrl,
|
||||
avatarUrl,
|
||||
},
|
||||
];
|
||||
|
||||
@ -189,12 +181,77 @@ export const SimpleMenuItem: Story = {
|
||||
render: (args) => (
|
||||
<DropdownMenu {...args}>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<FakeMenuItemList />
|
||||
{mockSelectArray.map(({ name }) => (
|
||||
<DropdownMenuItem>{name}</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithHeaders: Story = {
|
||||
...WithContentBelow,
|
||||
render: (args) => (
|
||||
<DropdownMenu {...args}>
|
||||
<DropdownMenuHeader>Header</DropdownMenuHeader>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSubheader>Subheader 1</DropdownMenuSubheader>
|
||||
<DropdownMenuItemsContainer>
|
||||
{mockSelectArray.slice(0, 3).map(({ name }) => (
|
||||
<DropdownMenuItem>{name}</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSubheader>Subheader 2</DropdownMenuSubheader>
|
||||
<DropdownMenuItemsContainer>
|
||||
{mockSelectArray.slice(3).map(({ name }) => (
|
||||
<DropdownMenuItem>{name}</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithIcons: Story = {
|
||||
...WithContentBelow,
|
||||
render: (args) => (
|
||||
<DropdownMenu {...args}>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{mockSelectArray.map(({ name }) => (
|
||||
<DropdownMenuItem>
|
||||
<IconUser size={16} />
|
||||
{name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithActions: Story = {
|
||||
...WithContentBelow,
|
||||
render: (args) => (
|
||||
<DropdownMenu {...args}>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{mockSelectArray.map(({ name }, index) => (
|
||||
<DropdownMenuItem
|
||||
className={index === 0 ? 'hover' : undefined}
|
||||
actions={[
|
||||
<IconButton icon={<IconUser />} />,
|
||||
<IconButton icon={<IconPlus />} />,
|
||||
]}
|
||||
>
|
||||
{name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
),
|
||||
parameters: {
|
||||
pseudo: { hover: ['.hover'] },
|
||||
},
|
||||
};
|
||||
|
||||
export const LoadingMenu: Story = {
|
||||
...WithContentBelow,
|
||||
render: () => (
|
||||
@ -215,25 +272,9 @@ export const Search: Story = {
|
||||
<DropdownMenuSearch />
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<FakeMenuItemList />
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
),
|
||||
};
|
||||
|
||||
export const Button: Story = {
|
||||
...WithContentBelow,
|
||||
render: (args) => (
|
||||
<DropdownMenu {...args}>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<DropdownMenuItem>
|
||||
<IconPlus size={16} />
|
||||
<div>Create new</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<FakeSelectableMenuItemList />
|
||||
{mockSelectArray.map(({ name }) => (
|
||||
<DropdownMenuItem>{name}</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user