@@ -14,6 +14,7 @@ import {
14
14
ListBox ,
15
15
ListBoxItem ,
16
16
} from "@pythnetwork/component-library/unstyled/ListBox" ;
17
+ import { useRouter } from "next/navigation" ;
17
18
import {
18
19
type ReactNode ,
19
20
useState ,
@@ -23,7 +24,7 @@ import {
23
24
use ,
24
25
useMemo ,
25
26
} from "react" ;
26
- import { useCollator , useFilter } from "react-aria" ;
27
+ import { RouterProvider , useCollator , useFilter } from "react-aria" ;
27
28
28
29
import styles from "./search-dialog.module.scss" ;
29
30
import { usePriceFeeds } from "../../hooks/use-price-feeds" ;
@@ -46,7 +47,7 @@ const SearchDialogOpenContext = createContext<
46
47
type Props = {
47
48
children : ReactNode ;
48
49
publishers : ( {
49
- id : string ;
50
+ publisherKey : string ;
50
51
averageScore : number ;
51
52
cluster : Cluster ;
52
53
} & (
@@ -63,54 +64,85 @@ export const SearchDialogProvider = ({ children, publishers }: Props) => {
63
64
const filter = useFilter ( { sensitivity : "base" , usage : "search" } ) ;
64
65
const feeds = usePriceFeeds ( ) ;
65
66
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
+ ) ;
73
79
74
80
const handleOpenChange = useCallback (
75
81
( isOpen : boolean ) => {
76
82
if ( ! isOpen ) {
77
- close ( ) ;
83
+ close ( ) . catch ( ( ) => {
84
+ /* no-op since this actually can't fail */
85
+ } ) ;
78
86
}
79
87
} ,
80
88
[ close ] ,
81
89
) ;
82
90
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
+
83
105
const results = useMemo (
84
106
( ) =>
85
107
[
86
108
...( type === ResultType . Publisher
87
109
? [ ]
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 ( ) ]
90
113
. filter ( ( [ , { displaySymbol } ] ) =>
91
114
filter . contains ( displaySymbol , search ) ,
92
115
)
93
- . map ( ( [ symbol , feed ] ) => ( {
116
+ . map ( ( [ symbol , { assetClass , displaySymbol } ] ) => ( {
94
117
type : ResultType . PriceFeed as const ,
95
118
id : symbol ,
96
- ...feed ,
119
+ assetClass,
120
+ displaySymbol,
97
121
} ) ) ) ,
98
122
...( type === ResultType . PriceFeed
99
123
? [ ]
100
124
: publishers
101
125
. filter (
102
126
( publisher ) =>
103
- filter . contains ( publisher . id , search ) ||
127
+ filter . contains ( publisher . publisherKey , search ) ||
104
128
( publisher . name && filter . contains ( publisher . name , search ) ) ,
105
129
)
106
130
. map ( ( publisher ) => ( {
107
131
type : ResultType . Publisher as const ,
132
+ id : [
133
+ ClusterToName [ publisher . cluster ] ,
134
+ publisher . publisherKey ,
135
+ ] . join ( ":" ) ,
108
136
...publisher ,
109
137
} ) ) ) ,
110
138
] . sort ( ( a , b ) =>
111
139
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 ) ,
114
146
) ,
115
147
) ,
116
148
[ feeds , publishers , collator , filter , search , type ] ,
@@ -182,80 +214,87 @@ export const SearchDialogProvider = ({ children, publishers }: Props) => {
182
214
</ Button >
183
215
</ div >
184
216
< 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 >
259
298
</ div >
260
299
</ ModalDialog >
261
300
</ >
0 commit comments