From 08ff8f569b034b8dc57be7569016ff71cdfe8c5c Mon Sep 17 00:00:00 2001 From: theborakompanioni Date: Sat, 10 Jan 2026 11:57:45 +0100 Subject: [PATCH] chore(ui): add dropdown menu component --- package-lock.json | 88 +++++++ package.json | 1 + src/components/orderbook/OrderbookContent.tsx | 84 ++----- src/components/ui/dropdown-menu.tsx | 218 ++++++++++++++++++ 4 files changed, 330 insertions(+), 61 deletions(-) create mode 100644 src/components/ui/dropdown-menu.tsx diff --git a/package-lock.json b/package-lock.json index a43d823d..e33614f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-avatar": "1.1.11", "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-label": "2.1.8", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-radio-group": "1.3.8", @@ -1687,6 +1688,35 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", @@ -1791,6 +1821,64 @@ } } }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popover": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", diff --git a/package.json b/package.json index f987d50e..49701fd6 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-avatar": "1.1.11", "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-label": "2.1.8", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-radio-group": "1.3.8", diff --git a/src/components/orderbook/OrderbookContent.tsx b/src/components/orderbook/OrderbookContent.tsx index 1e905817..b73f7006 100644 --- a/src/components/orderbook/OrderbookContent.tsx +++ b/src/components/orderbook/OrderbookContent.tsx @@ -1,4 +1,5 @@ -import { useState, useMemo, useEffect, useRef } from 'react' +import { useState, useMemo } from 'react' +import { DropdownMenu } from '@radix-ui/react-dropdown-menu' import { useQuery } from '@tanstack/react-query' import type { i18n } from 'i18next' import { ChevronDownIcon, RefreshCwIcon, PlusIcon, AlertCircleIcon, Loader2Icon } from 'lucide-react' @@ -17,7 +18,6 @@ import { isAbsoluteOffer, isRelativeOffer, humanReadableDuration, - delayedPromise, pseudoRandomNumber, } from '@/lib/utils' import { jamSettingsStore } from '@/store/jamSettingsStore' @@ -25,6 +25,7 @@ import { jmSessionStore } from '@/store/jmSessionStore' import { DevBadge } from '../dev/DevBadge' import { Alert, AlertDescription } from '../ui/alert' import { ButtonGroup } from '../ui/button-group' +import { DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger } from '../ui/dropdown-menu' import { Label } from '../ui/label' import { OrderbookTable, type OrderTableEntry } from './OrderbookTable' @@ -98,14 +99,10 @@ export const OrderbookContent = ({ className }: OrderbookContentProps) => { const [isHighlightMyOffers, setHighlightMyOffers] = useState(false) const [isPinMyOffers, setPinMyOffers] = useState(false) - const [showRefreshDropdown, setShowRefreshDropdown] = useState(false) - const [demoOffers, setDemoOffers] = useState([]) const isDeveloperMode = useStore(jamSettingsStore, (state) => state.state.developerMode) const showDemoButton = useMemo(() => isDeveloperMode, [isDeveloperMode]) - const dropdownRef = useRef(null) - const __dev_generateDemoReportEntryButton = () => { const randomMinsize = pseudoRandomNumber(JM_DUST_THRESHOLD, JM_DUST_THRESHOLD + 100_000) const randomOrdertype = Math.random() > 0.5 ? 'sw0absoffer' : 'sw0reloffer' @@ -125,22 +122,6 @@ export const OrderbookContent = ({ className }: OrderbookContentProps) => { setDemoOffers((prev) => [...prev, randomOffer]) } - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setShowRefreshDropdown(false) - } - } - - if (showRefreshDropdown) { - document.addEventListener('mousedown', handleClickOutside) - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside) - } - }, [showRefreshDropdown]) - const { isFetching: isFetchingOrderbookRefresh, refetch: refetchOrderbookRefresh } = useQuery({ queryKey: ['orderbook-refresh'], queryFn: withQueryDelay(OrderbookApi.refreshOrderbook, { @@ -214,13 +195,11 @@ export const OrderbookContent = ({ className }: OrderbookContentProps) => { }, [tableEntriesFiltered]) const handleClearAndReload = async () => { - setShowRefreshDropdown(false) await refetchOrderbookRefresh().then(() => refetchOrderbookData()) } const handleReload = async () => { - setShowRefreshDropdown(false) - await refetchOrderbookData().then(() => delayedPromise(200)) + await refetchOrderbookData() } const highlightedOffers: OrderTableEntry[] = isHighlightMyOffers ? myOffers : [] @@ -261,51 +240,34 @@ export const OrderbookContent = ({ className }: OrderbookContentProps) => {
{showDemoButton && ( - )} - {/* TODO: replace manual dropdown with shadcn component */} -
-
- - - + + + + - - {showRefreshDropdown && ( -
- -
- )} -
-
+ + + + + ) { + return +} + +function DropdownMenuPortal({ ...props }: React.ComponentProps) { + return +} + +function DropdownMenuTrigger({ ...props }: React.ComponentProps) { + return +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ ...props }: React.ComponentProps) { + return +} + +function DropdownMenuItem({ + className, + inset, + variant = 'default', + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: 'default' | 'destructive' +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ ...props }: React.ComponentProps) { + return +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ className, ...props }: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) { + return ( + + ) +} + +function DropdownMenuSub({ ...props }: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +}