Skip to content

Commit ec4cd2b

Browse files
committed
fix(insights): fix some bugs relating to the search dialog
This PR fixes a few related bugs: - Safari doesn't support `Iterator.filter`, so we need to iterators that need to be filtered into arrays instead. - The search dialog needs to be closed before triggering navigation or the close animation is janky - The react-aria ids for publishers in the search dialog need to be disambiguated between clusters because a few publishers use the same keys in pythtest and pythnet
1 parent 8731e69 commit ec4cd2b

File tree

3 files changed

+140
-101
lines changed

3 files changed

+140
-101
lines changed

apps/insights/src/components/PriceFeed/price-feed-select.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ export const PriceFeedSelect = ({ children }: Props) => {
3636
const filteredFeeds = useMemo(
3737
() =>
3838
search === ""
39-
? feeds.entries()
40-
: feeds
41-
.entries()
42-
.filter(
43-
([, { displaySymbol, assetClass, key }]) =>
44-
filter.contains(displaySymbol, search) ||
45-
filter.contains(assetClass, search) ||
46-
filter.contains(key[Cluster.Pythnet], search),
47-
),
39+
? // This is inefficient but Safari doesn't support `Iterator.filter`, see
40+
// https://bugs.webkit.org/show_bug.cgi?id=248650
41+
[...feeds.entries()]
42+
: [...feeds.entries()].filter(
43+
([, { displaySymbol, assetClass, key }]) =>
44+
filter.contains(displaySymbol, search) ||
45+
filter.contains(assetClass, search) ||
46+
filter.contains(key[Cluster.Pythnet], search),
47+
),
4848
[feeds, search, filter],
4949
);
5050
const sortedFeeds = useMemo(

apps/insights/src/components/Root/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ const getPublishersForSearchDialog = async (cluster: Cluster) => {
5959
const knownPublisher = lookupPublisher(publisher.key);
6060

6161
return {
62-
id: publisher.key,
62+
publisherKey: publisher.key,
6363
averageScore: publisher.averageScore,
6464
cluster,
6565
...(knownPublisher && {

apps/insights/src/components/Root/search-dialog.tsx

Lines changed: 130 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
ListBox,
1515
ListBoxItem,
1616
} from "@pythnetwork/component-library/unstyled/ListBox";
17+
import { useRouter } from "next/navigation";
1718
import {
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

2829
import styles from "./search-dialog.module.scss";
2930
import { usePriceFeeds } from "../../hooks/use-price-feeds";
@@ -46,7 +47,7 @@ const SearchDialogOpenContext = createContext<
4647
type 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

Comments
 (0)