diff --git a/package-lock.json b/package-lock.json index 5aab757..441bf42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,6 @@ "react-router-dom": "^6.22.3", "react-scripts": "^5.0.1", "react-spring": "^9.0.0", - "react-virtuoso": "^2.2.2", "recharts": "^2.0.9", "streamr-client-react": "^3.2.0", "styled-components": "^6.1.8", @@ -17843,25 +17842,6 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, - "node_modules/@virtuoso.dev/react-urx": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@virtuoso.dev/react-urx/-/react-urx-0.2.7.tgz", - "integrity": "sha512-iaJ/1vz7dkfpIxEjOpgOme8X6KkUsz16RoqpFHtqbKiQW1FI+1h7YbyTxhz5D9xmg/VZU9gHpXvPHTzdKIKa3Q==", - "dependencies": { - "@virtuoso.dev/urx": "^0.2.7" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": ">=16" - } - }, - "node_modules/@virtuoso.dev/urx": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@virtuoso.dev/urx/-/urx-0.2.7.tgz", - "integrity": "sha512-mv6V6Gzf1QnGUzb5Rwv5Eqs3DRX5R4nGIojVXtD+fAFnr2SAvpHNPMRNdAclvD/pcdiga6rTzUhFwq4FBC2MDw==" - }, "node_modules/@webassemblyjs/ast": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", @@ -43046,21 +43026,6 @@ "react-dom": ">=15.0.0" } }, - "node_modules/react-virtuoso": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-2.2.2.tgz", - "integrity": "sha512-ESwiOyliO/0q58P1X9e3IQosIKWmCv0JYO/ZDeLxh2O0FNWPQlXC5xgRT2fVxMXPoi+EaNI671foJuqOOITrOg==", - "dependencies": { - "@virtuoso.dev/react-urx": "^0.2.5", - "@virtuoso.dev/urx": "^0.2.5" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": ">=16" - } - }, "node_modules/reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", @@ -63174,19 +63139,6 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, - "@virtuoso.dev/react-urx": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@virtuoso.dev/react-urx/-/react-urx-0.2.7.tgz", - "integrity": "sha512-iaJ/1vz7dkfpIxEjOpgOme8X6KkUsz16RoqpFHtqbKiQW1FI+1h7YbyTxhz5D9xmg/VZU9gHpXvPHTzdKIKa3Q==", - "requires": { - "@virtuoso.dev/urx": "^0.2.7" - } - }, - "@virtuoso.dev/urx": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@virtuoso.dev/urx/-/urx-0.2.7.tgz", - "integrity": "sha512-mv6V6Gzf1QnGUzb5Rwv5Eqs3DRX5R4nGIojVXtD+fAFnr2SAvpHNPMRNdAclvD/pcdiga6rTzUhFwq4FBC2MDw==" - }, "@webassemblyjs/ast": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", @@ -82056,15 +82008,6 @@ "react-lifecycles-compat": "^3.0.4" } }, - "react-virtuoso": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-2.2.2.tgz", - "integrity": "sha512-ESwiOyliO/0q58P1X9e3IQosIKWmCv0JYO/ZDeLxh2O0FNWPQlXC5xgRT2fVxMXPoi+EaNI671foJuqOOITrOg==", - "requires": { - "@virtuoso.dev/react-urx": "^0.2.5", - "@virtuoso.dev/urx": "^0.2.5" - } - }, "reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", diff --git a/package.json b/package.json index 1558abb..0256552 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "react-router-dom": "^6.22.3", "react-scripts": "^5.0.1", "react-spring": "^9.0.0", - "react-virtuoso": "^2.2.2", "recharts": "^2.0.9", "streamr-client-react": "^3.2.0", "styled-components": "^6.1.8", diff --git a/src/Store.tsx b/src/Store.tsx index 849ce3a..10cb2ec 100644 --- a/src/Store.tsx +++ b/src/Store.tsx @@ -14,17 +14,21 @@ import { useGlobalKeyDownEffect, useStreamIdParam } from './hooks' import { useDebounced } from './hooks/wrapCallback' import { ActiveView, OperatorNode } from './types' import { useOperatorNodesForStreamQuery } from './utils/nodes' +import { truncate } from './utils/text' interface Store { activeView: ActiveView + displaySearchPhrase: string invalidateLocationParamKey(): void invalidateNodeIdParamKey(): void locationParamKey: number mapRef: RefObject - nodeIdParamkey: number + nodeIdParamKey: number resetViewport(): void + searchPhrase: string selectedNode: OperatorNode | null setActiveView(value: ActiveView): void + setSearchPhrase(value: string): void setViewport(fn: (viewport: ViewportProps) => ViewportProps): void setViewportDebounced(fn: (viewport: ViewportProps) => ViewportProps): void viewport: ViewportProps @@ -51,14 +55,17 @@ const defaultViewport: ViewportProps = { const StoreContext = createContext({ activeView: ActiveView.Map, + displaySearchPhrase: '', invalidateLocationParamKey: () => {}, invalidateNodeIdParamKey: () => {}, locationParamKey: -1, mapRef: { current: null }, - nodeIdParamkey: -1, + nodeIdParamKey: -1, resetViewport: () => {}, + searchPhrase: '', selectedNode: null, setActiveView: () => {}, + setSearchPhrase: () => {}, setViewport: () => {}, setViewportDebounced: () => {}, viewport: defaultViewport, @@ -74,7 +81,7 @@ export function StoreProvider({ mapRef, ...props }: StoreProviderProps) { const [locationParamKey, invalidateLocationParamKey] = useReducer((x: number) => x + 1, 0) - const [nodeIdParamkey, invalidateNodeIdParamKey] = useReducer((x: number) => x + 1, 0) + const [nodeIdParamKey, invalidateNodeIdParamKey] = useReducer((x: number) => x + 1, 0) const [viewport, setViewport] = useState(defaultViewport) @@ -93,19 +100,32 @@ export function StoreProvider({ mapRef, ...props }: StoreProviderProps) { const [activeView, setActiveView] = useState(ActiveView.Map) + const [rawSearchPhrase, setRawSearchPhrase] = useState('') + + const [displaySearchPhrase, setDisplaySearchPhrase] = useState('') + + const setSearchPhrase = useCallback((value: string) => { + setRawSearchPhrase(value) + + setDisplaySearchPhrase(truncate(value)) + }, []) + return ( { onClick?.(nodeId) + + /** + * If the page address includes the node id already and the user + * panned away then the above won't be sufficient to get node's location + * back into viewport. To address it, we have to invalidate the node id + * param key. + */ + invalidateNodeIdParamKey() }} onMouseEnter={() => { if (highlightPointOnHover) { diff --git a/src/components/SearchBox/SearchInput.tsx b/src/components/SearchBox/SearchInput.tsx index 5dff58f..73355ae 100644 --- a/src/components/SearchBox/SearchInput.tsx +++ b/src/components/SearchBox/SearchInput.tsx @@ -1,8 +1,7 @@ /* eslint-disable jsx-a11y/label-has-associated-control */ -import React, { ChangeEvent, InputHTMLAttributes, RefObject, useState } from 'react' -import styled from 'styled-components' - import uniqueId from 'lodash/uniqueId' +import React, { ChangeEvent, FocusEvent, InputHTMLAttributes, RefObject, useState } from 'react' +import styled from 'styled-components' import { MD, SANS, SM } from '../../utils/styled' export const SearchInputInner = styled.div` @@ -161,18 +160,23 @@ const ClearIcon = () => ( ) interface SearchInputProps - extends Omit, 'id' | 'type' | 'autoComplete'> { + extends Omit, 'id' | 'type' | 'autoComplete' | 'value'> { + displayValue?: string inputRef: RefObject onClearButtonClick?(): void + value?: string } const UnstyledSearchInput = ({ - inputRef, className, + displayValue = '', + inputRef, + onBlur: onBlurProp, + onChange: onChangeProp, onClearButtonClick, + onFocus: onFocusProp, placeholder = 'Search Streamr Network', value = '', - onChange: onChangeProp, ...props }: SearchInputProps) => { const inputId = useState(uniqueId('input-'))[0] @@ -181,6 +185,24 @@ const UnstyledSearchInput = ({ onChangeProp?.(e) } + const [focused, setFocused] = useState(false) + + function onFocus(e: FocusEvent) { + onFocusProp?.(e) + + setFocused(true) + + setTimeout(() => { + inputRef.current?.setSelectionRange(0, value.length) + }) + } + + function onBlur(e: FocusEvent) { + onBlurProp?.(e) + + setFocused(false) + } + return (
@@ -191,13 +213,15 @@ const UnstyledSearchInput = ({ {value ? ( diff --git a/src/components/SearchBox/SearchResults.tsx b/src/components/SearchBox/SearchResults.tsx index d2495d9..f81177b 100644 --- a/src/components/SearchBox/SearchResults.tsx +++ b/src/components/SearchBox/SearchResults.tsx @@ -1,14 +1,13 @@ import React from 'react' import { useNavigate, useSearchParams } from 'react-router-dom' -import { Virtuoso } from 'react-virtuoso' import styled from 'styled-components' import { useStore } from '../../Store' +import { useNavigateToNodeCallback } from '../../hooks' import { SearchResultItem } from '../../types' import { getNodeLocationId, setNodeFeatureState } from '../../utils/map' import { MD, SANS, SM } from '../../utils/styled' import Highlight from '../Highlight' import { LocationIcon, NodeIcon, StreamIcon } from './Icons' -import { useNavigateToNodeCallback } from '../../hooks' const IconWrapper = styled.div` display: flex; @@ -107,17 +106,6 @@ const Details = styled.div` } ` -const List = styled.div` - display: grid; - - a, - a:hover, - a:active, - a:visited { - text-decoration: none; - } -` - type Props = { results: SearchResultItem[] highlight?: string @@ -127,12 +115,9 @@ type Props = { export function SearchResults({ results, highlight, onItemClick, ...props }: Props) { return ( - ( - - )} - /> + {results.map((value) => ( + + ))} ) } @@ -228,10 +213,11 @@ function Item({ highlight, value, onClick }: ItemProps) { } export const SearchResultsRoot = styled.div` + max-height: 512px; + overflow: auto; + @media (max-width: ${SM}px) { - ${List} { - grid-row-gap: 8px; - } + max-height: 336px; ${Row} { border: 1px solid #efefef; diff --git a/src/components/SearchBox/index.tsx b/src/components/SearchBox/index.tsx index 0b3994d..6aaf0e0 100644 --- a/src/components/SearchBox/index.tsx +++ b/src/components/SearchBox/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react' +import React, { useEffect, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { useStore } from '../../Store' import { useGlobalKeyDownEffect, useStreamIdParam } from '../../hooks' @@ -11,17 +11,22 @@ import { SearchInput } from './SearchInput' import { SearchResults } from './SearchResults' export function SearchBox() { - const [phrase, setPhrase] = useState('') - - const { selectedNode, activeView, setActiveView } = useStore() + const { + selectedNode, + activeView, + setActiveView, + searchPhrase, + setSearchPhrase, + displaySearchPhrase, + } = useStore() const selectedNodeId = selectedNode?.id || null const streamId = useStreamIdParam() - const hasSelection = phrase === selectedNodeId || phrase === streamId + const hasSelection = searchPhrase === selectedNodeId || searchPhrase === streamId - const finalPhrase = hasSelection ? '' : phrase + const finalPhrase = hasSelection ? '' : searchPhrase const isSearchPending = useIsSearching(finalPhrase) @@ -29,13 +34,6 @@ export function SearchBox() { const searchRef = useRef(null) - useEffect( - function setSelectedStreamIdOrNodeIdAsPhrase() { - setPhrase(streamId ? streamId : selectedNodeId || '') - }, - [selectedNodeId, streamId], - ) - const navigate = useNavigate() const inputRef = useRef(null) @@ -50,6 +48,8 @@ export function SearchBox() { }, ) + useSetInitialSearchPhraseEffect() + return ( <> { - setPhrase(e.target.value) + const { value } = e.target + + setSearchPhrase(value) + + if (streamId) { + if (value !== streamId) { + navigate('/') + } + } else if (selectedNodeId && value !== selectedNodeId) { + navigate('/') + } }} onClearButtonClick={() => { - if (phrase === selectedNodeId || phrase === streamId) { + if (searchPhrase === selectedNodeId || searchPhrase === streamId) { navigate('/') } - setPhrase('') + setSearchPhrase('') inputRef.current?.focus() }} @@ -87,13 +98,13 @@ export function SearchBox() { return } - if (phrase === '') { + if (searchPhrase === '') { inputRef.current?.blur() return } - setPhrase('') + setSearchPhrase('') }} /> @@ -104,7 +115,7 @@ export function SearchBox() { {searchResults.length > 0 && ( { if (item.type !== 'node' && item.type !== 'stream') { return @@ -116,7 +127,7 @@ export function SearchBox() { * the effect calling `setSelectedStreamIdOrNodeIdAsPhrase`. We have to set the phrase * manually to ensure things are in good order. */ - setPhrase(item.payload.id) + setSearchPhrase(item.payload.id) }} /> )} @@ -127,3 +138,22 @@ export function SearchBox() { ) } + +function useSetInitialSearchPhraseEffect() { + const { setSearchPhrase, selectedNode } = useStore() + + const streamId = useStreamIdParam() + + const selectedNodeId = selectedNode?.id + + useEffect( + function setSearchPhraseOnMountOnly() { + if (streamId) { + setSearchPhrase(streamId) + } else if (selectedNodeId) { + setSearchPhrase(selectedNodeId) + } + }, + [streamId, selectedNodeId, setSearchPhrase], + ) +} diff --git a/src/hooks/index.tsx b/src/hooks/index.tsx index 34a56d9..cfc8726 100644 --- a/src/hooks/index.tsx +++ b/src/hooks/index.tsx @@ -1,7 +1,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useNavigate, useParams, useSearchParams } from 'react-router-dom' +import { useStore } from '../Store' -export function useGlobalKeyDownEffect(key: string | RegExp, fn: () => void, { preventDefault = false } = {}) { +export function useGlobalKeyDownEffect( + key: string | RegExp, + fn: () => void, + { preventDefault = false } = {}, +) { const fnRef = useRef(fn) if (fnRef.current !== fn) { @@ -29,7 +34,7 @@ export function useGlobalKeyDownEffect(key: string | RegExp, fn: () => void, { p fnRef.current() } - if (preventDefault) { + if (match && preventDefault) { e.preventDefault() } } @@ -40,7 +45,7 @@ export function useGlobalKeyDownEffect(key: string | RegExp, fn: () => void, { p window.removeEventListener('keydown', onKeyDown) } }, - [key], + [key, preventDefault], ) } @@ -127,6 +132,8 @@ export function useNavigateToNodeCallback() { const streamId = useStreamIdParam() + const { setSearchPhrase } = useStore() + return useCallback( (nodeId: string, { replace = false } = {}) => { const nodePath = nodeId ? `nodes/${encodeURIComponent(nodeId)}/` : '' @@ -134,7 +141,15 @@ export function useNavigateToNodeCallback() { navigate(streamId ? `/streams/${encodeURIComponent(streamId)}/${nodePath}` : `/${nodePath}`, { replace, }) + + if (streamId) { + setSearchPhrase(streamId) + } else if (nodeId) { + setSearchPhrase(nodeId) + } else { + setSearchPhrase('') + } }, - [navigate, streamId], + [navigate, streamId, setSearchPhrase], ) } diff --git a/src/utils/text.ts b/src/utils/text.ts index c83d5c5..e5a156c 100644 --- a/src/utils/text.ts +++ b/src/utils/text.ts @@ -1,3 +1,3 @@ export function truncate(value: string) { - return value.replace(/0x([a-f\d]{3})[a-f\d]{32,}([a-f\d]{5})/ig, '0x$1...$2') + return value.replace(/0x([a-f\d]{3})[a-f\d]{32,}([a-f\d]{5})/ig, '0x$1...$2').replace(/([a-f\d]{5})[a-f\d]{24,}([a-f\d]{5})/ig, '$1...$2') }