Refactor/new menu item (#1448)

* wip

* finished

* Added disabled

* Fixed disabled

* Finished cleaning

* Minor fixes from merge

* Added docs

* Added PascalCase

* Fix from review

* Fixes from merge

* Fix lint

* Fixed storybook tests
This commit is contained in:
Lucas Bordeau
2023-09-06 16:41:26 +02:00
committed by GitHub
parent 5c7660f588
commit 28ca9a9e49
96 changed files with 816 additions and 918 deletions

View File

@ -1,52 +0,0 @@
import React from 'react';
import styled from '@emotion/styled';
import { Checkbox } from '@/ui/input/checkbox/components/Checkbox';
import { DropdownMenuItem } from './DropdownMenuItem';
type Props = {
checked: boolean;
onChange?: (newCheckedValue: boolean) => void;
id?: string;
};
const StyledDropdownMenuCheckableItemContainer = styled(DropdownMenuItem)`
align-items: center;
display: flex;
justify-content: space-between;
`;
const StyledLeftContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledChildrenContainer = styled.div`
align-items: center;
display: flex;
font-size: ${({ theme }) => theme.font.size.sm};
gap: ${({ theme }) => theme.spacing(2)};
`;
export function DropdownMenuCheckableItem({
checked,
onChange,
children,
}: React.PropsWithChildren<Props>) {
function handleClick() {
onChange?.(!checked);
}
return (
<StyledDropdownMenuCheckableItemContainer onClick={handleClick}>
<StyledLeftContainer>
<Checkbox checked={checked} />
<StyledChildrenContainer>{children}</StyledChildrenContainer>
</StyledLeftContainer>
</StyledDropdownMenuCheckableItemContainer>
);
}

View File

@ -1,6 +1,9 @@
import { ComponentProps, ReactElement } from 'react';
import { ComponentProps } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent } from '@/ui/icon/types/IconComponent';
const StyledHeader = styled.li`
align-items: center;
color: ${({ theme }) => theme.font.color.primary};
@ -40,23 +43,31 @@ const StyledEndIconWrapper = styled(StyledStartIconWrapper)`
`;
type DropdownMenuHeaderProps = ComponentProps<'li'> & {
startIcon?: ReactElement;
endIcon?: ReactElement;
StartIcon?: IconComponent;
EndIcon?: IconComponent;
};
export function DropdownMenuHeader({
children,
startIcon,
endIcon,
StartIcon,
EndIcon,
...props
}: DropdownMenuHeaderProps) {
const theme = useTheme();
return (
<StyledHeader {...props}>
{startIcon && (
<StyledStartIconWrapper>{startIcon}</StyledStartIconWrapper>
{StartIcon && (
<StyledStartIconWrapper>
<StartIcon size={theme.icon.size.md} />
</StyledStartIconWrapper>
)}
{children}
{endIcon && <StyledEndIconWrapper>{endIcon}</StyledEndIconWrapper>}
{EndIcon && (
<StyledEndIconWrapper>
<EndIcon size={theme.icon.size.md} />
</StyledEndIconWrapper>
)}
</StyledHeader>
);
}

View File

@ -1,79 +0,0 @@
import { ComponentProps } from 'react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { FloatingIconButtonGroup } from '@/ui/button/components/FloatingIconButtonGroup';
import { hoverBackground } from '@/ui/theme/constants/effects';
export type DropdownMenuItemAccent = 'regular' | 'danger';
const StyledItem = styled.li<{ accent: DropdownMenuItemAccent }>`
--horizontal-padding: ${({ theme }) => theme.spacing(1)};
--vertical-padding: ${({ theme }) => theme.spacing(2)};
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme, accent }) =>
accent === 'danger' ? theme.color.red : theme.font.color.secondary};
cursor: pointer;
display: flex;
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.sm};
gap: ${({ theme }) => theme.spacing(2)};
height: calc(32px - 2 * var(--vertical-padding));
padding: var(--vertical-padding) var(--horizontal-padding);
${hoverBackground};
position: relative;
user-select: none;
width: calc(100% - 2 * var(--horizontal-padding));
&:hover .actions-hover-container {
display: flex;
}
`;
const StyledActions = styled(motion.div)`
display: none;
position: absolute;
right: ${({ theme }) => theme.spacing(1)};
`;
export type DropdownMenuItemProps = ComponentProps<'li'> & {
actions?: React.ReactNode[];
accent?: DropdownMenuItemAccent;
};
export function DropdownMenuItem({
actions,
children,
accent = 'regular',
...props
}: DropdownMenuItemProps) {
return (
<StyledItem {...props} accent={accent}>
{children}
{actions && (
<StyledActions
className="actions-hover-container"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<FloatingIconButtonGroup size="small">
{actions}
</FloatingIconButtonGroup>
</StyledActions>
)}
</StyledItem>
);
}

View File

@ -1,87 +0,0 @@
import React, { useEffect } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCheck } from '@/ui/icon/index';
import { hoverBackground } from '@/ui/theme/constants/effects';
import { DropdownMenuItem } from './DropdownMenuItem';
type Props = React.ComponentProps<'li'> & {
selected?: boolean;
hovered?: boolean;
disabled?: boolean;
};
const StyledDropdownMenuSelectableItemContainer = styled(DropdownMenuItem)<
Pick<Props, 'hovered'>
>`
${hoverBackground};
align-items: center;
background: ${(props) =>
props.hovered ? props.theme.background.transparent.light : 'transparent'};
display: flex;
justify-content: space-between;
width: calc(100% - ${({ theme }) => theme.spacing(2)});
`;
const StyledLeftContainer = styled.div<Pick<Props, 'disabled'>>`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
opacity: ${({ disabled }) => (disabled ? 0.5 : 1)};
overflow: hidden;
`;
const StyledRightIcon = styled.div`
display: flex;
`;
export function DropdownMenuSelectableItem({
selected,
onClick,
children,
hovered,
disabled,
...restProps
}: React.PropsWithChildren<Props>) {
const theme = useTheme();
function handleClick(event: React.MouseEvent<HTMLLIElement>) {
if (disabled) {
return;
}
onClick?.(event);
}
useEffect(() => {
if (hovered) {
window.scrollTo({
behavior: 'smooth',
});
}
}, [hovered]);
return (
<StyledDropdownMenuSelectableItemContainer
{...restProps}
onClick={handleClick}
hovered={hovered}
data-testid="dropdown-menu-item"
>
<StyledLeftContainer disabled={disabled}>{children}</StyledLeftContainer>
<StyledRightIcon>
{selected && <IconCheck size={theme.icon.size.md} />}
</StyledRightIcon>
</StyledDropdownMenuSelectableItemContainer>
);
}

View File

@ -15,5 +15,6 @@ export const StyledDropdownMenuItemsContainer = styled.div<{
overflow-y: auto;
padding: var(--padding);
padding-right: var(--padding);
width: calc(100% - 2 * var(--padding));
`;

View File

@ -2,17 +2,16 @@ import { useState } from 'react';
import styled from '@emotion/styled';
import type { Meta, StoryObj } from '@storybook/react';
import { IconButton } from '@/ui/button/components/IconButton';
import { IconPlus, IconUser } from '@/ui/icon';
import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { MenuItemMultiSelectAvatar } from '@/ui/menu-item/components/MenuItemMultiSelectAvatar';
import { MenuItemSelectAvatar } from '@/ui/menu-item/components/MenuItemSelectAvatar';
import { Avatar } from '@/users/components/Avatar';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { DropdownMenuCheckableItem } from '../DropdownMenuCheckableItem';
import { DropdownMenuHeader } from '../DropdownMenuHeader';
import { DropdownMenuInput } from '../DropdownMenuInput';
import { DropdownMenuItem } from '../DropdownMenuItem';
import { DropdownMenuSelectableItem } from '../DropdownMenuSelectableItem';
import { StyledDropdownMenu } from '../StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '../StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '../StyledDropdownMenuSeparator';
@ -101,21 +100,22 @@ const FakeSelectableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => {
return (
<>
{mockSelectArray.map((item) => (
<DropdownMenuSelectableItem
<MenuItemSelectAvatar
key={item.id}
selected={selectedItem === item.id}
onClick={() => setSelectedItem(item.id)}
>
{hasAvatar && (
<Avatar
placeholder="A"
avatarUrl={item.avatarUrl}
size="md"
type="squared"
/>
)}
{item.name}
</DropdownMenuSelectableItem>
avatar={
hasAvatar ? (
<Avatar
placeholder="A"
avatarUrl={item.avatarUrl}
size="md"
type="squared"
/>
) : undefined
}
text={item.name}
/>
))}
</>
);
@ -127,28 +127,28 @@ const FakeCheckableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => {
return (
<>
{mockSelectArray.map((item) => (
<DropdownMenuCheckableItem
<MenuItemMultiSelectAvatar
key={item.id}
id={item.id}
checked={selectedItems.includes(item.id)}
onChange={(checked) => {
selected={selectedItems.includes(item.id)}
onSelectChange={(checked) => {
if (checked) {
setSelectedItems([...selectedItems, item.id]);
} else {
setSelectedItems(selectedItems.filter((id) => id !== item.id));
}
}}
>
{hasAvatar && (
<Avatar
placeholder="A"
avatarUrl={item.avatarUrl}
size="md"
type="squared"
/>
)}
{item.name}
</DropdownMenuCheckableItem>
avatar={
hasAvatar ? (
<Avatar
placeholder="A"
avatarUrl={item.avatarUrl}
size="md"
type="squared"
/>
) : undefined
}
text={item.name}
/>
))}
</>
);
@ -182,7 +182,7 @@ export const SimpleMenuItem: Story = {
<StyledDropdownMenu {...args}>
<StyledDropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem>
<MenuItem text={name} />
))}
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
@ -198,14 +198,14 @@ export const WithHeaders: Story = {
<StyledDropdownMenuSubheader>Subheader 1</StyledDropdownMenuSubheader>
<StyledDropdownMenuItemsContainer>
{mockSelectArray.slice(0, 3).map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem>
<MenuItem text={name} />
))}
</StyledDropdownMenuItemsContainer>
<StyledDropdownMenuSeparator />
<StyledDropdownMenuSubheader>Subheader 2</StyledDropdownMenuSubheader>
<StyledDropdownMenuItemsContainer>
{mockSelectArray.slice(3).map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem>
<MenuItem text={name} />
))}
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
@ -218,10 +218,7 @@ export const WithIcons: Story = {
<StyledDropdownMenu {...args}>
<StyledDropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }) => (
<DropdownMenuItem>
<IconUser size={16} />
{name}
</DropdownMenuItem>
<MenuItem text={name} LeftIcon={IconUser} />
))}
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
@ -234,15 +231,11 @@ export const WithActions: Story = {
<StyledDropdownMenu {...args}>
<StyledDropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }, index) => (
<DropdownMenuItem
<MenuItem
className={index === 0 ? 'hover' : undefined}
actions={[
<IconButton icon={<IconUser />} />,
<IconButton icon={<IconPlus />} />,
]}
>
{name}
</DropdownMenuItem>
iconButtons={[{ Icon: IconUser }, { Icon: IconPlus }]}
text={name}
/>
))}
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
@ -273,7 +266,7 @@ export const Search: Story = {
<StyledDropdownMenuSeparator />
<StyledDropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem>
<MenuItem text={name} />
))}
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>