Skip to content

Commit

Permalink
chore(web): basic widget page (#578)
Browse files Browse the repository at this point in the history
  • Loading branch information
KaWaite authored Jul 21, 2023
1 parent c4f2c11 commit 3e8dc04
Show file tree
Hide file tree
Showing 57 changed files with 1,857 additions and 699 deletions.
25 changes: 7 additions & 18 deletions web/src/beta/components/Dropdown/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Meta, StoryObj } from "@storybook/react";
import { ReactNode } from "react";

import Dropdown from ".";
import Dropdown, { Menu } from ".";

const Wrapper: React.FC<{ children: ReactNode }> = ({ children }) => (
<div style={{ width: "100px", height: "60px" }}>{children}</div>
Expand All @@ -15,41 +15,30 @@ export default meta;

type Story = StoryObj<typeof Dropdown>;

const DropDownContent: React.FC = () => {
return (
<ul>
<li>Apple</li>
<li>Banana</li>
<li>Orange</li>
</ul>
);
const menu: Menu = {
width: 200,
items: [{ text: "HARHAR" }, { text: "HARHAR" }, { text: "HARHAR" }],
};

export const Default: Story = {
render: () => (
<Wrapper>
<Dropdown isOpen label="Sample">
<DropDownContent />
</Dropdown>
<Dropdown isOpen label="Sample" menu={menu} />
</Wrapper>
),
};

export const DirectionRight: Story = {
render: () => (
<Wrapper>
<Dropdown isOpen direction="right" label="Sample">
<DropDownContent />
</Dropdown>
<Dropdown isOpen direction="right" label="Sample" menu={menu} />
</Wrapper>
),
};
export const DirectionDown: Story = {
render: () => (
<Wrapper>
<Dropdown isOpen direction="down" label="Sample">
<DropDownContent />
</Dropdown>
<Dropdown isOpen direction="down" label="Sample" menu={menu} />
</Wrapper>
),
};
259 changes: 187 additions & 72 deletions web/src/beta/components/Dropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -1,141 +1,256 @@
import { ReactNode, useState, useRef, forwardRef, useImperativeHandle } from "react";
import { useClickAway } from "react-use";
import { ReactNode, useState, Fragment } from "react";

import Icon from "@reearth/beta/components/Icon";
import Icon, { Icons } from "@reearth/beta/components/Icon";
import {
MenuListItemLabel,
MenuList,
MenuListItem,
} from "@reearth/beta/features/Navbar/Menus/MenuList";
import { styled } from "@reearth/services/theme";

import Text from "../Text";

type Direction = "right" | "down" | "none";

type Gap = "sm" | "md" | "lg";

export type Menu = {
width?: number;
items?: MenuItem[];
};

export type MenuItem = {
text?: string;
icon?: Icons;
iconPosition?: "left" | "right";
breakpoint?: boolean;
items?: MenuItem[];
linkTo?: string;
selected?: boolean;
onClick?: () => void;
};

export type Props = {
className?: string;
isOpen?: boolean;
label: ReactNode;
openOnClick?: boolean;
direction?: Direction;
gap?: Gap;
dropdownWidth?: number;
parentWidth?: number;
hasIcon?: boolean;
noHoverStyle?: boolean;
centered?: boolean;
children?: ReactNode;
menu?: Menu;
isChild?: boolean;
onClick?: () => void;
};

export type Ref = {
close: () => void;
};
const defaultWidth = 200;

const Dropdown: React.ForwardRefRenderFunction<Ref, Props> = (
{
className,
isOpen = false,
openOnClick,
noHoverStyle,
centered,
label,
direction = "down",
hasIcon,
children,
},
ref,
) => {
const Dropdown: React.FC<Props> = ({
className,
isOpen = false,
label,
openOnClick,
direction = "down",
gap,
dropdownWidth = defaultWidth,
parentWidth = defaultWidth,
centered,
noHoverStyle,
hasIcon,
menu,
isChild,
onClick,
}) => {
const [open, setOpen] = useState(isOpen);

const wrapperRef = useRef(null);
useClickAway(wrapperRef, () => {
setOpen(false);
});

useImperativeHandle(
ref,
() => ({
close: () => setOpen(false),
}),
[],
);

return (
<Wrapper
className={className}
ref={wrapperRef}
onMouseEnter={openOnClick ? undefined : () => setOpen(true)}
onMouseLeave={openOnClick ? undefined : () => setOpen(false)}>
<Parent noHover={noHoverStyle} centered={centered}>
<Label onClick={openOnClick ? () => setOpen(o => !o) : undefined}>
<Wrapper className={className}>
<Parent
noHover={noHoverStyle}
centered={centered}
onClick={openOnClick ? () => setOpen(o => !o) : undefined}
onMouseEnter={openOnClick ? undefined : () => setOpen(true)}
onMouseLeave={openOnClick ? undefined : () => setOpen(false)}>
<Label isChild={isChild}>
{label}
{hasIcon && (
<StyledIcon icon={direction === "right" ? "arrowRight" : "arrowDown"} size={24} />
<StyledIcon icon={direction === "right" ? "arrowRight" : "arrowDown"} size={16} />
)}
</Label>
</Parent>
{open && (
<Child onClick={openOnClick ? () => setOpen(o => !o) : undefined} direction={direction}>
{children}
</Child>
<StyledMenuList
direction={direction}
gap={gap}
dropdownWidth={dropdownWidth}
parentWidth={parentWidth}>
{menu?.items?.map((item, idx) => {
const handleClick = () => {
onClick?.();
setOpen(false);
item.onClick?.();
};

return (
<Fragment key={idx}>
{item.items ? (
<Dropdown
direction="right"
hasIcon
openOnClick
gap="sm"
isChild
dropdownWidth={menu.width}
parentWidth={dropdownWidth}
label={
<MenuListItem noHover>
<MenuListItemLabel text={item.text} />
</MenuListItem>
}
menu={{ items: item.items }}
onClick={handleClick}
/>
) : item.linkTo ? (
<MenuListItem>
<MenuListItemLabel linkTo={item.linkTo} text={item.text} icon={item.icon} />
{item.selected && <SelectedIcon isActive />}
</MenuListItem>
) : item.onClick ? (
<MenuListItem>
<MenuListItemLabel onClick={handleClick} text={item.text} icon={item.icon} />
{item.selected && <SelectedIcon isActive />}
</MenuListItem>
) : item.text ? (
<InfoOnlyItem size="h5">{item.text}</InfoOnlyItem>
) : item.breakpoint ? (
<Spacer />
) : null}
</Fragment>
);
})}
</StyledMenuList>
)}
</Wrapper>
);
};

const Wrapper = styled.div`
position: relative;
display: flex;
align-items: center;
height: 100%;
`;

const Parent = styled.div<{ noHover?: boolean; centered?: boolean }>`
position: relative;
height: inherit;
width: 100%;
display: flex;
align-items: center;
justify-content: ${({ centered }) => centered && `center;`}
background-color: ${({ theme }) => theme.navbar.bg.main};
&:hover {
${({ centered }) => centered && "justify-content: center;"}
height: inherit;
border-radius: 4px;
color: inherit;
transition: all 0.3s;
cursor: pointer;
:hover {
${({ noHover, theme }) =>
!noHover &&
`
background-color: ${theme.navbar.bg.hover};
color: ${theme.general.content.main};
background: ${theme.general.bg.main};
`}
}
`;
}
`;

const Label = styled.div`
const Label = styled.div<{ isChild?: boolean }>`
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
cursor: pointer;
width: 100%;
gap: 8px;
user-select: none;
${({ isChild }) =>
!isChild &&
`
margin-left: 8px;
margin-right: 8px;
`}
`;

const childTransform = (direction: Direction) => {
const childTransform = (direction: Direction, width: number, gap?: Gap) => {
let translateValue;
if (direction === "down") {
translateValue = gap ? (gap === "sm" ? "102%" : gap === "md" ? "104%" : "106%") : "100%";
} else {
translateValue = gap
? gap === "sm"
? `${width * 1.02}px`
: gap === "md"
? `${width * 1.04}px`
: `${width * 1.06}px`
: width;
}
switch (direction) {
case "down":
return "translateY(100%)";
case "right":
case "none":
return "translateX(100%)";
return `translateX(${translateValue})`;
case "down":
default:
return "translateY(100%)";
return `translateY(${translateValue})`;
}
};

const Child = styled.div<{ direction: Direction }>`
const StyledMenuList = styled(MenuList)<{
direction: Direction;
dropdownWidth: number;
parentWidth?: number;
gap?: Gap;
}>`
position: absolute;
background-color: ${({ theme }) => theme.navbar.bg.main};
min-width: 200px;
max-width: 230px;
background: ${({ theme }) => theme.navbar.bg.main};
width: ${({ dropdownWidth }) => (dropdownWidth ? `${dropdownWidth}px` : "200px")};
margin: 0 auto;
padding: 2px 0;
left: 0;
right: 0;
top: ${({ direction }) => (direction === "down" ? "auto" : "0")};
bottom: ${({ direction }) => (direction === "down" ? "0" : "auto")};
transform: ${({ direction }) => childTransform(direction)};
transform: ${({ direction, dropdownWidth, parentWidth, gap }) =>
childTransform(direction, parentWidth ?? dropdownWidth, gap)};
box-shadow: 6px 6px 8px rgba(0, 0, 0, 0.3);
z-index: ${props => props.theme.zIndexes.dropDown};
`;

const StyledIcon = styled(Icon)`
margin-left: 8px;
color: ${({ theme }) => theme.general.content.weak};
pointer-events: none;
`;

const SelectedIcon = styled.div<{ isActive: boolean }>`
width: 10px;
height: 10px;
border-radius: 50%;
margin-left: 4px;
order: 2;
background-color: ${({ theme }) => theme.general.select};
`;

// const ChildrenWrapper = styled.div`
// background: ${({ theme }) => theme.general.bg.strong};
// border-radius: 4px;
// `;

const Spacer = styled.div`
border-top: 0.5px solid ${({ theme }) => theme.general.border};
margin: 2px 0;
`;

const InfoOnlyItem = styled(Text)`
padding: 2px 16px;
cursor: default;
user-select: none;
`;

export default forwardRef(Dropdown);
export default Dropdown;
6 changes: 6 additions & 0 deletions web/src/beta/components/Icon/Icons/desktop.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions web/src/beta/components/Icon/Icons/folderPlus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 3e8dc04

Please sign in to comment.