From 4c162ee288b0addcd2095b33ab83409b52afef31 Mon Sep 17 00:00:00 2001 From: Oluwatomilola Date: Wed, 29 Jul 2026 17:01:51 +0100 Subject: [PATCH] refactor: enhance MarketSelector component with keyboard accessibility and improved event handling --- .../trade/components/chart/MarketSelector.tsx | 168 +++++++++++++++--- 1 file changed, 143 insertions(+), 25 deletions(-) diff --git a/apps/web/src/features/trade/components/chart/MarketSelector.tsx b/apps/web/src/features/trade/components/chart/MarketSelector.tsx index d435b84..51243ac 100644 --- a/apps/web/src/features/trade/components/chart/MarketSelector.tsx +++ b/apps/web/src/features/trade/components/chart/MarketSelector.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from "react" +import { type KeyboardEvent, type MouseEvent, useState, useRef, useEffect, useId } from "react" import { MARKETS, type Market } from "../../data/markets" import { useTokenPrices } from "../../hooks/useTokenPrices" import { usePriceDelta24h } from "../../hooks/usePriceDelta24h" @@ -12,10 +12,12 @@ function MarketRow({ market, isActive, onSelect, + id, }: { market: Market isActive: boolean onSelect: () => void + id: string }) { const { getMidPrice } = useTokenPrices() const { data: delta } = usePriceDelta24h(market.indexTokenAddress) @@ -23,14 +25,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 activeMarket = MARKETS.find((m) => m.indexTokenAddress === symbol) @@ -69,17 +91,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 () => { @@ -88,14 +121,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)} /> )) )} -
+
)}