@@ -14,6 +14,7 @@ import {
1414 Virtualizer ,
1515} from "@pythnetwork/component-library/Virtualizer" ;
1616import type { Button as UnstyledButton } from "@pythnetwork/component-library/unstyled/Button" ;
17+ import type { ListBoxItemProps } from "@pythnetwork/component-library/unstyled/ListBox" ;
1718import {
1819 ListBox ,
1920 ListBoxItem ,
@@ -22,7 +23,7 @@ import { useDrawer } from "@pythnetwork/component-library/useDrawer";
2223import { useLogger } from "@pythnetwork/component-library/useLogger" ;
2324import { matchSorter } from "match-sorter" ;
2425import type { ReactNode } from "react" ;
25- import { useCallback , useEffect , useMemo , useState } from "react" ;
26+ import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
2627
2728import { Cluster , ClusterToName } from "../../services/pyth" ;
2829import { AssetClassBadge } from "../AssetClassBadge" ;
@@ -132,16 +133,62 @@ const SearchDialogContents = ({
132133 feeds,
133134 publishers,
134135} : SearchDialogContentsProps ) => {
136+ /** hooks */
135137 const drawer = useDrawer ( ) ;
136138 const logger = useLogger ( ) ;
139+
140+ /** refs */
141+ const closeDrawerDebounceRef = useRef < NodeJS . Timeout | undefined > ( undefined ) ;
142+ const openTabModifierActiveRef = useRef ( false ) ;
143+ const middleMousePressedRef = useRef ( false ) ;
144+
145+ /** state */
137146 const [ search , setSearch ] = useState ( "" ) ;
138147 const [ type , setType ] = useState < ResultType | "" > ( "" ) ;
148+
149+ /** callbacks */
139150 const closeDrawer = useCallback ( ( ) => {
140- drawer . close ( ) . catch ( ( error : unknown ) => {
141- logger . error ( error ) ;
142- } ) ;
151+ if ( closeDrawerDebounceRef . current ) {
152+ clearTimeout ( closeDrawerDebounceRef . current ) ;
153+ closeDrawerDebounceRef . current = undefined ;
154+ }
155+
156+ // we debounce the drawer closure because, if we don't,
157+ // mobile browsers (at least on iOS) may squash the native <a />
158+ // click, resulting in no price feed loading for the user
159+ closeDrawerDebounceRef . current = setTimeout ( ( ) => {
160+ drawer . close ( ) . catch ( ( error : unknown ) => {
161+ logger . error ( error ) ;
162+ } ) ;
163+ } , 250 ) ;
143164 } , [ drawer , logger ] ) ;
165+ const onLinkPointerDown = useCallback <
166+ NonNullable < ListBoxItemProps < never > [ "onPointerDown" ] >
167+ > ( ( e ) => {
168+ const { button, ctrlKey, metaKey } = e ;
144169
170+ middleMousePressedRef . current = button === 1 ;
171+
172+ // on press is too abstracted and doesn't give us the native event
173+ // for determining if the user clicked their middle mouse button,
174+ // so we need to use the native onClick directly
175+ middleMousePressedRef . current = button === 1 ;
176+ openTabModifierActiveRef . current = metaKey || ctrlKey ;
177+ } , [ ] ) ;
178+ const onLinkPointerUp = useCallback <
179+ NonNullable < ListBoxItemProps < never > [ "onPointerUp" ] >
180+ > ( ( ) => {
181+ const userWantsNewTab =
182+ middleMousePressedRef . current || openTabModifierActiveRef . current ;
183+
184+ // // they want a new tab, the search popover stays open
185+ if ( ! userWantsNewTab ) closeDrawer ( ) ;
186+
187+ middleMousePressedRef . current = false ;
188+ openTabModifierActiveRef . current = false ;
189+ } , [ closeDrawer ] ) ;
190+
191+ /** memos */
145192 const results = useMemo ( ( ) => {
146193 const filteredFeeds = matchSorter ( feeds , search , {
147194 keys : [ "displaySymbol" , "symbol" , "description" , "priceAccount" ] ,
@@ -168,6 +215,7 @@ const SearchDialogContents = ({
168215 }
169216 return [ ...filteredFeeds , ...filteredPublishers ] ;
170217 } , [ feeds , publishers , search , type ] ) ;
218+
171219 return (
172220 < div className = { styles . searchDialogContents } >
173221 < div className = { styles . searchBar } >
@@ -231,13 +279,14 @@ const SearchDialogContents = ({
231279 : ( result . name ?? result . publisherKey )
232280 }
233281 className = { styles . item ?? "" }
234- onAction = { closeDrawer }
235282 href = {
236283 result . type === ResultType . PriceFeed
237284 ? `/price-feeds/${ encodeURIComponent ( result . symbol ) } `
238285 : `/publishers/${ ClusterToName [ result . cluster ] } /${ encodeURIComponent ( result . publisherKey ) } `
239286 }
240287 data-is-first = { result . id === results [ 0 ] ?. id ? "" : undefined }
288+ onPointerDown = { onLinkPointerDown }
289+ onPointerUp = { onLinkPointerUp }
241290 >
242291 < div className = { styles . smallScreen } >
243292 { result . type === ResultType . PriceFeed ? (
0 commit comments