Uniformize folder structure (#693)

* Uniformize folder structure

* Fix icons

* Fix icons

* Fix tests

* Fix tests
This commit is contained in:
Charles Bochet
2023-07-16 14:29:28 -07:00
committed by GitHub
parent 900ec5572f
commit 6ced8434bd
462 changed files with 931 additions and 960 deletions
@@ -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>,
),
};