1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

Implement Network Menu Search (#19985)

* WIP: Implement Network Menu Search

* Maintain order, add tests

* Remove unwanted locale

* Fix duplicate import, better focus and item autofocus
This commit is contained in:
David Walsh 2023-07-28 11:25:48 -05:00 committed by GitHub
parent 99c709ff8f
commit 57ca5d9a67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 87 additions and 5 deletions

View File

@ -2538,6 +2538,9 @@
"noNFTs": { "noNFTs": {
"message": "No NFTs yet" "message": "No NFTs yet"
}, },
"noNetworksFound": {
"message": "No networks found for the given search query"
},
"noSnaps": { "noSnaps": {
"message": "You don't have any snaps installed." "message": "You don't have any snaps installed."
}, },

View File

@ -47,6 +47,7 @@ export const NetworkListItem = ({
name, name,
iconSrc, iconSrc,
selected = false, selected = false,
focus = true,
onClick, onClick,
onDeleteClick, onDeleteClick,
}) => { }) => {
@ -54,10 +55,10 @@ export const NetworkListItem = ({
const networkRef = useRef(); const networkRef = useRef();
useEffect(() => { useEffect(() => {
if (networkRef.current && selected) { if (networkRef.current && focus) {
networkRef.current.focus(); networkRef.current.focus();
} }
}, [networkRef, selected]); }, [networkRef, focus]);
return ( return (
<Box <Box
@ -148,4 +149,8 @@ NetworkListItem.propTypes = {
* Executes when the delete icon is clicked * Executes when the delete icon is clicked
*/ */
onDeleteClick: PropTypes.func, onDeleteClick: PropTypes.func,
/**
* Represents if the network item should be keyboard selected
*/
focus: PropTypes.bool,
}; };

View File

@ -1,7 +1,8 @@
import React, { useContext, useEffect } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import Fuse from 'fuse.js';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { NetworkListItem } from '../network-list-item'; import { NetworkListItem } from '../network-list-item';
import { import {
@ -21,8 +22,11 @@ import {
} from '../../../selectors'; } from '../../../selectors';
import ToggleButton from '../../ui/toggle-button'; import ToggleButton from '../../ui/toggle-button';
import { import {
BlockSize,
Display, Display,
JustifyContent, JustifyContent,
Size,
TextColor,
} from '../../../helpers/constants/design-system'; } from '../../../helpers/constants/design-system';
import { import {
BUTTON_SECONDARY_SIZES, BUTTON_SECONDARY_SIZES,
@ -33,6 +37,7 @@ import {
ModalOverlay, ModalOverlay,
Box, Box,
Text, Text,
TextFieldSearch,
} from '../../component-library'; } from '../../component-library';
import { ADD_POPULAR_CUSTOM_NETWORK } from '../../../helpers/constants/routes'; import { ADD_POPULAR_CUSTOM_NETWORK } from '../../../helpers/constants/routes';
import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { getEnvironmentType } from '../../../../app/scripts/lib/util';
@ -75,11 +80,35 @@ export const NetworkListMenu = ({ onClose }) => {
const lineaMainnetReleased = useSelector(isLineaMainnetNetworkReleased); const lineaMainnetReleased = useSelector(isLineaMainnetNetworkReleased);
const showSearch = nonTestNetworks.length > 3;
useEffect(() => { useEffect(() => {
if (currentlyOnTestNetwork) { if (currentlyOnTestNetwork) {
dispatch(setShowTestNetworks(currentlyOnTestNetwork)); dispatch(setShowTestNetworks(currentlyOnTestNetwork));
} }
}, [dispatch, currentlyOnTestNetwork]); }, [dispatch, currentlyOnTestNetwork]);
const [searchQuery, setSearchQuery] = useState('');
let searchResults = [...nonTestNetworks];
const isSearching = searchQuery !== '';
if (isSearching) {
const fuse = new Fuse(searchResults, {
threshold: 0.2,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
shouldSort: true,
keys: ['nickname', 'chainId', 'ticker'],
});
fuse.setCollection(searchResults);
const fuseResults = fuse.search(searchQuery);
// Ensure order integrity with original list
searchResults = searchResults.filter((network) =>
fuseResults.includes(network),
);
}
const generateMenuItems = (desiredNetworks) => { const generateMenuItems = (desiredNetworks) => {
return desiredNetworks.map((network, index) => { return desiredNetworks.map((network, index) => {
@ -98,6 +127,7 @@ export const NetworkListMenu = ({ onClose }) => {
iconSrc={network?.rpcPrefs?.imageUrl} iconSrc={network?.rpcPrefs?.imageUrl}
key={`${network.id || network.chainId}-${index}`} key={`${network.id || network.chainId}-${index}`}
selected={isCurrentNetwork} selected={isCurrentNetwork}
focus={isCurrentNetwork && !showSearch}
onClick={() => { onClick={() => {
dispatch(toggleNetworkMenu()); dispatch(toggleNetworkMenu());
if (network.providerType) { if (network.providerType) {
@ -162,8 +192,40 @@ export const NetworkListMenu = ({ onClose }) => {
{t('networkMenuHeading')} {t('networkMenuHeading')}
</ModalHeader> </ModalHeader>
<> <>
{showSearch ? (
<Box
paddingLeft={4}
paddingRight={4}
paddingBottom={4}
paddingTop={0}
>
<TextFieldSearch
size={Size.SM}
width={BlockSize.Full}
placeholder={t('search')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
clearButtonOnClick={() => setSearchQuery('')}
clearButtonProps={{
size: Size.SM,
}}
inputProps={{ autoFocus: true }}
/>
</Box>
) : null}
<Box className="multichain-network-list-menu"> <Box className="multichain-network-list-menu">
{generateMenuItems(nonTestNetworks)} {searchResults.length === 0 && isSearching ? (
<Text
paddingLeft={4}
paddingRight={4}
color={TextColor.textMuted}
data-testid="multichain-network-menu-popover-no-results"
>
{t('noNetworksFound')}
</Text>
) : (
generateMenuItems(searchResults)
)}
</Box> </Box>
<Box <Box
padding={4} padding={4}

View File

@ -44,10 +44,11 @@ const render = (
describe('NetworkListMenu', () => { describe('NetworkListMenu', () => {
it('displays important controls', () => { it('displays important controls', () => {
const { getByText } = render(); const { getByText, getByPlaceholderText } = render();
expect(getByText('Add network')).toBeInTheDocument(); expect(getByText('Add network')).toBeInTheDocument();
expect(getByText('Show test networks')).toBeInTheDocument(); expect(getByText('Show test networks')).toBeInTheDocument();
expect(getByPlaceholderText('Search')).toBeInTheDocument();
}); });
it('renders mainnet item', () => { it('renders mainnet item', () => {
@ -99,4 +100,15 @@ describe('NetworkListMenu', () => {
).textContent; ).textContent;
expect(selectedNodeText).toStrictEqual('Custom Mainnet RPC'); expect(selectedNodeText).toStrictEqual('Custom Mainnet RPC');
}); });
it('narrows down search results', () => {
const { queryByText, getByPlaceholderText } = render();
expect(queryByText('Chain 5')).toBeInTheDocument();
const searchBox = getByPlaceholderText('Search');
fireEvent.change(searchBox, { target: { value: 'Main' } });
expect(queryByText('Chain 5')).not.toBeInTheDocument();
});
}); });