Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add search feature #25170

Merged
merged 2 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`NetworkListMenu renders properly 1`] = `<div />`;
85 changes: 50 additions & 35 deletions ui/components/multichain/network-list-menu/network-list-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,9 @@ import ToggleButton from '../../ui/toggle-button';
import {
AlignItems,
BackgroundColor,
BlockSize,
Display,
FlexDirection,
JustifyContent,
Size,
TextColor,
} from '../../../helpers/constants/design-system';
import {
Expand All @@ -56,7 +54,6 @@ import {
ModalContent,
ModalHeader,
} from '../../component-library';
import { TextFieldSearch } from '../../component-library/text-field-search/deprecated';
import { ADD_POPULAR_CUSTOM_NETWORK } from '../../../helpers/constants/routes';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app';
Expand All @@ -71,6 +68,7 @@ import {
} from '../../../ducks/metamask/metamask';
import { getLocalNetworkMenuRedesignFeatureFlag } from '../../../helpers/utils/feature-flags';
import PopularNetworkList from './popular-network-list/popular-network-list';
import NetworkListSearch from './network-list-search/network-list-search';

export const NetworkListMenu = ({ onClose }) => {
const t = useI18nContext();
Expand Down Expand Up @@ -101,8 +99,6 @@ export const NetworkListMenu = ({ onClose }) => {

const isUnlocked = useSelector(getIsUnlocked);

const showSearch = nonTestNetworks.length > 3;

const orderedNetworksList = useSelector(getOrderedNetworksList);

const networkConfigurationChainIds = Object.values(networkConfigurations).map(
Expand All @@ -116,6 +112,7 @@ export const NetworkListMenu = ({ onClose }) => {
const notExistingNetworkConfigurations = sortedFeaturedNetworks.filter(
({ chainId }) => !networkConfigurationChainIds.includes(chainId),
);

const newOrderNetworks = () => {
if (!orderedNetworksList || orderedNetworksList.length === 0) {
return nonTestNetworks;
Expand Down Expand Up @@ -147,6 +144,7 @@ export const NetworkListMenu = ({ onClose }) => {
}, [dispatch, currentlyOnTestNetwork]);

const [searchQuery, setSearchQuery] = useState('');
const [focusSearch, setFocusSearch] = useState(false);
const onboardedInThisUISession = useSelector(getOnboardedInThisUISession);
const showNetworkBanner = useSelector(getShowNetworkBanner);
const showBanner =
Expand Down Expand Up @@ -175,14 +173,14 @@ export const NetworkListMenu = ({ onClose }) => {
let searchResults =
[...networksList].length === items.length ? items : [...networksList];

const searchAddNetworkResults =
let searchAddNetworkResults =
[...notExistingNetworkConfigurations].length === items.length
? items
: [...notExistingNetworkConfigurations];

const isSearching = searchQuery !== '';
let searchTestNetworkResults = [...testNetworks];

if (isSearching) {
if (focusSearch && searchQuery !== '') {
const fuse = new Fuse(searchResults, {
threshold: 0.2,
location: 0,
Expand All @@ -192,12 +190,45 @@ export const NetworkListMenu = ({ onClose }) => {
shouldSort: true,
keys: ['nickname', 'chainId', 'ticker'],
});
const fuseForPopularNetworks = new Fuse(searchAddNetworkResults, {
threshold: 0.2,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
shouldSort: true,
keys: ['nickname', 'chainId', 'ticker'],
});

const fuseForTestsNetworks = new Fuse(searchTestNetworkResults, {
threshold: 0.2,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
shouldSort: true,
keys: ['nickname', 'chainId', 'ticker'],
});

fuse.setCollection(searchResults);
fuseForPopularNetworks.setCollection(searchAddNetworkResults);
fuseForTestsNetworks.setCollection(searchTestNetworkResults);

const fuseResults = fuse.search(searchQuery);
// Ensure order integrity with original list
const fuseForPopularNetworksResults =
fuseForPopularNetworks.search(searchQuery);
const fuseForTestsNetworksResults =
fuseForTestsNetworks.search(searchQuery);

searchResults = searchResults.filter((network) =>
fuseResults.includes(network),
);
searchAddNetworkResults = searchAddNetworkResults.filter((network) =>
fuseForPopularNetworksResults.includes(network),
);
searchTestNetworkResults = searchTestNetworkResults.filter((network) =>
fuseForTestsNetworksResults.includes(network),
);
}

const generateNetworkListItem = ({
Expand All @@ -210,8 +241,8 @@ export const NetworkListMenu = ({ onClose }) => {
name={network.nickname}
iconSrc={network?.rpcPrefs?.imageUrl}
key={network.id}
selected={isCurrentNetwork}
focus={isCurrentNetwork && !showSearch}
selected={isCurrentNetwork && !focusSearch}
focus={isCurrentNetwork && !focusSearch}
onClick={() => {
dispatch(toggleNetworkMenu());
if (network.providerType) {
Expand Down Expand Up @@ -305,27 +336,11 @@ export const NetworkListMenu = ({ onClose }) => {
{t('networkMenuHeading')}
</ModalHeader>
<>
{showSearch ? (
salimtb marked this conversation as resolved.
Show resolved Hide resolved
<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}
<NetworkListSearch
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
setFocusSearch={setFocusSearch}
/>
{showBanner ? (
<BannerBase
className="network-list-menu__banner"
Expand All @@ -350,7 +365,7 @@ export const NetworkListMenu = ({ onClose }) => {
/>
) : null}
<Box className="multichain-network-list-menu">
{searchResults.length === 0 && isSearching ? (
{searchResults.length === 0 && focusSearch ? (
<Text
paddingLeft={4}
paddingRight={4}
Expand Down Expand Up @@ -424,11 +439,11 @@ export const NetworkListMenu = ({ onClose }) => {
</Box>
{showTestNetworks || currentlyOnTestNetwork ? (
<Box className="multichain-network-list-menu">
{generateMenuItems(testNetworks)}
{generateMenuItems(searchTestNetworkResults)}
</Box>
) : null}
</Box>
<Box padding={4}>
<Box paddingLeft={4} paddingRight={4} paddingTop={4}>
<ButtonSecondary
size={ButtonSecondarySize.Lg}
startIconName={IconName.Add}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ describe('NetworkListMenu', () => {
mockNetworkMenuRedesignToggle.mockReturnValue(false);
});

it('renders properly', () => {
const { container } = render();
expect(container).toMatchSnapshot();
});
it('displays important controls', () => {
const { getByText, getByPlaceholderText } = render();

Expand Down Expand Up @@ -129,6 +133,7 @@ describe('NetworkListMenu', () => {
expect(queryByText('Chain 5')).toBeInTheDocument();

const searchBox = getByPlaceholderText('Search');
fireEvent.focus(searchBox);
fireEvent.change(searchBox, { target: { value: 'Main' } });

expect(queryByText('Chain 5')).not.toBeInTheDocument();
Expand All @@ -150,4 +155,51 @@ describe('NetworkListMenu', () => {
document.querySelectorAll('multichain-network-list-item__delete'),
).toHaveLength(0);
});

describe('NetworkListMenu with ENABLE_NETWORK_UI_REDESIGN', () => {
// Set the environment variable before tests run
beforeEach(() => {
process.env.ENABLE_NETWORK_UI_REDESIGN = 'true';
});

// Reset the environment variable after tests complete
afterEach(() => {
delete process.env.ENABLE_NETWORK_UI_REDESIGN;
});

it('should display "Arbitrum" when ENABLE_NETWORK_UI_REDESIGN is true', async () => {
const { queryByText, getByPlaceholderText } = render();

// Now "Arbitrum" should be in the document if PopularNetworkList is rendered
expect(queryByText('Arbitrum One')).toBeInTheDocument();

// Simulate typing "Optimism" into the search box
const searchBox = getByPlaceholderText('Search');
fireEvent.focus(searchBox);
fireEvent.change(searchBox, { target: { value: 'OP Mainnet' } });

// "Optimism" should be visible, but "Arbitrum" should not
expect(queryByText('OP Mainnet')).toBeInTheDocument();
expect(queryByText('Arbitrum One')).not.toBeInTheDocument();
});

it('should filter testNets when ENABLE_NETWORK_UI_REDESIGN is true', async () => {
const { queryByText, getByPlaceholderText } = render({
showTestNetworks: true,
});

// Check if all testNets are available
expect(queryByText('Linea Sepolia')).toBeInTheDocument();
expect(queryByText('Sepolia')).toBeInTheDocument();

// Simulate typing "Linea Sepolia" into the search box
const searchBox = getByPlaceholderText('Search');
fireEvent.focus(searchBox);
fireEvent.change(searchBox, { target: { value: 'Linea Sepolia' } });

// "Linea Sepolia" should be visible, but "Sepolia" should not
expect(queryByText('Linea Sepolia')).toBeInTheDocument();
expect(queryByText('Sepolia')).not.toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`NetworkListSearch renders search list component 1`] = `
<div>
<div
class="mm-box mm-box--padding-top-0 mm-box--padding-right-4 mm-box--padding-bottom-4 mm-box--padding-left-4"
>
<div
class="mm-box mm-text-field mm-text-field--size-lg mm-text-field--focused mm-text-field--truncate mm-text-field-search mm-box--padding-right-0 mm-box--padding-left-4 mm-box--display-inline-flex mm-box--align-items-center mm-box--width-full mm-box--background-color-background-default mm-box--rounded-sm mm-box--border-width-1 box--border-style-solid"
data-testid="search-list"
>
<span
class="mm-box mm-icon mm-icon--size-sm mm-box--display-inline-block mm-box--color-inherit"
style="mask-image: url('./images/icons/search.svg');"
/>
<input
autocomplete="off"
class="mm-box mm-text mm-input mm-input--disable-state-styles mm-text-field__input mm-text--body-md mm-box--margin-0 mm-box--margin-right-6 mm-box--padding-0 mm-box--padding-right-4 mm-box--padding-left-2 mm-box--color-text-default mm-box--background-color-transparent mm-box--border-style-none"
data-testid="network-redesign-modal-search-input"
focused="true"
placeholder="search"
type="search"
value=""
/>
</div>
</div>
</div>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import NetworkListSearch from './network-list-search';

jest.mock('../../../../hooks/useI18nContext', () => ({
useI18nContext: jest.fn(),
}));

describe('NetworkListSearch', () => {
const mockSetSearchQuery = jest.fn();
const mockSetFocusSearch = jest.fn();
const useI18nContextMock = useI18nContext as jest.Mock;

beforeEach(() => {
jest.clearAllMocks();
useI18nContextMock.mockReturnValue((key: string) => key);
});

it('renders search list component', () => {
const { container } = render(
<NetworkListSearch
searchQuery=""
setSearchQuery={mockSetSearchQuery}
setFocusSearch={mockSetFocusSearch}
/>,
);

expect(container).toMatchSnapshot();
});

it('should update search query on user input', () => {
const { getByPlaceholderText } = render(
<NetworkListSearch
searchQuery=""
setSearchQuery={mockSetSearchQuery}
setFocusSearch={mockSetFocusSearch}
/>,
);

const searchInput = getByPlaceholderText('search');
fireEvent.change(searchInput, { target: { value: 'Ethereum' } });

expect(mockSetSearchQuery).toHaveBeenCalledWith('Ethereum');
});

it('should clear search query when clear button is clicked', () => {
const { getByRole } = render(
<NetworkListSearch
searchQuery="Ethereum"
setSearchQuery={mockSetSearchQuery}
setFocusSearch={mockSetFocusSearch}
/>,
);

const clearButton = getByRole('button', { name: /clear/u });
fireEvent.click(clearButton);

expect(mockSetSearchQuery).toHaveBeenCalledWith('');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import {
Box,
ButtonIconSize,
TextFieldSearch,
TextFieldSearchSize,
} from '../../../component-library';
import { BlockSize } from '../../../../helpers/constants/design-system';

const NetworkListSearch = ({
searchQuery,
setSearchQuery,
setFocusSearch,
}: {
searchQuery: string;
setSearchQuery: (query: string) => void;
setFocusSearch: (val: boolean) => void;
}) => {
const t = useI18nContext();

return (
<Box paddingLeft={4} paddingRight={4} paddingBottom={4} paddingTop={0}>
<TextFieldSearch
size={TextFieldSearchSize.Lg}
width={BlockSize.Full}
placeholder={t('search')}
autoFocus
value={searchQuery}
onFocus={() => setFocusSearch(true)}
onBlur={() => setFocusSearch(false)}
onChange={(e) => setSearchQuery(e.target.value)}
clearButtonOnClick={() => setSearchQuery('')}
clearButtonProps={{
size: ButtonIconSize.Sm,
}}
inputProps={{ 'data-testid': 'network-redesign-modal-search-input' }}
data-testid="search-list"
/>
</Box>
);
};

export default NetworkListSearch;