diff --git a/packages/app-explorer/src/systems/Core/components/Search/SearchInput/SearchInput.tsx b/packages/app-explorer/src/systems/Core/components/Search/SearchInput/SearchInput.tsx index e20008d7..8737ad3a 100644 --- a/packages/app-explorer/src/systems/Core/components/Search/SearchInput/SearchInput.tsx +++ b/packages/app-explorer/src/systems/Core/components/Search/SearchInput/SearchInput.tsx @@ -2,320 +2,20 @@ import type { GQLSearchResult, Maybe } from '@fuel-explorer/graphql'; import type { BaseProps, InputProps } from '@fuels/ui'; -import { - Box, - Dropdown, - Focus, - Icon, - IconButton, - Input, - Link, - Portal, - Text, - Tooltip, - VStack, - shortAddress, - useBreakpoints, -} from '@fuels/ui'; +import { Focus, Icon, IconButton, Input, Tooltip, VStack } from '@fuels/ui'; import { IconCheck, IconSearch, IconX } from '@tabler/icons-react'; -import NextLink from 'next/link'; -import type { KeyboardEvent, RefObject } from 'react'; -import { forwardRef, useContext, useEffect, useRef, useState } from 'react'; +import type { KeyboardEvent } from 'react'; +import { useContext, useRef, useState } from 'react'; import { useFormStatus } from 'react-dom'; -import { Routes } from '~/routes'; -import { cx } from '../../utils/cx'; +import { cx } from '../../../utils/cx'; -import { useRouter } from 'next/navigation'; -import { SearchContext } from './SearchWidget'; +import { SearchResultDropdown } from '../SearchResultDropdown'; +import { SearchContext } from '../SearchWidget'; +import { usePropagateInputMouseClick } from '../hooks/usePropagateInputMouseClick'; +import { DEFAULT_SEARCH_INPUT_WIDTH } from './constants'; import { styles } from './styles'; -const DEFAULT_WIDTH = 400; - -type SearchDropdownProps = { - searchResult?: Maybe; - openDropdown: boolean; - isFocused: boolean; - onOpenChange: (open: boolean) => void; - searchValue: string; - width: number; - onSelectItem: () => void; -}; - -// Radix's Dropdown component uses a Portal to render the dropdown content, -// That causes Input to not capture click events, forcing the user to double click the input when the dropdown is open. -// To fix that we need to render a separate Portal to render the overlay and capture the click to make it work. -function InputDropdownOverlay({ - containerRef, - inputRef, -}: { - containerRef: RefObject; - inputRef: RefObject; -}) { - const boundingData = containerRef.current?.getBoundingClientRect(); - - useEffect(() => { - const onClick = (e: MouseEvent) => { - if (boundingData) { - const { clientX, clientY } = e; - if ( - clientX >= boundingData.x && - clientX <= boundingData.x + boundingData.width && - clientY >= boundingData.y && - clientY <= boundingData.y + boundingData.height - ) { - inputRef.current?.focus(); - } - } - }; - document.addEventListener('click', onClick); - - return () => { - setTimeout(() => { - document.removeEventListener('click', onClick); - }, 500); - }; - }, []); - - if (!boundingData) { - return null; - } - const { x, y, width, height } = boundingData; - - return ( - -
- - ); -} - -const SearchResultDropdown = forwardRef( - ( - { - searchResult, - searchValue, - openDropdown, - onOpenChange, - width, - onSelectItem, - isFocused, - }, - ref, - ) => { - const router = useRouter(); - - function onClick(href: string | undefined) { - onSelectItem?.(); - if (href) { - router.push(href); - } - } - const classes = styles(); - const { isMobile } = useBreakpoints(); - const trimL = isMobile ? 15 : 20; - const trimR = isMobile ? 13 : 18; - - const hasResult = - searchResult?.account || - searchResult?.block || - searchResult?.contract || - searchResult?.transaction; - - return ( - - - - - - {!searchResult && ( - <> - - {`"${shortAddress( - searchValue, - trimL, - trimR, - )}" is not a valid address.`} - - - )} - {hasResult ? ( - <> - {searchResult?.account && ( - <> - Account - - searchResult.account?.address && - onClick( - Routes.accountAssets(searchResult.account.address!), - ) - } - > - - {shortAddress( - searchResult.account.address || '', - trimL, - trimR, - )} - - - - Recent Transactions - {searchResult.account.transactions?.map((transaction) => { - return ( - - transaction?.id && - onClick(Routes.txSimple(transaction?.id)) - } - > - - {shortAddress(transaction?.id || '', trimL, trimR)} - - - ); - })} - - )} - {searchResult?.block && ( - <> - {searchResult.block.id === searchValue && ( - <> - Block Hash - - searchResult.block?.id && - onClick(`/block/${searchResult.block.id}/simple`) - } - > - - {shortAddress( - searchResult.block.id || '', - trimL, - trimR, - )} - - - - )} - {searchResult.block.height === searchValue && ( - <> - Block Height - - searchResult.block?.height && - onClick(`/block/${searchResult.block?.height}/simple`) - } - > - - {searchResult.block.height} - - - - )} - - )} - {searchResult?.contract && ( - <> - Contract - - searchResult.contract?.id && - onClick(Routes.contractAssets(searchResult.contract.id)) - } - > - - {shortAddress( - searchResult.contract.id || '', - trimL, - trimR, - )} - - - - )} - {searchResult?.transaction && ( - <> - Transaction - - searchResult.transaction?.id && - onClick(Routes.txSimple(searchResult.transaction?.id)) - } - > - - {shortAddress( - searchResult.transaction.id || '', - trimL, - trimR, - )} - - - - )} - - ) : ( - <> - No instances found for: - - "{shortAddress(searchValue, trimL, trimR)}" - - - )} - - - ); - }, -); - type SearchInputProps = BaseProps & { onSubmit?: (value: string) => void; searchResult?: Maybe; @@ -343,6 +43,12 @@ export function SearchInput({ ? !pending : isOpen && !pending && !!searchResult; + usePropagateInputMouseClick({ + containerRef, + inputRef, + enabled: openDropdown, + }); + function handleChange(event: React.ChangeEvent) { setValue(event.target.value); } @@ -406,12 +112,6 @@ export function SearchInput({ onBlur={handleBlur} onKeyDown={onKeyDown} > - {openDropdown && ( - - )}
( + ( + { + searchResult, + searchValue, + openDropdown, + onOpenChange, + width, + onSelectItem, + isFocused, + }, + ref, + ) => { + const router = useRouter(); + + function onClick(href: string | undefined) { + onSelectItem?.(); + if (href) { + router.push(href); + } + } + const classes = styles(); + const searchClasses = searchStyles(); + const { isMobile } = useBreakpoints(); + const trimL = isMobile ? 15 : 20; + const trimR = isMobile ? 13 : 18; + + const hasResult = + searchResult?.account || + searchResult?.block || + searchResult?.contract || + searchResult?.transaction; + + return ( + + + + + + {!searchResult && ( + <> + + {`"${shortAddress( + searchValue, + trimL, + trimR, + )}" is not a valid address.`} + + + )} + {hasResult ? ( + <> + {searchResult?.account && ( + <> + Account + + searchResult.account?.address && + onClick( + Routes.accountAssets(searchResult.account.address!), + ) + } + > + + {shortAddress( + searchResult.account.address || '', + trimL, + trimR, + )} + + + + Recent Transactions + {searchResult.account.transactions?.map((transaction) => { + return ( + + transaction?.id && + onClick(Routes.txSimple(transaction?.id)) + } + > + + {shortAddress(transaction?.id || '', trimL, trimR)} + + + ); + })} + + )} + {searchResult?.block && ( + <> + {searchResult.block.id === searchValue && ( + <> + Block Hash + + searchResult.block?.id && + onClick(`/block/${searchResult.block.id}/simple`) + } + > + + {shortAddress( + searchResult.block.id || '', + trimL, + trimR, + )} + + + + )} + {searchResult.block.height === searchValue && ( + <> + Block Height + + searchResult.block?.height && + onClick(`/block/${searchResult.block?.height}/simple`) + } + > + + {searchResult.block.height} + + + + )} + + )} + {searchResult?.contract && ( + <> + Contract + + searchResult.contract?.id && + onClick(Routes.contractAssets(searchResult.contract.id)) + } + > + + {shortAddress( + searchResult.contract.id || '', + trimL, + trimR, + )} + + + + )} + {searchResult?.transaction && ( + <> + Transaction + + searchResult.transaction?.id && + onClick(Routes.txSimple(searchResult.transaction?.id)) + } + > + + {shortAddress( + searchResult.transaction.id || '', + trimL, + trimR, + )} + + + + )} + + ) : ( + <> + No instances found for: + + "{shortAddress(searchValue, trimL, trimR)}" + + + )} + + + ); + }, +); diff --git a/packages/app-explorer/src/systems/Core/components/Search/SearchResultDropdown/index.ts b/packages/app-explorer/src/systems/Core/components/Search/SearchResultDropdown/index.ts new file mode 100644 index 00000000..e1fca467 --- /dev/null +++ b/packages/app-explorer/src/systems/Core/components/Search/SearchResultDropdown/index.ts @@ -0,0 +1 @@ +export * from './SearchResultDropdown'; diff --git a/packages/app-explorer/src/systems/Core/components/Search/SearchResultDropdown/styles.ts b/packages/app-explorer/src/systems/Core/components/Search/SearchResultDropdown/styles.ts new file mode 100644 index 00000000..9794792a --- /dev/null +++ b/packages/app-explorer/src/systems/Core/components/Search/SearchResultDropdown/styles.ts @@ -0,0 +1,12 @@ +import { tv } from 'tailwind-variants'; + +export const styles = tv({ + slots: { + dropdownItem: 'hover:bg-border focus:bg-border cursor-pointer', + dropdownContent: [ + 'ml-[-10px] tablet:ml-0', + 'mt-[-10px] rounded-t-none shadow-none border border-t-0 border-border', + '[&[data-active=true]]:border-t-0', + ], + }, +}); diff --git a/packages/app-explorer/src/systems/Core/components/Search/SearchResultDropdown/types.ts b/packages/app-explorer/src/systems/Core/components/Search/SearchResultDropdown/types.ts new file mode 100644 index 00000000..402d1809 --- /dev/null +++ b/packages/app-explorer/src/systems/Core/components/Search/SearchResultDropdown/types.ts @@ -0,0 +1,11 @@ +import type { GQLSearchResult, Maybe } from '@fuel-explorer/graphql/sdk'; + +export type SearchDropdownProps = { + searchResult?: Maybe; + openDropdown: boolean; + isFocused: boolean; + onOpenChange: (open: boolean) => void; + searchValue: string; + width: number; + onSelectItem: () => void; +}; diff --git a/packages/app-explorer/src/systems/Core/components/Search/hooks/usePropagateInputMouseClick.ts b/packages/app-explorer/src/systems/Core/components/Search/hooks/usePropagateInputMouseClick.ts new file mode 100644 index 00000000..4067e97a --- /dev/null +++ b/packages/app-explorer/src/systems/Core/components/Search/hooks/usePropagateInputMouseClick.ts @@ -0,0 +1,43 @@ +import { type RefObject, useEffect } from 'react'; + +// Radix's Dropdown component uses a Portal to render the dropdown content, +// That causes Input to not capture click events, forcing the user to double click the input when the dropdown is open. +// To fix that we need to render a separate Portal to render the overlay and capture the click to make it work. + +export function usePropagateInputMouseClick({ + containerRef, + inputRef, + enabled, +}: { + containerRef: RefObject; + inputRef: RefObject; + enabled: boolean | undefined; +}) { + const boundingData = containerRef.current?.getBoundingClientRect(); + + useEffect(() => { + if (!enabled) { + return; + } + const onClick = (e: MouseEvent) => { + if (boundingData) { + const { clientX, clientY } = e; + if ( + clientX >= boundingData.x && + clientX <= boundingData.x + boundingData.width && + clientY >= boundingData.y && + clientY <= boundingData.y + boundingData.height + ) { + inputRef.current?.focus(); + } + } + }; + document.addEventListener('click', onClick); + + return () => { + setTimeout(() => { + document.removeEventListener('click', onClick); + }, 500); + }; + }, [enabled]); +} diff --git a/packages/app-explorer/src/systems/Core/components/Search/styles.ts b/packages/app-explorer/src/systems/Core/components/Search/styles.ts index c7d49ea1..451224c2 100644 --- a/packages/app-explorer/src/systems/Core/components/Search/styles.ts +++ b/packages/app-explorer/src/systems/Core/components/Search/styles.ts @@ -2,34 +2,6 @@ import { tv } from 'tailwind-variants'; export const styles = tv({ slots: { - searchBox: [ - 'transition-all duration-200 [&[data-active=false]]:ease-in [&[data-active=true]]:ease-out', - 'group justify-center items-center', - 'block left-0 w-full', // needed for properly execution of transitions - '[&[data-active=true]]:w-[calc(100vw+1px)] [&[data-active=true]]:left-[-64px] tablet:[&[data-active=true]]:w-full', - '[&[data-active=true]]:absolute tablet:[&[data-active=true]]:left-0 [&[data-active=true]]:right-0', - '[&[data-active=true]]:top-[-14px] tablet:[&[data-active=true]]:top-[-4px] desktop:[&[data-active=true]]:top-[-20px]', - '[&[data-active=true]]:z-50', - ], - dropdownItem: 'hover:bg-border focus:bg-border cursor-pointer', - inputContainer: 'w-full', - inputWrapper: [ - 'outline-none h-[40px] group-[&[data-active=true]]:h-[60px] tablet:group-[&[data-active=true]]:h-[40px]', - 'group-[&[data-active=true]]:rounded-none tablet:group-[&[data-active=true]]:rounded-[var(--text-field-border-radius)] ', - 'border-x-[1px] border-y-[1px] group-[&[data-active=true]]:border-x-0 group-[&[data-active=true]]:border-y-0 tablet:group-[&[data-active=true]]:border-x-[1px] tablet:group-[&[data-active=true]]:border-y-[1px]', - 'border-[var(--color-border)] shadow-none', - 'bg-none dark:bg-[var(--color-surface)] group-[&[data-active=true]]:bg-[var(--color-panel-solid)]', - '[&_.rt-TextFieldChrome]:bg-gray-1 [&_.rt-TextFieldChrome]:outline-none', - '[&_.rt-TextFieldChrome]:[&[data-opened=true]]:rounded-b-none', - 'group-[&[data-active=true]]:[&_.rt-TextFieldChrome]:shadow-none', - ], searchSize: 'w-full sm:w-[400px] group-[&[data-active=true]]:w-full', - inputActionsContainer: - '[&[data-show=false]]:hidden absolute flex items-center h-full right-0 top-0 transform', - dropdownContent: [ - 'ml-[-10px] tablet:ml-0', - 'mt-[-10px] rounded-t-none shadow-none border border-t-0 border-border', - '[&[data-active=true]]:border-t-0', - ], }, });