import { useState, useRef, useEffect, useCallback } from "react"; // ============================================= // Types & Data (DO NOT MODIFY) // ============================================= interface Option { value: string; label: string; } const OPTIONS: Option[] = [ { value: "react", label: "React" }, { value: "vue", label: "Vue" }, { value: "angular", label: "Angular" }, { value: "svelte", label: "Svelte" }, { value: "nextjs", label: "Next.js" }, { value: "nuxt", label: "Nuxt" }, { value: "remix", label: "Remix" }, { value: "astro", label: "Astro" }, { value: "solid", label: "SolidJS" }, { value: "qwik", label: "Qwik" }, ]; // ============================================= // TODO: Build a Multi-Select Dropdown // ============================================= // // Requirements: // 1. Click trigger to open/close the dropdown // 2. Select/deselect options by clicking them // 3. Show selected items as chips with a remove (×) button // 4. Search/filter input inside the dropdown // 5. Close on outside click (useRef + useEffect) // 6. Keyboard navigation: // - ArrowDown/ArrowUp to move focus // - Enter to toggle focused option // - Escape to close // 7. Select All / Clear All actions // 8. ARIA attributes (role="listbox", role="option", aria-selected, etc.) // // Hints: // - `selected`: string[] of selected values // - `isOpen`: boolean for dropdown visibility // - `search`: string for filtering options // - `focusedIndex`: number for keyboard-highlighted option // - Use a ref on the wrapper div to detect outside clicks // - Filter OPTIONS by search string for the visible list // - toggleOption: add if not selected, remove if selected export default function App() { // TODO: Set up state const [selected, setSelected] = useState<string[]>([]); const [isOpen, setIsOpen] = useState(false); const [search, setSearch] = useState(""); const [focusedIndex, setFocusedIndex] = useState(-1); // TODO: Create refs for outside click detection and list scrolling // const wrapperRef = useRef<HTMLDivElement>(null); // const listRef = useRef<HTMLUListElement>(null); // const searchInputRef = useRef<HTMLInputElement>(null); // ============================================= // TODO: Outside click detection // ============================================= // useEffect(() => { // const handleClickOutside = (e: MouseEvent) => { // if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) { // close dropdown, reset search & focusedIndex // } // }; // document.addEventListener("mousedown", handleClickOutside); // return () => document.removeEventListener("mousedown", handleClickOutside); // }, []); // ============================================= // TODO: Implement toggleOption // ============================================= // If value is in selected → remove it // If value is not in selected → add it const toggleOption = (value: string) => { // Your code here }; // ============================================= // TODO: Implement removeOption (for chip × button) // ============================================= const removeOption = (value: string) => { // Your code here }; // ============================================= // TODO: Implement selectAll and clearAll // ============================================= const selectAll = () => { // Your code here }; const clearAll = () => { // Your code here }; // ============================================= // TODO: Filter options by search string // ============================================= // const filteredOptions = OPTIONS.filter(...) const filteredOptions = OPTIONS; // ← replace with filtered version // Helper to get label from value const getLabel = (value: string) => OPTIONS.find((o) => o.value === value)?.label ?? value; // ============================================= // TODO: Keyboard navigation handler // ============================================= // Handle these keys inside the dropdown: // ArrowDown → move focusedIndex forward (wrap around) // ArrowUp → move focusedIndex backward (wrap around) // Enter → toggle the focused option // Escape → close the dropdown const handleDropdownKeyDown = (e: React.KeyboardEvent) => { // Your code here }; return ( <div style={{ maxWidth: 480, margin: "0 auto", padding: 24, fontFamily: "system-ui" }}> <h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 4 }}> Multi-Select Dropdown </h2> <p style={{ fontSize: 14, color: "#666", marginBottom: 24 }}> Build a dropdown with multiple selection, search, chips, keyboard nav, and accessibility. </p> {/* ============================================= */} {/* TODO: Dropdown wrapper (attach wrapperRef) */} {/* ============================================= */} <div style={{ position: "relative", marginBottom: 24 }}> {/* =========================================== */} {/* TODO: Trigger button */} {/* =========================================== */} {/* - Shows chips for selected items, or placeholder text - Clicking toggles isOpen - Add aria-haspopup="listbox" and aria-expanded={isOpen} - Each chip has a × button that calls removeOption - Show a chevron icon that rotates when open */} <button type="button" onClick={() => setIsOpen(!isOpen)} style={{ width: "100%", minHeight: 48, padding: "8px 12px", border: "1px solid #d1d5db", borderRadius: 8, background: "#fff", textAlign: "left", display: "flex", alignItems: "center", flexWrap: "wrap", gap: 6, cursor: "pointer", fontSize: 14, }} > {selected.length === 0 ? ( <span style={{ color: "#999" }}>Select frameworks...</span> ) : ( selected.map((val) => ( <span key={val} style={{ display: "inline-flex", alignItems: "center", gap: 4, padding: "2px 8px", background: "#f3f4f6", borderRadius: 4, fontSize: 13, }} > {getLabel(val)} {/* TODO: Add remove button for each chip */} </span> )) )} <span style={{ marginLeft: "auto", color: "#999", fontSize: 18 }}> {isOpen ? "▲" : "▼"} </span> </button> {/* =========================================== */} {/* TODO: Dropdown panel (only render if isOpen) */} {/* =========================================== */} {isOpen && ( <div onKeyDown={handleDropdownKeyDown} style={{ position: "absolute", top: "100%", left: 0, right: 0, marginTop: 4, background: "#fff", border: "1px solid #e5e7eb", borderRadius: 8, boxShadow: "0 4px 12px rgba(0,0,0,0.08)", zIndex: 10, overflow: "hidden", }} > {/* TODO: Search input */} <div style={{ padding: 8, borderBottom: "1px solid #f3f4f6" }}> <input type="text" value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Search..." style={{ width: "100%", padding: "6px 10px", border: "1px solid #e5e7eb", borderRadius: 6, fontSize: 13, outline: "none", boxSizing: "border-box", }} /> </div> {/* TODO: Select All / Clear All actions */} <div style={{ display: "flex", justifyContent: "space-between", padding: "6px 12px", borderBottom: "1px solid #f3f4f6", fontSize: 12, }}> <button onClick={selectAll} style={{ color: "#666", cursor: "pointer" }}> Select All </button> <button onClick={clearAll} style={{ color: "#666", cursor: "pointer" }}> Clear All </button> </div> {/* TODO: Options list */} {/* - Use <ul role="listbox" aria-multiselectable="true"> - Each <li role="option" aria-selected={isSelected}> - Show a checkbox indicator (filled when selected) - Highlight focused item with a background color - Click to toggleOption */} <ul style={{ maxHeight: 240, overflowY: "auto", padding: "4px 0", margin: 0, listStyle: "none" }}> {filteredOptions.length === 0 ? ( <li style={{ padding: "12px 16px", textAlign: "center", color: "#999", fontSize: 13 }}> No results found </li> ) : ( filteredOptions.map((option, index) => { const isSelected = selected.includes(option.value); const isFocused = index === focusedIndex; return ( <li key={option.value} onClick={() => toggleOption(option.value)} style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 14px", cursor: "pointer", fontSize: 13, background: isFocused ? "#f3f4f6" : isSelected ? "#f9fafb" : "transparent", color: isSelected ? "#111" : "#555", }} > {/* TODO: Checkbox indicator */} <span style={{ width: 16, height: 16, borderRadius: 3, border: isSelected ? "none" : "1px solid #d1d5db", background: isSelected ? "#111" : "transparent", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, color: "#fff", fontSize: 11, }}> {isSelected ? "✓" : ""} </span> {option.label} </li> ); }) )} </ul> </div> )} </div> {/* Selected output */} <div style={{ border: "1px solid #e5e7eb", borderRadius: 8, padding: 16, background: "#f9fafb", marginBottom: 24, }}> <p style={{ fontSize: 11, color: "#999", textTransform: "uppercase", letterSpacing: 1, fontWeight: 600, marginBottom: 8 }}> Selected Values </p> <pre style={{ fontSize: 13, fontFamily: "monospace", color: "#333", margin: 0 }}> {JSON.stringify(selected, null, 2)} </pre> </div> {/* Hint */} <div style={{ padding: 16, background: "#f9fafb", borderLeft: "4px solid #111", borderRadius: "0 8px 8px 0", fontSize: 13, color: "#555", }}> <strong>Checklist:</strong> <ul style={{ margin: "8px 0 0 16px", padding: 0, lineHeight: 1.8 }}> <li>Toggle options on click</li> <li>Remove chips with × button</li> <li>Filter options with search input</li> <li>Close on outside click</li> <li>Arrow keys to navigate, Enter to select, Escape to close</li> <li>Select All / Clear All</li> <li>Add ARIA attributes (role, aria-selected, aria-expanded)</li> </ul> </div> </div> ); }