diff --git a/apps/web/src/features/trade/components/chart/MarketSelector.tsx b/apps/web/src/features/trade/components/chart/MarketSelector.tsx index 82bfd3a..e06c0de 100644 --- a/apps/web/src/features/trade/components/chart/MarketSelector.tsx +++ b/apps/web/src/features/trade/components/chart/MarketSelector.tsx @@ -14,10 +14,12 @@ function MarketRow({ market, isActive, onSelect, + id, }: { market: Market isActive: boolean onSelect: () => void + id: string }) { const { getMidPrice } = useTokenPrices() const { data: delta } = usePriceDelta24h(market.indexTokenAddress) @@ -25,14 +27,30 @@ function MarketRow({ const isPositive = (delta?.deltaPercentage ?? 0) > 0 const isNegative = (delta?.deltaPercentage ?? 0) < 0 + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault() + onSelect() + } + } + return ( - + ) } export function MarketSelector({ symbol, onSelect }: Props) { const [open, setOpen] = useState(false) const [search, setSearch] = useState("") + const [activeIndex, setActiveIndex] = useState(0) const containerRef = useRef(null) + const inputRef = useRef(null) + const comboboxId = useId() + const listboxId = `${comboboxId}-listbox` const { markets } = useMarkets() @@ -73,17 +95,28 @@ export function MarketSelector({ symbol, onSelect }: Props) { m.indexTokenAddress.toLowerCase().includes(search.toLowerCase()), ) - // Close on outside click useEffect(() => { if (!open) return + + const currentIndex = filtered.findIndex((m) => m.indexTokenAddress === symbol) + setActiveIndex(Math.max(0, currentIndex)) + }, [filtered, open, symbol]) + + useEffect(() => { + if (!open) return + function handleClick(e: MouseEvent) { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setOpen(false) } } + function handleKey(e: KeyboardEvent) { - if (e.key === "Escape") setOpen(false) + if (e.key === "Escape") { + setOpen(false) + } } + document.addEventListener("mousedown", handleClick) document.addEventListener("keydown", handleKey) return () => { @@ -92,14 +125,81 @@ export function MarketSelector({ symbol, onSelect }: Props) { } }, [open]) + const selectMarket = (market: Market) => { + onSelect(market.indexTokenAddress) + setOpen(false) + setSearch("") + } + + const moveActive = (delta: number) => { + if (filtered.length === 0) return + setActiveIndex((current) => { + const next = current + delta + return Math.max(0, Math.min(next, filtered.length - 1)) + }) + } + + const handleInputKeyDown = (event: KeyboardEvent) => { + if (event.key === "ArrowDown") { + event.preventDefault() + if (!open) { + setOpen(true) + return + } + moveActive(1) + } + + if (event.key === "ArrowUp") { + event.preventDefault() + moveActive(-1) + } + + if (event.key === "Home") { + event.preventDefault() + setActiveIndex(0) + } + + if (event.key === "End") { + event.preventDefault() + setActiveIndex(filtered.length - 1) + } + + if (event.key === "Enter") { + event.preventDefault() + const market = filtered[activeIndex] + if (market) selectMarket(market) + } + } + + const handleToggle = () => { + setOpen((value) => { + const next = !value + if (next) { + setTimeout(() => inputRef.current?.focus(), 0) + } + return next + }) + } + + const handleClear = (event: MouseEvent) => { + event.stopPropagation() + onSelect("") + setSearch("") + setOpen(false) + } + return ( -
+
+ {symbol && ( + + )} + {open && (
+ setSearch(e.target.value)} + onKeyDown={handleInputKeyDown} placeholder="Search markets..." className="w-full rounded bg-background px-2.5 py-1.5 text-xs text-foreground outline-none ring-1 ring-border placeholder:text-muted-foreground focus:ring-primary" />
-
+
    {filtered.length === 0 ? ( -

    - No markets found -

    +
  • No markets found
  • ) : ( - filtered.map((market) => ( + filtered.map((market, index) => ( { - onSelect(market.indexTokenAddress) - setOpen(false) - setSearch("") - }} + isActive={index === activeIndex} + onSelect={() => selectMarket(market)} /> )) )} -
+
)}