-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(web): basic widget page (#578)
- Loading branch information
Showing
57 changed files
with
1,857 additions
and
699 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.