Created
March 3, 2025 02:48
-
-
Save nixjs/0399d75d70197363ce46e2f4ce5982d3 to your computer and use it in GitHub Desktop.
Multi level dropdown
This file contains hidden or 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
"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