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:
parent
99c709ff8f
commit
57ca5d9a67
3
app/_locales/en/messages.json
generated
3
app/_locales/en/messages.json
generated
@ -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."
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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}
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user