Skip to content

Instantly share code, notes, and snippets.

@nixjs
Created March 3, 2025 02:48
Show Gist options
  • Save nixjs/0399d75d70197363ce46e2f4ce5982d3 to your computer and use it in GitHub Desktop.
Save nixjs/0399d75d70197363ce46e2f4ce5982d3 to your computer and use it in GitHub Desktop.
Multi level dropdown
"use client"
import { useState, useRef, useEffect } from "react"
import { ChevronLeft, ChevronRight, Menu } from "lucide-react"
const locationData: MenuItem = {
id: 'root',
label: 'Location',
children: [
{
id: 'hcm',
label: 'Ho Chi Minh',
children: [
{
id: 'district1',
label: 'District 1',
children: [
{ id: 'ward1', label: 'Ward 1' },
{ id: 'ward2', label: 'Ward 2' },
{ id: 'ward3', label: 'Ward 3' },
{ id: 'ward4', label: 'Ward 4' },
{ id: 'ward5', label: 'Ward 5' }
]
},
{ id: 'district2', label: 'District 2' },
{ id: 'district3', label: 'District 3' },
{ id: 'district4', label: 'District 4' },
{ id: 'district5', label: 'District 5' }
]
},
{ id: 'hanoi', label: 'Ha Noi' },
{ id: 'vungtau', label: 'Vung Tau' },
{ id: 'dalat', label: 'Da Lat' },
{ id: 'nhatrang', label: 'Nha Trang' }
]
}
// Define the data structure for menu items
type MenuItem = {
id: string
label: string
children?: MenuItem[]
}
// Sample data structure
const locationData: MenuItem = {
id: "root",
label: "Location",
children: [
{
id: "hcm",
label: "Ho Chi Minh",
children: [
{
id: "district1",
label: "District 1",
children: [
{ id: "ward1", label: "Ward 1" },
{ id: "ward2", label: "Ward 2" },
{ id: "ward3", label: "Ward 3" },
{ id: "ward4", label: "Ward 4" },
{ id: "ward5", label: "Ward 5" },
],
},
{ id: "district2", label: "District 2" },
{ id: "district3", label: "District 3" },
{ id: "district4", label: "District 4" },
{ id: "district5", label: "District 5" },
],
},
{ id: "hanoi", label: "Ha Noi" },
{ id: "vungtau", label: "Vung Tau" },
{ id: "dalat", label: "Da Lat" },
{ id: "nhatrang", label: "Nha Trang" },
],
}
export default function MultiLevelDropdown() {
const [isOpen, setIsOpen] = useState(false)
const [navigationPath, setNavigationPath] = useState<MenuItem[]>([locationData])
const [selectedValue, setSelectedValue] = useState<string>("")
const dropdownRef = useRef<HTMLDivElement>(null)
// Current level is the last item in the navigation path
const currentLevel = navigationPath[navigationPath.length - 1]
// Close dropdown when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
document.addEventListener("mousedown", handleClickOutside)
return () => {
document.removeEventListener("mousedown", handleClickOutside)
}
}, [])
// Navigate to a child item
const navigateToChild = (item: MenuItem) => {
if (item.children && item.children.length > 0) {
setNavigationPath([...navigationPath, item])
} else {
// If it's a leaf node, select it and close the dropdown
setSelectedValue(item.label)
setIsOpen(false)
// Reset navigation path for next open
setNavigationPath([locationData])
}
}
// Navigate back to parent
const navigateBack = () => {
if (navigationPath.length > 1) {
setNavigationPath(navigationPath.slice(0, -1))
}
}
// Handle done button click
const handleDone = () => {
setSelectedValue(currentLevel.label)
setIsOpen(false)
setNavigationPath([locationData])
}
return (
<div className="relative w-full max-w-md mx-auto" ref={dropdownRef}>
{/* Dropdown trigger button */}
<button
className="flex items-center justify-between w-full p-3 border rounded-md bg-white"
onClick={() => setIsOpen(!isOpen)}
>
<span>{selectedValue || "Select Location"}</span>
<Menu className="h-5 w-5 text-gray-500" />
</button>
{/* Dropdown menu */}
{isOpen && (
<div className="absolute z-10 w-full mt-1 bg-white rounded-md shadow-lg border overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b">
{navigationPath.length > 1 ? (
<button onClick={navigateBack} className="p-1">
<ChevronLeft className="h-5 w-5" />
</button>
) : (
<div className="w-5"></div> // Empty space for alignment
)}
<span className="font-medium">{currentLevel.label}</span>
<button onClick={handleDone} className="text-blue-500 font-medium">
Done
</button>
</div>
{/* Menu items */}
<div className="max-h-60 overflow-y-auto">
{currentLevel.children?.map((item) => (
<div
key={item.id}
className="flex items-center justify-between p-4 hover:bg-gray-50 cursor-pointer border-b border-gray-100"
onClick={() => navigateToChild(item)}
>
<span>{item.label}</span>
{item.children && item.children.length > 0 && <ChevronRight className="h-4 w-4 text-gray-400" />}
</div>
))}
</div>
</div>
)}
</div>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment