Uniformize folder structure (#693)
* Uniformize folder structure * Fix icons * Fix icons * Fix tests * Fix tests
This commit is contained in:
19
front/src/modules/ui/dropdown/components/DropdownMenu.tsx
Normal file
19
front/src/modules/ui/dropdown/components/DropdownMenu.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const DropdownMenu = styled.div<{ disableBlur?: boolean }>`
|
||||
align-items: center;
|
||||
backdrop-filter: ${({ disableBlur }) =>
|
||||
disableBlur ? 'none' : 'blur(20px)'};
|
||||
|
||||
background: ${({ theme }) => theme.background.transparent.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
width: 200px;
|
||||
`;
|
||||
@ -0,0 +1,30 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { hoverBackground } from '@/ui/themes/effects';
|
||||
|
||||
export const DropdownMenuButton = styled.div`
|
||||
--horizontal-padding: ${({ theme }) => theme.spacing(1.5)};
|
||||
--vertical-padding: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
align-items: center;
|
||||
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
color: ${({ theme }) => 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};
|
||||
|
||||
user-select: none;
|
||||
width: calc(100% - 2 * var(--horizontal-padding));
|
||||
`;
|
||||
@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Checkbox } from '@/ui/input/components/Checkbox';
|
||||
|
||||
import { DropdownMenuButton } from './DropdownMenuButton';
|
||||
|
||||
type Props = {
|
||||
checked: boolean;
|
||||
onChange?: (newCheckedValue: boolean) => void;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
const DropdownMenuCheckableItemContainer = styled(DropdownMenuButton)`
|
||||
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 (
|
||||
<DropdownMenuCheckableItemContainer>
|
||||
<StyledLeftContainer>
|
||||
<Checkbox checked={checked} onChange={handleClick} />
|
||||
<StyledChildrenContainer>{children}</StyledChildrenContainer>
|
||||
</StyledLeftContainer>
|
||||
</DropdownMenuCheckableItemContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const DropdownMenuItem = styled.div`
|
||||
--horizontal-padding: ${({ theme }) => theme.spacing(1.5)};
|
||||
--vertical-padding: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
align-items: center;
|
||||
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
|
||||
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);
|
||||
width: calc(100% - 2 * var(--horizontal-padding));
|
||||
`;
|
||||
@ -0,0 +1,17 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const DropdownMenuItemContainer = styled.div`
|
||||
--padding: ${({ theme }) => theme.spacing(1 / 2)};
|
||||
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
|
||||
padding: var(--padding);
|
||||
width: calc(100% - 2 * var(--padding));
|
||||
`;
|
||||
@ -0,0 +1,39 @@
|
||||
import { InputHTMLAttributes } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { textInputStyle } from '@/ui/themes/effects';
|
||||
|
||||
export const DropdownMenuSearchContainer = styled.div`
|
||||
--vertical-padding: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
align-items: center;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: calc(36px - 2 * var(--vertical-padding));
|
||||
padding: var(--vertical-padding) 0;
|
||||
|
||||
width: calc(100%);
|
||||
`;
|
||||
|
||||
const StyledEditModeSearchInput = styled.input`
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
|
||||
${textInputStyle}
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export function DropdownMenuSearch(
|
||||
props: InputHTMLAttributes<HTMLInputElement>,
|
||||
) {
|
||||
return (
|
||||
<DropdownMenuSearchContainer>
|
||||
<StyledEditModeSearchInput
|
||||
{...props}
|
||||
placeholder={props.placeholder ?? 'Search'}
|
||||
/>
|
||||
</DropdownMenuSearchContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
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/themes/effects';
|
||||
|
||||
import { DropdownMenuButton } from './DropdownMenuButton';
|
||||
|
||||
type Props = {
|
||||
selected?: boolean;
|
||||
onClick: () => void;
|
||||
hovered?: boolean;
|
||||
};
|
||||
|
||||
const DropdownMenuSelectableItemContainer = styled(DropdownMenuButton)<Props>`
|
||||
${hoverBackground};
|
||||
|
||||
align-items: center;
|
||||
|
||||
background: ${(props) =>
|
||||
props.hovered ? props.theme.background.transparent.light : 'transparent'};
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const StyledLeftContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledRightIcon = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export function DropdownMenuSelectableItem({
|
||||
selected,
|
||||
onClick,
|
||||
children,
|
||||
hovered,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (hovered) {
|
||||
window.scrollTo({
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}, [hovered]);
|
||||
|
||||
return (
|
||||
<DropdownMenuSelectableItemContainer
|
||||
onClick={onClick}
|
||||
selected={selected}
|
||||
hovered={hovered}
|
||||
data-testid="dropdown-menu-item"
|
||||
>
|
||||
<StyledLeftContainer>{children}</StyledLeftContainer>
|
||||
<StyledRightIcon>
|
||||
{selected && <IconCheck size={theme.icon.size.md} />}
|
||||
</StyledRightIcon>
|
||||
</DropdownMenuSelectableItemContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const DropdownMenuSeparator = styled.div`
|
||||
background-color: ${({ theme }) => theme.border.color.light};
|
||||
height: 1px;
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
@ -0,0 +1,333 @@
|
||||
import React, { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { IconPlus } from '@/ui/icon/index';
|
||||
import { Avatar } from '@/users/components/Avatar';
|
||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
|
||||
import { DropdownMenu } from '../DropdownMenu';
|
||||
import { DropdownMenuButton } from '../DropdownMenuButton';
|
||||
import { DropdownMenuCheckableItem } from '../DropdownMenuCheckableItem';
|
||||
import { DropdownMenuItem } from '../DropdownMenuItem';
|
||||
import { DropdownMenuItemContainer } from '../DropdownMenuItemContainer';
|
||||
import { DropdownMenuSearch } from '../DropdownMenuSearch';
|
||||
import { DropdownMenuSelectableItem } from '../DropdownMenuSelectableItem';
|
||||
import { DropdownMenuSeparator } from '../DropdownMenuSeparator';
|
||||
|
||||
const meta: Meta<typeof DropdownMenu> = {
|
||||
title: 'UI/Menu/DropdownMenu',
|
||||
component: DropdownMenu,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof DropdownMenu>;
|
||||
|
||||
const FakeContentBelow = () => (
|
||||
<div style={{ position: 'absolute' }}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
|
||||
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
|
||||
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||
consequat.
|
||||
</div>
|
||||
);
|
||||
|
||||
const avatarUrl =
|
||||
'https://s3-alpha-sig.figma.com/img/bbb5/4905/f0a52cc2b9aaeb0a82a360d478dae8bf?Expires=1687132800&Signature=iVBr0BADa3LHoFVGbwqO-wxC51n1o~ZyFD-w7nyTyFP4yB-Y6zFawL-igewaFf6PrlumCyMJThDLAAc-s-Cu35SBL8BjzLQ6HymzCXbrblUADMB208PnMAvc1EEUDq8TyryFjRO~GggLBk5yR0EXzZ3zenqnDEGEoQZR~TRqS~uDF-GwQB3eX~VdnuiU2iittWJkajIDmZtpN3yWtl4H630A3opQvBnVHZjXAL5YPkdh87-a-H~6FusWvvfJxfNC2ZzbrARzXofo8dUFtH7zUXGCC~eUk~hIuLbLuz024lFQOjiWq2VKyB7dQQuGFpM-OZQEV8tSfkViP8uzDLTaCg__&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4';
|
||||
|
||||
const FakeMenuContent = styled.div`
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const FakeBelowContainer = styled.div`
|
||||
height: 600px;
|
||||
position: relative;
|
||||
|
||||
width: 300px;
|
||||
`;
|
||||
|
||||
const MenuAbsolutePositionWrapper = styled.div`
|
||||
height: fit-content;
|
||||
position: absolute;
|
||||
|
||||
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,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Company B',
|
||||
avatarUrl: avatarUrl,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Company C',
|
||||
avatarUrl: avatarUrl,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Person 2',
|
||||
avatarUrl: avatarUrl,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Company D',
|
||||
avatarUrl: avatarUrl,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'Person 1',
|
||||
avatarUrl: avatarUrl,
|
||||
},
|
||||
];
|
||||
|
||||
export const Empty: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<DropdownMenu>
|
||||
<FakeMenuContent />
|
||||
</DropdownMenu>,
|
||||
),
|
||||
};
|
||||
|
||||
const DropdownMenuStoryWrapper = ({
|
||||
children,
|
||||
}: React.PropsWithChildren<unknown>) => (
|
||||
<FakeBelowContainer>
|
||||
<FakeContentBelow />
|
||||
<MenuAbsolutePositionWrapper>
|
||||
<DropdownMenu>{children}</DropdownMenu>
|
||||
</MenuAbsolutePositionWrapper>
|
||||
</FakeBelowContainer>
|
||||
);
|
||||
|
||||
export const EmptyWithContentBelow: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<DropdownMenuStoryWrapper>
|
||||
<FakeMenuContent />
|
||||
</DropdownMenuStoryWrapper>,
|
||||
),
|
||||
};
|
||||
|
||||
export const SimpleMenuItem: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<DropdownMenuStoryWrapper>
|
||||
<DropdownMenuItemContainer>
|
||||
<FakeMenuItemList />
|
||||
</DropdownMenuItemContainer>
|
||||
</DropdownMenuStoryWrapper>,
|
||||
),
|
||||
};
|
||||
|
||||
export const Search: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<FakeBelowContainer>
|
||||
<FakeContentBelow />
|
||||
<MenuAbsolutePositionWrapper>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuSearch />
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemContainer>
|
||||
<FakeMenuItemList />
|
||||
</DropdownMenuItemContainer>
|
||||
</DropdownMenu>
|
||||
</MenuAbsolutePositionWrapper>
|
||||
</FakeBelowContainer>,
|
||||
),
|
||||
};
|
||||
|
||||
const FakeSelectableMenuItemList = () => {
|
||||
const [selectedItem, setSelectedItem] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
{mockSelectArray.map((item) => (
|
||||
<DropdownMenuSelectableItem
|
||||
key={item.id}
|
||||
selected={selectedItem === item.id}
|
||||
onClick={() => setSelectedItem(item.id)}
|
||||
>
|
||||
{item.name}
|
||||
</DropdownMenuSelectableItem>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Button: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<FakeBelowContainer>
|
||||
<FakeContentBelow />
|
||||
<MenuAbsolutePositionWrapper>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuItemContainer>
|
||||
<DropdownMenuButton>
|
||||
<IconPlus size={16} />
|
||||
<div>Create new</div>
|
||||
</DropdownMenuButton>
|
||||
</DropdownMenuItemContainer>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemContainer>
|
||||
<FakeSelectableMenuItemList />
|
||||
</DropdownMenuItemContainer>
|
||||
</DropdownMenu>
|
||||
</MenuAbsolutePositionWrapper>
|
||||
</FakeBelowContainer>,
|
||||
),
|
||||
};
|
||||
|
||||
export const SelectableMenuItem: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<FakeBelowContainer>
|
||||
<FakeContentBelow />
|
||||
<MenuAbsolutePositionWrapper>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuItemContainer>
|
||||
<FakeSelectableMenuItemList />
|
||||
</DropdownMenuItemContainer>
|
||||
</DropdownMenu>
|
||||
</MenuAbsolutePositionWrapper>
|
||||
</FakeBelowContainer>,
|
||||
),
|
||||
};
|
||||
|
||||
const FakeSelectableMenuItemWithAvatarList = () => {
|
||||
const [selectedItem, setSelectedItem] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
{mockSelectArray.map((item) => (
|
||||
<DropdownMenuSelectableItem
|
||||
key={item.id}
|
||||
selected={selectedItem === item.id}
|
||||
onClick={() => setSelectedItem(item.id)}
|
||||
>
|
||||
<Avatar
|
||||
placeholder="A"
|
||||
avatarUrl={item.avatarUrl}
|
||||
size={16}
|
||||
type="squared"
|
||||
/>
|
||||
{item.name}
|
||||
</DropdownMenuSelectableItem>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const SelectableMenuItemWithAvatar: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<FakeBelowContainer>
|
||||
<FakeContentBelow />
|
||||
<MenuAbsolutePositionWrapper>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuItemContainer>
|
||||
<FakeSelectableMenuItemWithAvatarList />
|
||||
</DropdownMenuItemContainer>
|
||||
</DropdownMenu>
|
||||
</MenuAbsolutePositionWrapper>
|
||||
</FakeBelowContainer>,
|
||||
),
|
||||
};
|
||||
|
||||
const FakeCheckableMenuItemList = () => {
|
||||
const [selectedItems, setSelectedItems] = useState<string[]>([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{mockSelectArray.map((item) => (
|
||||
<DropdownMenuCheckableItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
checked={selectedItems.includes(item.id)}
|
||||
onChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedItems([...selectedItems, item.id]);
|
||||
} else {
|
||||
setSelectedItems(selectedItems.filter((id) => id !== item.id));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</DropdownMenuCheckableItem>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const CheckableMenuItem: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<FakeBelowContainer>
|
||||
<FakeContentBelow />
|
||||
<MenuAbsolutePositionWrapper>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuItemContainer>
|
||||
<FakeCheckableMenuItemList />
|
||||
</DropdownMenuItemContainer>
|
||||
</DropdownMenu>
|
||||
</MenuAbsolutePositionWrapper>
|
||||
</FakeBelowContainer>,
|
||||
),
|
||||
};
|
||||
|
||||
const FakeCheckableMenuItemWithAvatarList = () => {
|
||||
const [selectedItems, setSelectedItems] = useState<string[]>([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{mockSelectArray.map((item) => (
|
||||
<DropdownMenuCheckableItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
checked={selectedItems.includes(item.id)}
|
||||
onChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedItems([...selectedItems, item.id]);
|
||||
} else {
|
||||
setSelectedItems(selectedItems.filter((id) => id !== item.id));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
placeholder="A"
|
||||
avatarUrl={item.avatarUrl}
|
||||
size={16}
|
||||
type="squared"
|
||||
/>
|
||||
{item.name}
|
||||
</DropdownMenuCheckableItem>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const CheckableMenuItemWithAvatar: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<FakeBelowContainer>
|
||||
<FakeContentBelow />
|
||||
<MenuAbsolutePositionWrapper>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuItemContainer>
|
||||
<FakeCheckableMenuItemWithAvatarList />
|
||||
</DropdownMenuItemContainer>
|
||||
</DropdownMenu>
|
||||
</MenuAbsolutePositionWrapper>
|
||||
</FakeBelowContainer>,
|
||||
),
|
||||
};
|
||||
Reference in New Issue
Block a user