{isBeta() ? t('betaMetamaskVersion') : t('metamaskVersion')}
diff --git a/ui/pages/settings/networks-tab/networks-list-item/networks-list-item.js b/ui/pages/settings/networks-tab/networks-list-item/networks-list-item.js
index 378afe584..8cb3ca456 100644
--- a/ui/pages/settings/networks-tab/networks-list-item/networks-list-item.js
+++ b/ui/pages/settings/networks-tab/networks-list-item/networks-list-item.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
@@ -14,7 +14,14 @@ import { getEnvironmentType } from '../../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../shared/constants/app';
import { getProvider } from '../../../../selectors';
-const NetworksListItem = ({ network, networkIsSelected, selectedRpcUrl }) => {
+import { handleHooksSettingsRefs } from '../../../../helpers/utils/settings-search';
+
+const NetworksListItem = ({
+ network,
+ networkIsSelected,
+ selectedRpcUrl,
+ networkIndex,
+}) => {
const t = useI18nContext();
const history = useHistory();
const dispatch = useDispatch();
@@ -37,9 +44,15 @@ const NetworksListItem = ({ network, networkIsSelected, selectedRpcUrl }) => {
(listItemUrlIsProviderUrl || listItemTypeIsProviderNonRpcType);
const displayNetworkListItemAsSelected =
listItemNetworkIsSelected || listItemNetworkIsCurrentProvider;
+ const settingsRefs = useRef();
+
+ useEffect(() => {
+ handleHooksSettingsRefs(t, t('networks'), settingsRefs, networkIndex);
+ }, [networkIndex, settingsRefs, t]);
return (
{
@@ -76,6 +89,7 @@ NetworksListItem.propTypes = {
network: PropTypes.object.isRequired,
networkIsSelected: PropTypes.bool,
selectedRpcUrl: PropTypes.string,
+ networkIndex: PropTypes.number,
};
export default NetworksListItem;
diff --git a/ui/pages/settings/networks-tab/networks-list/networks-list.js b/ui/pages/settings/networks-tab/networks-list/networks-list.js
index 26be55938..fa91ca798 100644
--- a/ui/pages/settings/networks-tab/networks-list/networks-list.js
+++ b/ui/pages/settings/networks-tab/networks-list/networks-list.js
@@ -16,12 +16,13 @@ const NetworksList = ({
networkIsSelected && !networkDefaultedToProvider,
})}
>
- {networksToRender.map((network) => (
+ {networksToRender.map((network, index) => (
))}
diff --git a/ui/pages/settings/security-tab/security-tab.component.js b/ui/pages/settings/security-tab/security-tab.component.js
index 05fcc9e0a..4bc7c25f3 100644
--- a/ui/pages/settings/security-tab/security-tab.component.js
+++ b/ui/pages/settings/security-tab/security-tab.component.js
@@ -3,6 +3,10 @@ import PropTypes from 'prop-types';
import ToggleButton from '../../../components/ui/toggle-button';
import { REVEAL_SEED_ROUTE } from '../../../helpers/constants/routes';
import Button from '../../../components/ui/button';
+import {
+ getSettingsSectionNumber,
+ handleSettingsRefs,
+} from '../../../helpers/utils/settings-search';
export default class SecurityTab extends PureComponent {
static contextTypes = {
@@ -21,12 +25,33 @@ export default class SecurityTab extends PureComponent {
usePhishDetect: PropTypes.bool.isRequired,
};
+ settingsRefs = Array(
+ getSettingsSectionNumber(
+ this.context.t,
+ this.context.t('securityAndPrivacy'),
+ ),
+ )
+ .fill(undefined)
+ .map(() => {
+ return React.createRef();
+ });
+
+ componentDidUpdate() {
+ const { t } = this.context;
+ handleSettingsRefs(t, t('securityAndPrivacy'), this.settingsRefs);
+ }
+
+ componentDidMount() {
+ const { t } = this.context;
+ handleSettingsRefs(t, t('securityAndPrivacy'), this.settingsRefs);
+ }
+
renderSeedWords() {
const { t } = this.context;
const { history } = this.props;
return (
-
+
{t('revealSeedWords')}
@@ -63,7 +88,7 @@ export default class SecurityTab extends PureComponent {
} = this.props;
return (
-
+
{t('participateInMetaMetrics')}
@@ -92,7 +117,7 @@ export default class SecurityTab extends PureComponent {
} = this.props;
return (
-
+
{t('showIncomingTransactions')}
@@ -120,7 +145,7 @@ export default class SecurityTab extends PureComponent {
const { usePhishDetect, setUsePhishDetect } = this.props;
return (
-
+
{t('usePhishingDetection')}
diff --git a/ui/pages/settings/settings-search-list/index.js b/ui/pages/settings/settings-search-list/index.js
new file mode 100644
index 000000000..5d5fec4be
--- /dev/null
+++ b/ui/pages/settings/settings-search-list/index.js
@@ -0,0 +1,3 @@
+import SettingsSearchList from './settings-search-list';
+
+export default SettingsSearchList;
diff --git a/ui/pages/settings/settings-search-list/settings-search-list.js b/ui/pages/settings/settings-search-list/settings-search-list.js
new file mode 100644
index 000000000..01c76fbdf
--- /dev/null
+++ b/ui/pages/settings/settings-search-list/settings-search-list.js
@@ -0,0 +1,101 @@
+import React, { useContext, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+import { highlightSearchedText } from '../../../helpers/utils/settings-search';
+import { I18nContext } from '../../../contexts/i18n';
+
+export default function SettingsSearchList({ results, onClickSetting }) {
+ const t = useContext(I18nContext);
+
+ useEffect(() => {
+ results.forEach((_, i) => {
+ highlightSearchedText(i);
+ });
+ }, [results]);
+
+ return (
+
+ {Array(5)
+ .fill(undefined)
+ .map((_, i) => {
+ const { image, tab, section } = results[i] || {};
+
+ return (
+ Boolean(image || tab || section) && (
+
+
onClickSetting(results[i])}
+ >
+

+
+
+
+
+
+
+ )
+ );
+ })}
+ {results.length === 0 && (
+
+
+ {t('settingsSearchMatchingNotFound')}
+
+
+ )}
+
+
+ );
+}
+
+SettingsSearchList.propTypes = {
+ results: PropTypes.array,
+ onClickSetting: PropTypes.func,
+};
diff --git a/ui/pages/settings/settings-search/index.js b/ui/pages/settings/settings-search/index.js
new file mode 100644
index 000000000..302b3e22e
--- /dev/null
+++ b/ui/pages/settings/settings-search/index.js
@@ -0,0 +1,3 @@
+import SettingsSearch from './settings-search';
+
+export default SettingsSearch;
diff --git a/ui/pages/settings/settings-search/settings-search.js b/ui/pages/settings/settings-search/settings-search.js
new file mode 100644
index 000000000..e580d39ca
--- /dev/null
+++ b/ui/pages/settings/settings-search/settings-search.js
@@ -0,0 +1,105 @@
+import React, { useState, useContext } from 'react';
+import PropTypes from 'prop-types';
+import Fuse from 'fuse.js';
+import InputAdornment from '@material-ui/core/InputAdornment';
+import TextField from '../../../components/ui/text-field';
+import { isEqualCaseInsensitive } from '../../../helpers/utils/util';
+import { I18nContext } from '../../../contexts/i18n';
+import SearchIcon from '../../../components/ui/search-icon';
+
+export default function SettingsSearch({
+ onSearch,
+ error,
+ settingsRoutesList,
+}) {
+ const t = useContext(I18nContext);
+
+ const [searchQuery, setSearchQuery] = useState('');
+ const [searchIconColor, setSearchIconColor] = useState('#9b9b9b');
+
+ const settingsRoutesListArray = Object.values(settingsRoutesList);
+ const settingsSearchFuse = new Fuse(settingsRoutesListArray, {
+ shouldSort: true,
+ threshold: 0.2,
+ location: 0,
+ distance: 100,
+ maxPatternLength: 32,
+ minMatchCharLength: 1,
+ keys: ['tab', 'section', 'description'],
+ });
+
+ // eslint-disable-next-line no-shadow
+ const handleSearch = (searchQuery) => {
+ setSearchQuery(searchQuery);
+ if (searchQuery === '') {
+ setSearchIconColor('#9b9b9b');
+ } else {
+ setSearchIconColor('#24292E');
+ }
+ const fuseSearchResult = settingsSearchFuse.search(searchQuery);
+ const addressSearchResult = settingsRoutesListArray.filter((routes) => {
+ return (
+ routes.tab &&
+ searchQuery &&
+ isEqualCaseInsensitive(routes.tab, searchQuery)
+ );
+ });
+
+ const results = [...addressSearchResult, ...fuseSearchResult];
+ onSearch({ searchQuery, results });
+ };
+
+ const renderStartAdornment = () => {
+ return (
+
+
+
+ );
+ };
+
+ const renderEndAdornment = () => {
+ return (
+ <>
+ {searchQuery && (
+
handleSearch('')}
+ style={{ cursor: 'pointer' }}
+ >
+
+
+ )}
+ >
+ );
+ };
+
+ return (
+
handleSearch(e.target.value)}
+ error={error}
+ fullWidth
+ autoFocus
+ autoComplete="off"
+ style={{ backgroundColor: '#fff' }}
+ startAdornment={renderStartAdornment()}
+ endAdornment={renderEndAdornment()}
+ />
+ );
+}
+
+SettingsSearch.propTypes = {
+ onSearch: PropTypes.func,
+ error: PropTypes.string,
+ settingsRoutesList: PropTypes.array,
+};
diff --git a/ui/pages/settings/settings-search/settings-search.stories.js b/ui/pages/settings/settings-search/settings-search.stories.js
new file mode 100644
index 000000000..cd45c49c1
--- /dev/null
+++ b/ui/pages/settings/settings-search/settings-search.stories.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import SettingsSearch from './settings-search';
+
+export default {
+ title: 'Pages/Settings/SettingsSearch',
+ id: __filename,
+};
+
+export const SettingsSearchComponent = () => {
+ return ;
+};
diff --git a/ui/pages/settings/settings-tab/settings-tab.component.js b/ui/pages/settings/settings-tab/settings-tab.component.js
index 0836d428b..5dca12135 100644
--- a/ui/pages/settings/settings-tab/settings-tab.component.js
+++ b/ui/pages/settings/settings-tab/settings-tab.component.js
@@ -10,6 +10,11 @@ import Jazzicon from '../../../components/ui/jazzicon';
import BlockieIdenticon from '../../../components/ui/identicon/blockieIdenticon';
import Typography from '../../../components/ui/typography';
+import {
+ getSettingsSectionNumber,
+ handleSettingsRefs,
+} from '../../../helpers/utils/settings-search';
+
const sortedCurrencies = availableCurrencies.sort((a, b) => {
return a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase());
});
@@ -53,6 +58,24 @@ export default class SettingsTab extends PureComponent {
tokenList: PropTypes.object,
};
+ settingsRefs = Array(
+ getSettingsSectionNumber(this.context.t, this.context.t('general')),
+ )
+ .fill(undefined)
+ .map(() => {
+ return React.createRef();
+ });
+
+ componentDidUpdate() {
+ const { t } = this.context;
+ handleSettingsRefs(t, t('general'), this.settingsRefs);
+ }
+
+ componentDidMount() {
+ const { t } = this.context;
+ handleSettingsRefs(t, t('general'), this.settingsRefs);
+ }
+
renderCurrentConversion() {
const { t } = this.context;
const {
@@ -62,7 +85,7 @@ export default class SettingsTab extends PureComponent {
} = this.props;
return (
-
+
{t('currencyConversion')}
@@ -96,7 +119,7 @@ export default class SettingsTab extends PureComponent {
const currentLocaleName = currentLocaleMeta ? currentLocaleMeta.name : '';
return (
-
+
{t('currentLanguage')}
@@ -124,7 +147,11 @@ export default class SettingsTab extends PureComponent {
const { hideZeroBalanceTokens, setHideZeroBalanceTokens } = this.props;
return (
-
+
{t('hideZeroBalanceTokens')}
@@ -160,7 +187,11 @@ export default class SettingsTab extends PureComponent {
});
return (
-
+
{t('accountIdenticon')}
@@ -238,7 +269,7 @@ export default class SettingsTab extends PureComponent {
} = this.props;
return (
-
+
{t('primaryCurrencySetting')}
diff --git a/ui/pages/settings/settings-tab/settings-tab.container.test.js b/ui/pages/settings/settings-tab/settings-tab.component.test.js
similarity index 100%
rename from ui/pages/settings/settings-tab/settings-tab.container.test.js
rename to ui/pages/settings/settings-tab/settings-tab.component.test.js
diff --git a/ui/pages/settings/settings.component.js b/ui/pages/settings/settings.component.js
index 7d6f37458..4274ee4ea 100644
--- a/ui/pages/settings/settings.component.js
+++ b/ui/pages/settings/settings.component.js
@@ -22,6 +22,8 @@ import {
EXPERIMENTAL_ROUTE,
ADD_NETWORK_ROUTE,
} from '../../helpers/constants/routes';
+import { getSettingsRoutes } from '../../helpers/utils/settings-search';
+
import SettingsTab from './settings-tab';
import AlertsTab from './alerts-tab';
import NetworksTab from './networks-tab';
@@ -34,6 +36,8 @@ import ExperimentalTab from './experimental-tab';
import SnapListTab from './flask/snaps-list-tab';
import ViewSnap from './flask/view-snap';
///: END:ONLY_INCLUDE_IN
+import SettingsSearch from './settings-search';
+import SettingsSearchList from './settings-search-list';
class SettingsPage extends PureComponent {
static propTypes = {
@@ -59,6 +63,9 @@ class SettingsPage extends PureComponent {
state = {
lastFetchedConversionDate: null,
+ searchResults: [],
+ isSearchList: false,
+ searchText: '',
};
componentDidMount() {
@@ -76,6 +83,15 @@ class SettingsPage extends PureComponent {
}
}
+ handleClickSetting(setting) {
+ const { history } = this.props;
+ history.push(setting.route);
+ this.setState({
+ searchResults: '',
+ isSearchList: '',
+ });
+ }
+
render() {
const {
history,
@@ -85,6 +101,10 @@ class SettingsPage extends PureComponent {
addNewNetwork,
isSnapViewPage,
} = this.props;
+
+ const { searchResults, isSearchList, searchText } = this.state;
+ const { t } = this.context;
+
return (
history.push(backRoute)}
/>
)}
- {this.renderTitle()}
-
{
- if (addNewNetwork) {
- history.push(NETWORKS_ROUTE);
- } else {
- history.push(mostRecentOverviewPage);
- }
- }}
- />
+
+ {this.renderTitle()}
+
+
{
+ if (addNewNetwork) {
+ history.push(NETWORKS_ROUTE);
+ } else {
+ history.push(mostRecentOverviewPage);
+ }
+ }}
+ />
+
+
+
+ {
+ this.setState({
+ searchResults: results,
+ isSearchList: searchQuery !== '',
+ searchText: searchQuery,
+ });
+ }}
+ settingsRoutesList={getSettingsRoutes(t)}
+ />
+ {isSearchList && searchText.length >= 2 && (
+ this.handleClickSetting(setting)}
+ />
+ )}
+
+
{this.renderTabs()}
@@ -142,7 +186,11 @@ class SettingsPage extends PureComponent {
titleText = t('settings');
}
- return
{titleText}
;
+ return (
+
+ {titleText}
+
+ );
}
renderSubHeader() {
diff --git a/ui/pages/settings/settings.component.test.js b/ui/pages/settings/settings.component.test.js
new file mode 100644
index 000000000..f2b8c6f37
--- /dev/null
+++ b/ui/pages/settings/settings.component.test.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import TextField from '../../components/ui/text-field';
+import Settings from './settings.container';
+import SettingsSearch from './settings-search';
+
+describe('SettingsPage', () => {
+ let wrapper;
+
+ const props = {
+ isAddressEntryPage: false,
+ backRoute: '/',
+ currentPath: '/settings',
+ location: '/settings',
+ mostRecentOverviewPage: '',
+ isPopup: false,
+ pathnameI18nKey: undefined,
+ addressName: '',
+ initialBreadCrumbRoute: undefined,
+ initialBreadCrumbKey: undefined,
+ addNewNetwork: false,
+ conversionDate: Date.now(),
+ };
+
+ beforeEach(() => {
+ wrapper = shallow(
, {
+ context: {
+ t: (str) => str,
+ },
+ });
+ });
+
+ it('should render title correctly', () => {
+ expect(
+ wrapper.find('.settings-page__header__title-container__title').text(),
+ ).toStrictEqual('settings');
+ });
+
+ it('should render search correctly', () => {
+ wrapper = shallow(
+
undefined} settingsRoutesList={[]} />,
+ {
+ context: {
+ t: (s) => `${s}`,
+ },
+ },
+ );
+
+ expect(wrapper.find(TextField).props().id).toStrictEqual('search-settings');
+ expect(wrapper.find(TextField).props().value).toStrictEqual('');
+ });
+});