ソースを参照

Merge pull request #3187 from pyth-network/bduran/UI-324-insights-hub-search-popover

feat(insights-hub): made dialog stick around if new tab is opening
Ben Duran 1 週間 前
コミット
9f595ae7b6

+ 54 - 5
apps/insights/src/components/Root/search-button.tsx

@@ -14,6 +14,7 @@ import {
   Virtualizer,
 } from "@pythnetwork/component-library/Virtualizer";
 import type { Button as UnstyledButton } from "@pythnetwork/component-library/unstyled/Button";
+import type { ListBoxItemProps } from "@pythnetwork/component-library/unstyled/ListBox";
 import {
   ListBox,
   ListBoxItem,
@@ -22,7 +23,7 @@ import { useDrawer } from "@pythnetwork/component-library/useDrawer";
 import { useLogger } from "@pythnetwork/component-library/useLogger";
 import { matchSorter } from "match-sorter";
 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 { AssetClassBadge } from "../AssetClassBadge";
@@ -132,16 +133,62 @@ const SearchDialogContents = ({
   feeds,
   publishers,
 }: SearchDialogContentsProps) => {
+  /** hooks */
   const drawer = useDrawer();
   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 [type, setType] = useState<ResultType | "">("");
+
+  /** callbacks */
   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]);
+  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 filteredFeeds = matchSorter(feeds, search, {
       keys: ["displaySymbol", "symbol", "description", "priceAccount"],
@@ -168,6 +215,7 @@ const SearchDialogContents = ({
     }
     return [...filteredFeeds, ...filteredPublishers];
   }, [feeds, publishers, search, type]);
+
   return (
     <div className={styles.searchDialogContents}>
       <div className={styles.searchBar}>
@@ -231,13 +279,14 @@ const SearchDialogContents = ({
                     : (result.name ?? result.publisherKey)
                 }
                 className={styles.item ?? ""}
-                onAction={closeDrawer}
                 href={
                   result.type === ResultType.PriceFeed
                     ? `/price-feeds/${encodeURIComponent(result.symbol)}`
                     : `/publishers/${ClusterToName[result.cluster]}/${encodeURIComponent(result.publisherKey)}`
                 }
                 data-is-first={result.id === results[0]?.id ? "" : undefined}
+                onPointerDown={onLinkPointerDown}
+                onPointerUp={onLinkPointerUp}
               >
                 <div className={styles.smallScreen}>
                   {result.type === ResultType.PriceFeed ? (

+ 1 - 1
packages/component-library/src/unstyled/ListBox/index.tsx

@@ -7,7 +7,7 @@ import { usePrefetch } from "../../use-prefetch.js";
 
 export { ListBox, ListBoxSection } from "react-aria-components";
 
-type ListBoxItemProps<T extends object> = ComponentProps<
+export type ListBoxItemProps<T extends object> = ComponentProps<
   typeof BaseListBoxItem<T>
 > & {
   prefetch?: Parameters<typeof usePrefetch>[0]["prefetch"];

+ 3 - 0
packages/react-hooks/jest.config.js

@@ -0,0 +1,3 @@
+import { defineJestConfigForNextJs } from "@pythnetwork/jest-config/define-next-config";
+
+export default defineJestConfigForNextJs();

+ 1 - 0
packages/react-hooks/package.json

@@ -14,6 +14,7 @@
   },
   "devDependencies": {
     "@cprussin/eslint-config": "catalog:",
+    "@pythnetwork/jest-config": "workspace:",
     "@cprussin/tsconfig": "catalog:",
     "@types/react": "catalog:",
     "@types/react-dom": "catalog:",

+ 3 - 0
packages/react-hooks/tsconfig.json

@@ -1,4 +1,7 @@
 {
   "extends": "@cprussin/tsconfig/base.json",
+  "compilerOptions": {
+    "lib": ["DOM", "ESNext"]
+  },
   "exclude": ["node_modules", "dist"]
 }

+ 3 - 0
pnpm-lock.yaml

@@ -2405,6 +2405,9 @@ importers:
       '@cprussin/tsconfig':
         specifier: 'catalog:'
         version: 3.1.2(typescript@5.9.3)
+      '@pythnetwork/jest-config':
+        specifier: 'workspace:'
+        version: link:../jest-config
       '@types/react':
         specifier: 'catalog:'
         version: 19.1.0