Skip to content

Commit

Permalink
Merge pull request #2387 from pyth-network/cprussin/ui-49-fix-safari-…
Browse files Browse the repository at this point in the history
…issues

fix(insights): fix some bugs relating to the search dialog
  • Loading branch information
cprussin authored Feb 18, 2025
2 parents 8731e69 + ec4cd2b commit d9508e3
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 101 deletions.
18 changes: 9 additions & 9 deletions apps/insights/src/components/PriceFeed/price-feed-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ export const PriceFeedSelect = ({ children }: Props) => {
const filteredFeeds = useMemo(
() =>
search === ""
? feeds.entries()
: feeds
.entries()
.filter(
([, { displaySymbol, assetClass, key }]) =>
filter.contains(displaySymbol, search) ||
filter.contains(assetClass, search) ||
filter.contains(key[Cluster.Pythnet], search),
),
? // This is inefficient but Safari doesn't support `Iterator.filter`, see
// https://bugs.webkit.org/show_bug.cgi?id=248650
[...feeds.entries()]
: [...feeds.entries()].filter(
([, { displaySymbol, assetClass, key }]) =>
filter.contains(displaySymbol, search) ||
filter.contains(assetClass, search) ||
filter.contains(key[Cluster.Pythnet], search),
),
[feeds, search, filter],
);
const sortedFeeds = useMemo(
Expand Down
2 changes: 1 addition & 1 deletion apps/insights/src/components/Root/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const getPublishersForSearchDialog = async (cluster: Cluster) => {
const knownPublisher = lookupPublisher(publisher.key);

return {
id: publisher.key,
publisherKey: publisher.key,
averageScore: publisher.averageScore,
cluster,
...(knownPublisher && {
Expand Down
221 changes: 130 additions & 91 deletions apps/insights/src/components/Root/search-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
ListBox,
ListBoxItem,
} from "@pythnetwork/component-library/unstyled/ListBox";
import { useRouter } from "next/navigation";
import {
type ReactNode,
useState,
Expand All @@ -23,7 +24,7 @@ import {
use,
useMemo,
} from "react";
import { useCollator, useFilter } from "react-aria";
import { RouterProvider, useCollator, useFilter } from "react-aria";

import styles from "./search-dialog.module.scss";
import { usePriceFeeds } from "../../hooks/use-price-feeds";
Expand All @@ -46,7 +47,7 @@ const SearchDialogOpenContext = createContext<
type Props = {
children: ReactNode;
publishers: ({
id: string;
publisherKey: string;
averageScore: number;
cluster: Cluster;
} & (
Expand All @@ -63,54 +64,85 @@ export const SearchDialogProvider = ({ children, publishers }: Props) => {
const filter = useFilter({ sensitivity: "base", usage: "search" });
const feeds = usePriceFeeds();

const close = useCallback(() => {
searchDialogState.close();
setTimeout(() => {
setSearch("");
setType("");
}, CLOSE_DURATION_IN_MS);
}, [searchDialogState, setSearch, setType]);
const close = useCallback(
() =>
new Promise<void>((resolve) => {
searchDialogState.close();
setTimeout(() => {
setSearch("");
setType("");
resolve();
}, CLOSE_DURATION_IN_MS);
}),
[searchDialogState, setSearch, setType],
);

const handleOpenChange = useCallback(
(isOpen: boolean) => {
if (!isOpen) {
close();
close().catch(() => {
/* no-op since this actually can't fail */
});
}
},
[close],
);

const router = useRouter();
const handleOpenItem = useCallback(
(href: string) => {
close()
.then(() => {
router.push(href);
})
.catch(() => {
/* no-op since this actually can't fail */
});
},
[close, router],
);

const results = useMemo(
() =>
[
...(type === ResultType.Publisher
? []
: feeds
.entries()
: // This is inefficient but Safari doesn't support `Iterator.filter`,
// see https://bugs.webkit.org/show_bug.cgi?id=248650
[...feeds.entries()]
.filter(([, { displaySymbol }]) =>
filter.contains(displaySymbol, search),
)
.map(([symbol, feed]) => ({
.map(([symbol, { assetClass, displaySymbol }]) => ({
type: ResultType.PriceFeed as const,
id: symbol,
...feed,
assetClass,
displaySymbol,
}))),
...(type === ResultType.PriceFeed
? []
: publishers
.filter(
(publisher) =>
filter.contains(publisher.id, search) ||
filter.contains(publisher.publisherKey, search) ||
(publisher.name && filter.contains(publisher.name, search)),
)
.map((publisher) => ({
type: ResultType.Publisher as const,
id: [
ClusterToName[publisher.cluster],
publisher.publisherKey,
].join(":"),
...publisher,
}))),
].sort((a, b) =>
collator.compare(
a.type === ResultType.PriceFeed ? a.displaySymbol : (a.name ?? a.id),
b.type === ResultType.PriceFeed ? b.displaySymbol : (b.name ?? b.id),
a.type === ResultType.PriceFeed
? a.displaySymbol
: (a.name ?? a.publisherKey),
b.type === ResultType.PriceFeed
? b.displaySymbol
: (b.name ?? b.publisherKey),
),
),
[feeds, publishers, collator, filter, search, type],
Expand Down Expand Up @@ -182,80 +214,87 @@ export const SearchDialogProvider = ({ children, publishers }: Props) => {
</Button>
</div>
<div className={styles.body}>
<Virtualizer layout={new ListLayout()}>
<ListBox
aria-label="Search"
items={results}
className={styles.listbox ?? ""}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={false}
// @ts-expect-error looks like react-aria isn't exposing this
// property in the typescript types correctly...
shouldFocusOnHover
onAction={close}
emptyState={
<NoResults
query={search}
onClearSearch={() => {
setSearch("");
}}
/>
}
>
{(result) => (
<ListBoxItem
textValue={
result.type === ResultType.PriceFeed
? result.displaySymbol
: (result.name ?? result.id)
}
className={styles.item ?? ""}
href={`${result.type === ResultType.PriceFeed ? "/price-feeds" : `/publishers/${ClusterToName[result.cluster]}`}/${encodeURIComponent(result.id)}`}
data-is-first={result.id === results[0]?.id ? "" : undefined}
>
<div className={styles.itemType}>
<Badge
variant={
result.type === ResultType.PriceFeed
? "warning"
: "info"
}
style="filled"
size="xs"
>
{result.type === ResultType.PriceFeed
? "PRICE FEED"
: "PUBLISHER"}
</Badge>
</div>
{result.type === ResultType.PriceFeed ? (
<>
<PriceFeedTag
compact
symbol={result.id}
className={styles.itemTag}
/>
<AssetClassTag symbol={result.id} />
</>
) : (
<>
<PublisherTag
className={styles.itemTag}
compact
cluster={result.cluster}
publisherKey={result.id}
{...(result.name && {
name: result.name,
icon: result.icon,
})}
/>
<Score score={result.averageScore} />
</>
)}
</ListBoxItem>
)}
</ListBox>
</Virtualizer>
<RouterProvider navigate={handleOpenItem}>
<Virtualizer layout={new ListLayout()}>
<ListBox
aria-label="Search"
items={results}
className={styles.listbox ?? ""}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={false}
// @ts-expect-error looks like react-aria isn't exposing this
// property in the typescript types correctly...
shouldFocusOnHover
emptyState={
<NoResults
query={search}
onClearSearch={() => {
setSearch("");
}}
/>
}
>
{(result) => (
<ListBoxItem
textValue={
result.type === ResultType.PriceFeed
? result.displaySymbol
: (result.name ?? result.publisherKey)
}
className={styles.item ?? ""}
href={
result.type === ResultType.PriceFeed
? `/price-feeds/${encodeURIComponent(result.id)}`
: `/publishers/${ClusterToName[result.cluster]}/${encodeURIComponent(result.publisherKey)}`
}
data-is-first={
result.id === results[0]?.id ? "" : undefined
}
>
<div className={styles.itemType}>
<Badge
variant={
result.type === ResultType.PriceFeed
? "warning"
: "info"
}
style="filled"
size="xs"
>
{result.type === ResultType.PriceFeed
? "PRICE FEED"
: "PUBLISHER"}
</Badge>
</div>
{result.type === ResultType.PriceFeed ? (
<>
<PriceFeedTag
compact
symbol={result.id}
className={styles.itemTag}
/>
<AssetClassTag symbol={result.id} />
</>
) : (
<>
<PublisherTag
className={styles.itemTag}
compact
cluster={result.cluster}
publisherKey={result.publisherKey}
{...(result.name && {
name: result.name,
icon: result.icon,
})}
/>
<Score score={result.averageScore} />
</>
)}
</ListBoxItem>
)}
</ListBox>
</Virtualizer>
</RouterProvider>
</div>
</ModalDialog>
</>
Expand Down

0 comments on commit d9508e3

Please sign in to comment.