-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added Menu component with simple stories view
- Loading branch information
1 parent
cb17e55
commit eb52aa6
Showing
6 changed files
with
251 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { MenuBtnWrap, MenuList, MenuWrap } from "./styles"; | ||
import { IMenuProps } from "./types"; | ||
import { Text } from "../Text"; | ||
import { useCallback, useMemo, useRef, useState } from "react"; | ||
import useOnClickOutside from "../../hooks/useOnClickOutside"; | ||
import { CheckIcon } from "../Svg"; | ||
import { Flex } from "../Box"; | ||
|
||
export default function Menu<T = any>({ | ||
activator, | ||
listWidth, | ||
align = "center", | ||
offsetX = 0, | ||
offsetY = 0, | ||
opened, | ||
openedChange = (v: boolean) => {}, | ||
value, | ||
onChange, | ||
renderItem, | ||
items = [], | ||
valueKey, | ||
multiple, | ||
canByEmpty, | ||
}: IMenuProps<T>) { | ||
const [locOpened, setLocOpened] = useState(opened); | ||
|
||
const menuRef = useRef<HTMLDivElement>(null); | ||
|
||
const values = useMemo(() => (Array.isArray(value) ? value : value ? [value] : []), [value]); | ||
|
||
const openedChangeHandler = (opened: boolean) => { | ||
openedChange(opened); | ||
setLocOpened(opened); | ||
}; | ||
|
||
const isOpened = useMemo(() => (typeof opened === "boolean" ? opened : locOpened), [opened, locOpened]); | ||
|
||
useOnClickOutside(menuRef, () => { | ||
openedChangeHandler(false); | ||
}); | ||
|
||
const getWithValueKey = useCallback( | ||
(item: T) => { | ||
return typeof valueKey === "undefined" ? item : item[valueKey]; | ||
}, | ||
[valueKey] | ||
); | ||
|
||
return ( | ||
<MenuWrap align={align} ref={menuRef}> | ||
<MenuBtnWrap | ||
onClick={() => { | ||
openedChangeHandler(!isOpened); | ||
}} | ||
> | ||
{activator || ( | ||
<Text bg={"black"} color={"textGray"} p={"4px"}> | ||
Activator | ||
</Text> | ||
)} | ||
</MenuBtnWrap> | ||
{isOpened && ( | ||
<MenuList width={listWidth} offsetX={offsetX} offsetY={offsetY}> | ||
{items.map((item, i) => { | ||
const isActive = values.includes(getWithValueKey(item)); | ||
return ( | ||
<MenuBtnWrap | ||
key={i} | ||
onClick={() => { | ||
if (onChange) { | ||
const itemValue = getWithValueKey(item); | ||
if (!multiple) { | ||
const newValue = isActive ? undefined : itemValue; | ||
if (!(newValue === undefined && !canByEmpty)) onChange(newValue); | ||
} else { | ||
const newValues = items.map(getWithValueKey).filter((v) => { | ||
if (isActive) { | ||
return values.includes(v) && v !== itemValue; | ||
} else { | ||
return [itemValue, ...values].includes(v); | ||
} | ||
}); | ||
onChange(newValues); | ||
} | ||
} | ||
}} | ||
> | ||
{renderItem ? ( | ||
renderItem(item, isActive) | ||
) : ( | ||
<Flex p={"8px"} alignItems={"center"} justifyContent={"space-between"}> | ||
<Text color={"textGray"}>{String(getWithValueKey(item))}</Text> | ||
{isActive && <CheckIcon size={"16px"} color={"textGray"} />} | ||
</Flex> | ||
)} | ||
</MenuBtnWrap> | ||
); | ||
})} | ||
</MenuList> | ||
)} | ||
</MenuWrap> | ||
); | ||
} |
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 |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import MenuComponent from "./Menu"; | ||
import { Box, Flex, Grid } from "../Box"; | ||
import { Text } from "../Text"; | ||
import React, { useState } from "react"; | ||
import { Button } from "../Button"; | ||
|
||
export default { | ||
title: "Components/Menu", | ||
component: MenuComponent, | ||
argTypes: {}, | ||
}; | ||
|
||
export const Menu = () => { | ||
const numericItems = [1, 2, 3, 4, 5]; | ||
|
||
const items = [ | ||
{ id: 1, name: "First Item" }, | ||
{ id: 2, name: "Second Item" }, | ||
{ id: 3, name: "Last Item" }, | ||
]; | ||
|
||
const [opened, setOpened] = useState(false); | ||
const [value0, setValue0] = useState(1); | ||
const [value1, setValue1] = useState(2); | ||
return ( | ||
<Grid gridTemplateColumns={"repeat(2, 1fr)"} mt={"10px"}> | ||
<Box> | ||
<Text variant={"h5"} mb="20px"> | ||
Simple | ||
</Text> | ||
<MenuComponent<(typeof numericItems)[number]> | ||
listWidth={"100%"} | ||
align={"center"} | ||
offsetY={5} | ||
items={numericItems} | ||
value={value0} | ||
onChange={setValue0} | ||
/> | ||
</Box> | ||
<Box> | ||
<Text variant={"h5"} mb="20px"> | ||
Reactive (custom activator & items) | ||
</Text> | ||
<Flex alignItems={"center"}> | ||
<MenuComponent<(typeof items)[number]> | ||
align={"center"} | ||
offsetY={5} | ||
opened={opened} | ||
openedChange={setOpened} | ||
items={items} | ||
valueKey={"id"} | ||
value={value1} | ||
onChange={setValue1} | ||
listWidth={"100%"} | ||
activator={ | ||
<Text bg={"black"} color={"textGray"} p={"4px"} width={"120px"} height={"26px"}> | ||
{items.find((i) => i.id === value1)?.name} | ||
</Text> | ||
} | ||
renderItem={(item, isActive) => ( | ||
<Text bg={"black"} color={isActive ? "red" : "textGray"} p={"8px"}> | ||
{item.name} | ||
</Text> | ||
)} | ||
/> | ||
<Button | ||
width={"90px"} | ||
scale={"small"} | ||
disabled={opened} | ||
onClick={() => { | ||
setOpened(true); | ||
}} | ||
ml={"40px"} | ||
> | ||
Open (reactive) | ||
</Button> | ||
</Flex> | ||
</Box> | ||
<Text color="strokeGray" variant={"h5"} mt="40px"> | ||
Overlapping content. | ||
</Text> | ||
<Text color="strokeGray" variant={"h5"} mt="40px"> | ||
Overlapping content. | ||
</Text> | ||
</Grid> | ||
); | ||
}; |
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 |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { default as Menu } from "./Menu"; | ||
export type { IMenuProps } from "./types"; |
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 |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import styled from "styled-components"; | ||
import { Flex, Grid } from "../Box"; | ||
import { Z_INDEX } from "../../constants"; | ||
import { rgba } from "polished"; | ||
import { IMenuProps } from "./types"; | ||
|
||
type AlignProp = Exclude<IMenuProps["align"], undefined>; | ||
|
||
const menuAligns: { [key in AlignProp]: string } = { | ||
center: "center", | ||
left: "flex-start", | ||
right: "flex-end", | ||
}; | ||
|
||
export const MenuWrap = styled(Flex)<{ align: AlignProp }>` | ||
display: inline-flex; | ||
position: relative; | ||
justify-content: ${({ align }) => menuAligns[align]}; | ||
`; | ||
|
||
export const MenuBtnWrap = styled.button` | ||
outline: none; | ||
background: transparent; | ||
border: none; | ||
cursor: pointer; | ||
padding: 0; | ||
`; | ||
|
||
export const MenuList = styled(Grid)<{ offsetX: number; offsetY: number }>` | ||
position: absolute; | ||
top: 100%; | ||
transform: ${({ offsetX, offsetY }) => `translate(${offsetX}px, ${offsetY}px)`}; | ||
background: ${({ theme }) => theme.colors.darkBg}; | ||
box-shadow: 0 2px 16px -4px ${({ theme }) => rgba(theme.colors.shadowDark, 0.04)}; | ||
border: 2px solid ${({ theme: A }) => rgba(A.colors.strokeGray, 0.4)}; | ||
border-radius: 12px; | ||
box-sizing: border-box; | ||
z-index: ${Z_INDEX.DROPDOWN}; | ||
overflow: hidden; | ||
`; |
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 |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { ReactNode } from "react"; | ||
|
||
export interface IMenuProps<T = any> { | ||
opened?: boolean; | ||
openedChange?: (value: boolean) => any; | ||
activator?: ReactNode; | ||
listWidth?: string; | ||
align?: "center" | "left" | "right"; | ||
offsetX?: number; | ||
offsetY?: number; | ||
items: T[]; | ||
renderItem?: (value: T, isActive: boolean) => ReactNode; | ||
value?: any; | ||
valueKey?: keyof T; | ||
onChange?: (value: any) => any; | ||
multiple?: boolean; | ||
canByEmpty?: boolean; | ||
} |
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