Skip to content

Commit 209ddec

Browse files
committed
[#97] ✨ add dropdown component
1 parent 5b4dbf3 commit 209ddec

File tree

2 files changed

+147
-0
lines changed

2 files changed

+147
-0
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { createContext, useContext, useState } from 'react'
2+
3+
import clsx from 'clsx'
4+
import { twMerge } from 'tailwind-merge'
5+
6+
import { Box } from '@/components/common/containers'
7+
8+
type BaseProps = React.HTMLAttributes<HTMLElement>
9+
10+
interface DropdownContextType {
11+
isOpen: boolean
12+
toggle: () => void
13+
close: () => void
14+
}
15+
16+
const DropdownContext = createContext<DropdownContextType | null>(null)
17+
18+
const useDropdownContext = () => {
19+
const context = useContext(DropdownContext)
20+
if (!context) {
21+
throw new Error('useDropdownContext must be used within a DropdownProvider')
22+
}
23+
return context
24+
}
25+
26+
const Dropdown = ({ children, className }: BaseProps): JSX.Element => {
27+
const [isOpen, setIsOpen] = useState(false)
28+
29+
const toggle = () => setIsOpen(!isOpen)
30+
const close = () => setIsOpen(false)
31+
32+
const dropdownClass = twMerge('relative', className)
33+
34+
const handleBlur = (event: React.FocusEvent<HTMLDivElement>) => {
35+
const relatedTarget = event.relatedTarget as HTMLElement
36+
37+
if (!relatedTarget || !event.currentTarget.contains(relatedTarget)) {
38+
// close()
39+
}
40+
}
41+
42+
return (
43+
<DropdownContext.Provider value={{ isOpen, toggle, close }}>
44+
<div className={dropdownClass} onBlur={handleBlur}>
45+
{children}
46+
</div>
47+
</DropdownContext.Provider>
48+
)
49+
}
50+
51+
const Trigger = ({ children, className }: BaseProps) => {
52+
const { toggle } = useDropdownContext()
53+
54+
return (
55+
<button
56+
type='button'
57+
className={className}
58+
onClick={toggle}
59+
aria-haspopup='listbox'
60+
aria-expanded={true}
61+
>
62+
{children}
63+
</button>
64+
)
65+
}
66+
67+
interface MenuProps extends BaseProps {
68+
position?: 'dropup' | 'dropdown'
69+
alignment?: 'left' | 'right'
70+
}
71+
72+
const getMenuStyle = (position: string, alignment: string, className: string) =>
73+
twMerge(
74+
'absolute z-10 w-216 flex-col items-start p-8 shadow-level4',
75+
position === 'dropdown' ? 'mt-4' : 'bottom-full mb-4',
76+
alignment === 'right' ? 'right-0' : 'left-0',
77+
className
78+
)
79+
80+
const Menu = ({
81+
children,
82+
className = '',
83+
position = 'dropdown',
84+
alignment = 'left',
85+
}: MenuProps): JSX.Element | null => {
86+
const { isOpen } = useDropdownContext()
87+
if (!isOpen) return null
88+
89+
return (
90+
<Box
91+
role='listbox'
92+
variant='contained'
93+
className={getMenuStyle(position, alignment, className)}
94+
>
95+
{children}
96+
</Box>
97+
)
98+
}
99+
100+
interface ItemProps extends BaseProps {
101+
closeOnSelect?: boolean
102+
}
103+
104+
const getItemStyle = (className: string) =>
105+
clsx(
106+
'flex h-40 w-full items-center rounded-8 px-12 text-body2 text-gray-800 hover:bg-gray-100',
107+
className
108+
)
109+
110+
const Item = ({
111+
children,
112+
className = '',
113+
closeOnSelect = true,
114+
onClick,
115+
...props
116+
}: ItemProps) => {
117+
const { close } = useDropdownContext()
118+
119+
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
120+
if (typeof onClick === 'function') {
121+
onClick(e)
122+
}
123+
if (closeOnSelect) {
124+
close()
125+
}
126+
}
127+
128+
return (
129+
<button
130+
className={getItemStyle(className)}
131+
type='button'
132+
onClick={handleClick}
133+
{...props}
134+
>
135+
{children}
136+
</button>
137+
)
138+
}
139+
140+
Dropdown.Trigger = Trigger
141+
Dropdown.Menu = Menu
142+
Dropdown.Item = Item
143+
144+
export { Dropdown }
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Dropdown } from './Dropdown'
2+
3+
export { Dropdown }

0 commit comments

Comments
 (0)