diff --git a/ui/components/multichain/network-list-menu/__snapshots__/network-list-menu.test.js.snap b/ui/components/multichain/network-list-menu/__snapshots__/network-list-menu.test.js.snap new file mode 100644 index 000000000000..1b2a8d056105 --- /dev/null +++ b/ui/components/multichain/network-list-menu/__snapshots__/network-list-menu.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NetworkListMenu renders properly 1`] = `
`; diff --git a/ui/components/multichain/network-list-menu/network-list-menu.js b/ui/components/multichain/network-list-menu/network-list-menu.js index da7f145e7389..022ad7f055b0 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.js +++ b/ui/components/multichain/network-list-menu/network-list-menu.js @@ -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 { @@ -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'; @@ -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(); @@ -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( @@ -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; @@ -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 = @@ -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, @@ -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 = ({ @@ -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) { @@ -305,27 +336,11 @@ export const NetworkListMenu = ({ onClose }) => { {t('networkMenuHeading')} <> - {showSearch ? ( - - setSearchQuery(e.target.value)} - clearButtonOnClick={() => setSearchQuery('')} - clearButtonProps={{ - size: Size.SM, - }} - inputProps={{ autoFocus: true }} - /> - - ) : null} + {showBanner ? ( { /> ) : null} - {searchResults.length === 0 && isSearching ? ( + {searchResults.length === 0 && focusSearch ? ( { {showTestNetworks || currentlyOnTestNetwork ? ( - {generateMenuItems(testNetworks)} + {generateMenuItems(searchTestNetworkResults)} ) : null} - + { mockNetworkMenuRedesignToggle.mockReturnValue(false); }); + it('renders properly', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); it('displays important controls', () => { const { getByText, getByPlaceholderText } = render(); @@ -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(); @@ -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(); + }); + }); }); diff --git a/ui/components/multichain/network-list-menu/network-list-search/__snapshots__/network-list-search.test.tsx.snap b/ui/components/multichain/network-list-menu/network-list-search/__snapshots__/network-list-search.test.tsx.snap new file mode 100644 index 000000000000..6b2597430661 --- /dev/null +++ b/ui/components/multichain/network-list-menu/network-list-search/__snapshots__/network-list-search.test.tsx.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NetworkListSearch renders search list component 1`] = ` +
+
+ +
+
+`; diff --git a/ui/components/multichain/network-list-menu/network-list-search/network-list-search.test.tsx b/ui/components/multichain/network-list-menu/network-list-search/network-list-search.test.tsx new file mode 100644 index 000000000000..c6b164976563 --- /dev/null +++ b/ui/components/multichain/network-list-menu/network-list-search/network-list-search.test.tsx @@ -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( + , + ); + + expect(container).toMatchSnapshot(); + }); + + it('should update search query on user input', () => { + const { getByPlaceholderText } = render( + , + ); + + 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( + , + ); + + const clearButton = getByRole('button', { name: /clear/u }); + fireEvent.click(clearButton); + + expect(mockSetSearchQuery).toHaveBeenCalledWith(''); + }); +}); diff --git a/ui/components/multichain/network-list-menu/network-list-search/network-list-search.tsx b/ui/components/multichain/network-list-menu/network-list-search/network-list-search.tsx new file mode 100644 index 000000000000..fdd74ec8a444 --- /dev/null +++ b/ui/components/multichain/network-list-menu/network-list-search/network-list-search.tsx @@ -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 ( + + 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" + /> + + ); +}; + +export default NetworkListSearch;