@@ -14,6 +14,7 @@ import {
1414 ListBox ,
1515 ListBoxItem ,
1616} from "@pythnetwork/component-library/unstyled/ListBox" ;
17+ import { useRouter } from "next/navigation" ;
1718import {
1819 type ReactNode ,
1920 useState ,
@@ -23,7 +24,7 @@ import {
2324 use ,
2425 useMemo ,
2526} from "react" ;
26- import { useCollator , useFilter } from "react-aria" ;
27+ import { RouterProvider , useCollator , useFilter } from "react-aria" ;
2728
2829import styles from "./search-dialog.module.scss" ;
2930import { usePriceFeeds } from "../../hooks/use-price-feeds" ;
@@ -46,7 +47,7 @@ const SearchDialogOpenContext = createContext<
4647type Props = {
4748 children : ReactNode ;
4849 publishers : ( {
49- id : string ;
50+ publisherKey : string ;
5051 averageScore : number ;
5152 cluster : Cluster ;
5253 } & (
@@ -63,54 +64,85 @@ export const SearchDialogProvider = ({ children, publishers }: Props) => {
6364 const filter = useFilter ( { sensitivity : "base" , usage : "search" } ) ;
6465 const feeds = usePriceFeeds ( ) ;
6566
66- const close = useCallback ( ( ) => {
67- searchDialogState . close ( ) ;
68- setTimeout ( ( ) => {
69- setSearch ( "" ) ;
70- setType ( "" ) ;
71- } , CLOSE_DURATION_IN_MS ) ;
72- } , [ searchDialogState , setSearch , setType ] ) ;
67+ const close = useCallback (
68+ ( ) =>
69+ new Promise < void > ( ( resolve ) => {
70+ searchDialogState . close ( ) ;
71+ setTimeout ( ( ) => {
72+ setSearch ( "" ) ;
73+ setType ( "" ) ;
74+ resolve ( ) ;
75+ } , CLOSE_DURATION_IN_MS ) ;
76+ } ) ,
77+ [ searchDialogState , setSearch , setType ] ,
78+ ) ;
7379
7480 const handleOpenChange = useCallback (
7581 ( isOpen : boolean ) => {
7682 if ( ! isOpen ) {
77- close ( ) ;
83+ close ( ) . catch ( ( ) => {
84+ /* no-op since this actually can't fail */
85+ } ) ;
7886 }
7987 } ,
8088 [ close ] ,
8189 ) ;
8290
91+ const router = useRouter ( ) ;
92+ const handleOpenItem = useCallback (
93+ ( href : string ) => {
94+ close ( )
95+ . then ( ( ) => {
96+ router . push ( href ) ;
97+ } )
98+ . catch ( ( ) => {
99+ /* no-op since this actually can't fail */
100+ } ) ;
101+ } ,
102+ [ close , router ] ,
103+ ) ;
104+
83105 const results = useMemo (
84106 ( ) =>
85107 [
86108 ...( type === ResultType . Publisher
87109 ? [ ]
88- : feeds
89- . entries ( )
110+ : // This is inefficient but Safari doesn't support `Iterator.filter`,
111+ // see https://bugs.webkit.org/show_bug.cgi?id=248650
112+ [ ...feeds . entries ( ) ]
90113 . filter ( ( [ , { displaySymbol } ] ) =>
91114 filter . contains ( displaySymbol , search ) ,
92115 )
93- . map ( ( [ symbol , feed ] ) => ( {
116+ . map ( ( [ symbol , { assetClass , displaySymbol } ] ) => ( {
94117 type : ResultType . PriceFeed as const ,
95118 id : symbol ,
96- ...feed ,
119+ assetClass,
120+ displaySymbol,
97121 } ) ) ) ,
98122 ...( type === ResultType . PriceFeed
99123 ? [ ]
100124 : publishers
101125 . filter (
102126 ( publisher ) =>
103- filter . contains ( publisher . id , search ) ||
127+ filter . contains ( publisher . publisherKey , search ) ||
104128 ( publisher . name && filter . contains ( publisher . name , search ) ) ,
105129 )
106130 . map ( ( publisher ) => ( {
107131 type : ResultType . Publisher as const ,
132+ id : [
133+ ClusterToName [ publisher . cluster ] ,
134+ publisher . publisherKey ,
135+ ] . join ( ":" ) ,
108136 ...publisher ,
109137 } ) ) ) ,
110138 ] . sort ( ( a , b ) =>
111139 collator . compare (
112- a . type === ResultType . PriceFeed ? a . displaySymbol : ( a . name ?? a . id ) ,
113- b . type === ResultType . PriceFeed ? b . displaySymbol : ( b . name ?? b . id ) ,
140+ a . type === ResultType . PriceFeed
141+ ? a . displaySymbol
142+ : ( a . name ?? a . publisherKey ) ,
143+ b . type === ResultType . PriceFeed
144+ ? b . displaySymbol
145+ : ( b . name ?? b . publisherKey ) ,
114146 ) ,
115147 ) ,
116148 [ feeds , publishers , collator , filter , search , type ] ,
@@ -182,80 +214,87 @@ export const SearchDialogProvider = ({ children, publishers }: Props) => {
182214 </ Button >
183215 </ div >
184216 < div className = { styles . body } >
185- < Virtualizer layout = { new ListLayout ( ) } >
186- < ListBox
187- aria-label = "Search"
188- items = { results }
189- className = { styles . listbox ?? "" }
190- // eslint-disable-next-line jsx-a11y/no-autofocus
191- autoFocus = { false }
192- // @ts -expect-error looks like react-aria isn't exposing this
193- // property in the typescript types correctly...
194- shouldFocusOnHover
195- onAction = { close }
196- emptyState = {
197- < NoResults
198- query = { search }
199- onClearSearch = { ( ) => {
200- setSearch ( "" ) ;
201- } }
202- />
203- }
204- >
205- { ( result ) => (
206- < ListBoxItem
207- textValue = {
208- result . type === ResultType . PriceFeed
209- ? result . displaySymbol
210- : ( result . name ?? result . id )
211- }
212- className = { styles . item ?? "" }
213- href = { `${ result . type === ResultType . PriceFeed ? "/price-feeds" : `/publishers/${ ClusterToName [ result . cluster ] } ` } /${ encodeURIComponent ( result . id ) } ` }
214- data-is-first = { result . id === results [ 0 ] ?. id ? "" : undefined }
215- >
216- < div className = { styles . itemType } >
217- < Badge
218- variant = {
219- result . type === ResultType . PriceFeed
220- ? "warning"
221- : "info"
222- }
223- style = "filled"
224- size = "xs"
225- >
226- { result . type === ResultType . PriceFeed
227- ? "PRICE FEED"
228- : "PUBLISHER" }
229- </ Badge >
230- </ div >
231- { result . type === ResultType . PriceFeed ? (
232- < >
233- < PriceFeedTag
234- compact
235- symbol = { result . id }
236- className = { styles . itemTag }
237- />
238- < AssetClassTag symbol = { result . id } />
239- </ >
240- ) : (
241- < >
242- < PublisherTag
243- className = { styles . itemTag }
244- compact
245- cluster = { result . cluster }
246- publisherKey = { result . id }
247- { ...( result . name && {
248- name : result . name ,
249- icon : result . icon ,
250- } ) }
251- />
252- < Score score = { result . averageScore } />
253- </ >
254- ) }
255- </ ListBoxItem >
256- ) }
257- </ ListBox >
258- </ Virtualizer >
217+ < RouterProvider navigate = { handleOpenItem } >
218+ < Virtualizer layout = { new ListLayout ( ) } >
219+ < ListBox
220+ aria-label = "Search"
221+ items = { results }
222+ className = { styles . listbox ?? "" }
223+ // eslint-disable-next-line jsx-a11y/no-autofocus
224+ autoFocus = { false }
225+ // @ts -expect-error looks like react-aria isn't exposing this
226+ // property in the typescript types correctly...
227+ shouldFocusOnHover
228+ emptyState = {
229+ < NoResults
230+ query = { search }
231+ onClearSearch = { ( ) => {
232+ setSearch ( "" ) ;
233+ } }
234+ />
235+ }
236+ >
237+ { ( result ) => (
238+ < ListBoxItem
239+ textValue = {
240+ result . type === ResultType . PriceFeed
241+ ? result . displaySymbol
242+ : ( result . name ?? result . publisherKey )
243+ }
244+ className = { styles . item ?? "" }
245+ href = {
246+ result . type === ResultType . PriceFeed
247+ ? `/price-feeds/${ encodeURIComponent ( result . id ) } `
248+ : `/publishers/${ ClusterToName [ result . cluster ] } /${ encodeURIComponent ( result . publisherKey ) } `
249+ }
250+ data-is-first = {
251+ result . id === results [ 0 ] ?. id ? "" : undefined
252+ }
253+ >
254+ < div className = { styles . itemType } >
255+ < Badge
256+ variant = {
257+ result . type === ResultType . PriceFeed
258+ ? "warning"
259+ : "info"
260+ }
261+ style = "filled"
262+ size = "xs"
263+ >
264+ { result . type === ResultType . PriceFeed
265+ ? "PRICE FEED"
266+ : "PUBLISHER" }
267+ </ Badge >
268+ </ div >
269+ { result . type === ResultType . PriceFeed ? (
270+ < >
271+ < PriceFeedTag
272+ compact
273+ symbol = { result . id }
274+ className = { styles . itemTag }
275+ />
276+ < AssetClassTag symbol = { result . id } />
277+ </ >
278+ ) : (
279+ < >
280+ < PublisherTag
281+ className = { styles . itemTag }
282+ compact
283+ cluster = { result . cluster }
284+ publisherKey = { result . publisherKey }
285+ { ...( result . name && {
286+ name : result . name ,
287+ icon : result . icon ,
288+ } ) }
289+ />
290+ < Score score = { result . averageScore } />
291+ </ >
292+ ) }
293+ </ ListBoxItem >
294+ ) }
295+ </ ListBox >
296+ </ Virtualizer >
297+ </ RouterProvider >
259298 </ div >
260299 </ ModalDialog >
261300 </ >
0 commit comments