|
@@ -14,6 +14,7 @@ import {
|
|
|
Virtualizer,
|
|
Virtualizer,
|
|
|
} from "@pythnetwork/component-library/Virtualizer";
|
|
} from "@pythnetwork/component-library/Virtualizer";
|
|
|
import type { Button as UnstyledButton } from "@pythnetwork/component-library/unstyled/Button";
|
|
import type { Button as UnstyledButton } from "@pythnetwork/component-library/unstyled/Button";
|
|
|
|
|
+import type { ListBoxItemProps } from "@pythnetwork/component-library/unstyled/ListBox";
|
|
|
import {
|
|
import {
|
|
|
ListBox,
|
|
ListBox,
|
|
|
ListBoxItem,
|
|
ListBoxItem,
|
|
@@ -22,7 +23,7 @@ import { useDrawer } from "@pythnetwork/component-library/useDrawer";
|
|
|
import { useLogger } from "@pythnetwork/component-library/useLogger";
|
|
import { useLogger } from "@pythnetwork/component-library/useLogger";
|
|
|
import { matchSorter } from "match-sorter";
|
|
import { matchSorter } from "match-sorter";
|
|
|
import type { ReactNode } from "react";
|
|
import type { ReactNode } from "react";
|
|
|
-import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
|
|
|
|
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
|
|
|
|
import { Cluster, ClusterToName } from "../../services/pyth";
|
|
import { Cluster, ClusterToName } from "../../services/pyth";
|
|
|
import { AssetClassBadge } from "../AssetClassBadge";
|
|
import { AssetClassBadge } from "../AssetClassBadge";
|
|
@@ -132,16 +133,62 @@ const SearchDialogContents = ({
|
|
|
feeds,
|
|
feeds,
|
|
|
publishers,
|
|
publishers,
|
|
|
}: SearchDialogContentsProps) => {
|
|
}: SearchDialogContentsProps) => {
|
|
|
|
|
+ /** hooks */
|
|
|
const drawer = useDrawer();
|
|
const drawer = useDrawer();
|
|
|
const logger = useLogger();
|
|
const logger = useLogger();
|
|
|
|
|
+
|
|
|
|
|
+ /** refs */
|
|
|
|
|
+ const closeDrawerDebounceRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
|
|
|
|
+ const openTabModifierActiveRef = useRef(false);
|
|
|
|
|
+ const middleMousePressedRef = useRef(false);
|
|
|
|
|
+
|
|
|
|
|
+ /** state */
|
|
|
const [search, setSearch] = useState("");
|
|
const [search, setSearch] = useState("");
|
|
|
const [type, setType] = useState<ResultType | "">("");
|
|
const [type, setType] = useState<ResultType | "">("");
|
|
|
|
|
+
|
|
|
|
|
+ /** callbacks */
|
|
|
const closeDrawer = useCallback(() => {
|
|
const closeDrawer = useCallback(() => {
|
|
|
- drawer.close().catch((error: unknown) => {
|
|
|
|
|
- logger.error(error);
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ if (closeDrawerDebounceRef.current) {
|
|
|
|
|
+ clearTimeout(closeDrawerDebounceRef.current);
|
|
|
|
|
+ closeDrawerDebounceRef.current = undefined;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // we debounce the drawer closure because, if we don't,
|
|
|
|
|
+ // mobile browsers (at least on iOS) may squash the native <a />
|
|
|
|
|
+ // click, resulting in no price feed loading for the user
|
|
|
|
|
+ closeDrawerDebounceRef.current = setTimeout(() => {
|
|
|
|
|
+ drawer.close().catch((error: unknown) => {
|
|
|
|
|
+ logger.error(error);
|
|
|
|
|
+ });
|
|
|
|
|
+ }, 250);
|
|
|
}, [drawer, logger]);
|
|
}, [drawer, logger]);
|
|
|
|
|
+ const onLinkPointerDown = useCallback<
|
|
|
|
|
+ NonNullable<ListBoxItemProps<never>["onPointerDown"]>
|
|
|
|
|
+ >((e) => {
|
|
|
|
|
+ const { button, ctrlKey, metaKey } = e;
|
|
|
|
|
|
|
|
|
|
+ middleMousePressedRef.current = button === 1;
|
|
|
|
|
+
|
|
|
|
|
+ // on press is too abstracted and doesn't give us the native event
|
|
|
|
|
+ // for determining if the user clicked their middle mouse button,
|
|
|
|
|
+ // so we need to use the native onClick directly
|
|
|
|
|
+ middleMousePressedRef.current = button === 1;
|
|
|
|
|
+ openTabModifierActiveRef.current = metaKey || ctrlKey;
|
|
|
|
|
+ }, []);
|
|
|
|
|
+ const onLinkPointerUp = useCallback<
|
|
|
|
|
+ NonNullable<ListBoxItemProps<never>["onPointerUp"]>
|
|
|
|
|
+ >(() => {
|
|
|
|
|
+ const userWantsNewTab =
|
|
|
|
|
+ middleMousePressedRef.current || openTabModifierActiveRef.current;
|
|
|
|
|
+
|
|
|
|
|
+ // // they want a new tab, the search popover stays open
|
|
|
|
|
+ if (!userWantsNewTab) closeDrawer();
|
|
|
|
|
+
|
|
|
|
|
+ middleMousePressedRef.current = false;
|
|
|
|
|
+ openTabModifierActiveRef.current = false;
|
|
|
|
|
+ }, [closeDrawer]);
|
|
|
|
|
+
|
|
|
|
|
+ /** memos */
|
|
|
const results = useMemo(() => {
|
|
const results = useMemo(() => {
|
|
|
const filteredFeeds = matchSorter(feeds, search, {
|
|
const filteredFeeds = matchSorter(feeds, search, {
|
|
|
keys: ["displaySymbol", "symbol", "description", "priceAccount"],
|
|
keys: ["displaySymbol", "symbol", "description", "priceAccount"],
|
|
@@ -168,6 +215,7 @@ const SearchDialogContents = ({
|
|
|
}
|
|
}
|
|
|
return [...filteredFeeds, ...filteredPublishers];
|
|
return [...filteredFeeds, ...filteredPublishers];
|
|
|
}, [feeds, publishers, search, type]);
|
|
}, [feeds, publishers, search, type]);
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<div className={styles.searchDialogContents}>
|
|
<div className={styles.searchDialogContents}>
|
|
|
<div className={styles.searchBar}>
|
|
<div className={styles.searchBar}>
|
|
@@ -231,13 +279,14 @@ const SearchDialogContents = ({
|
|
|
: (result.name ?? result.publisherKey)
|
|
: (result.name ?? result.publisherKey)
|
|
|
}
|
|
}
|
|
|
className={styles.item ?? ""}
|
|
className={styles.item ?? ""}
|
|
|
- onAction={closeDrawer}
|
|
|
|
|
href={
|
|
href={
|
|
|
result.type === ResultType.PriceFeed
|
|
result.type === ResultType.PriceFeed
|
|
|
? `/price-feeds/${encodeURIComponent(result.symbol)}`
|
|
? `/price-feeds/${encodeURIComponent(result.symbol)}`
|
|
|
: `/publishers/${ClusterToName[result.cluster]}/${encodeURIComponent(result.publisherKey)}`
|
|
: `/publishers/${ClusterToName[result.cluster]}/${encodeURIComponent(result.publisherKey)}`
|
|
|
}
|
|
}
|
|
|
data-is-first={result.id === results[0]?.id ? "" : undefined}
|
|
data-is-first={result.id === results[0]?.id ? "" : undefined}
|
|
|
|
|
+ onPointerDown={onLinkPointerDown}
|
|
|
|
|
+ onPointerUp={onLinkPointerUp}
|
|
|
>
|
|
>
|
|
|
<div className={styles.smallScreen}>
|
|
<div className={styles.smallScreen}>
|
|
|
{result.type === ResultType.PriceFeed ? (
|
|
{result.type === ResultType.PriceFeed ? (
|