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

View 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;
`;

View File

@ -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));
`;

View File

@ -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>
);
}

View File

@ -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));
`;

View File

@ -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));
`;

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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%;
`;

View File

@ -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>,
),
};