diff --git a/packages/extension-ui/src/Popup/Accounts/index.tsx b/packages/extension-ui/src/Popup/Accounts/index.tsx index b938b9c4..1fbea6aa 100644 --- a/packages/extension-ui/src/Popup/Accounts/index.tsx +++ b/packages/extension-ui/src/Popup/Accounts/index.tsx @@ -3,9 +3,12 @@ import type { ThemeProps } from '../../types'; -import React, { useContext } from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; +import { AccountWithChildren } from '@polkadot/extension-base/background/types'; +import getNetworkMap from '@polkadot/extension-ui/util/getNetworkMap'; + import { AccountContext } from '../../components'; import useTranslation from '../../hooks/useTranslation'; import { Header } from '../../partials'; @@ -18,7 +21,25 @@ interface Props extends ThemeProps { function Accounts ({ className }: Props): React.ReactElement { const { t } = useTranslation(); + const [filter, setFilter] = useState(''); + const [filteredAccount, setFilteredAccount] = useState([]); const { hierarchy } = useContext(AccountContext); + const networkMap = useMemo(() => getNetworkMap(), []); + + useEffect(() => { + setFilteredAccount( + filter + ? hierarchy.filter((account) => + account.name?.toLowerCase().includes(filter) || + (account.genesisHash && networkMap.get(account.genesisHash)?.toLowerCase().includes(filter)) + ) + : hierarchy + ); + }, [filter, hierarchy, networkMap]); + + const _onFilter = useCallback((filter: string) => { + setFilter(filter.toLowerCase()); + }, []); return ( <> @@ -27,12 +48,14 @@ function Accounts ({ className }: Props): React.ReactElement { : ( <>
('Accounts')} />
- {hierarchy.map((json, index): React.ReactNode => ( + {filteredAccount.map((json, index): React.ReactNode => ( { onChange={_onChangeFilter} placeholder={t('example.com')} value={filter} + withReset />
{ diff --git a/packages/extension-ui/src/components/InputFilter.tsx b/packages/extension-ui/src/components/InputFilter.tsx index eb8717ad..5d0f7cc1 100644 --- a/packages/extension-ui/src/components/InputFilter.tsx +++ b/packages/extension-ui/src/components/InputFilter.tsx @@ -1,23 +1,34 @@ // Copyright 2017-2021 @polkadot/react-components authors & contributors // SPDX-License-Identifier: Apache-2.0 -import React, { useCallback } from 'react'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import React, { useCallback, useRef } from 'react'; import styled from 'styled-components'; +import { ThemeProps } from '../types'; import { Input } from './TextInputs'; -interface Props { +interface Props extends ThemeProps { className?: string; onChange: (filter: string) => void; placeholder: string; value: string; + withReset?: boolean; } -function InputFilter ({ className, onChange, placeholder, value }: Props) { +function InputFilter ({ className, onChange, placeholder, value, withReset = false }: Props) { + const inputRef: React.RefObject | null = useRef(null); + const onChangeFilter = useCallback((event: React.ChangeEvent) => { onChange(event.target.value); }, [onChange]); + const onResetFilter = useCallback(() => { + onChange(''); + inputRef.current && inputRef.current.select(); + }, [onChange]); + return (
+ {withReset && !!value && ( + + )}
); } -export default styled(InputFilter)` +export default styled(InputFilter)(({ theme }: Props) => ` padding-left: 1rem !important; padding-right: 1rem !important; -`; + position: relative; + + .resetIcon { + position: absolute; + right: 28px; + top: 12px; + color: ${theme.iconNeutralColor}; + cursor: pointer; + } +`); diff --git a/packages/extension-ui/src/partials/Header.tsx b/packages/extension-ui/src/partials/Header.tsx index 38ed27f0..9c2a73e7 100644 --- a/packages/extension-ui/src/partials/Header.tsx +++ b/packages/extension-ui/src/partials/Header.tsx @@ -3,30 +3,37 @@ import type { ThemeProps } from '../types'; -import { faArrowLeft, faCog, faPlusCircle } from '@fortawesome/free-solid-svg-icons'; +import { faArrowLeft, faCog, faPlusCircle, faSearch } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import React, { useCallback, useRef, useState } from 'react'; import styled from 'styled-components'; import logo from '../assets/pjs.svg'; +import InputFilter from '../components/InputFilter'; import Link from '../components/Link'; import useOutsideClick from '../hooks/useOutsideClick'; +import useTranslation from '../hooks/useTranslation'; import MenuAdd from './MenuAdd'; import MenuSettings from './MenuSettings'; interface Props extends ThemeProps { children?: React.ReactNode; className?: string; + onFilter?: (filter: string) => void; showAdd?: boolean; showBackArrow?: boolean; + showSearch?: boolean; showSettings?: boolean; smallMargin?: boolean; text?: React.ReactNode; } -function Header ({ children, className = '', showAdd, showBackArrow, showSettings, smallMargin = false, text }: Props): React.ReactElement { +function Header ({ children, className = '', onFilter, showAdd, showBackArrow, showSearch, showSettings, smallMargin = false, text }: Props): React.ReactElement { const [isAddOpen, setShowAdd] = useState(false); const [isSettingsOpen, setShowSettings] = useState(false); + const [isSearchOpen, setShowSearch] = useState(false); + const [filter, setFilter] = useState(''); + const { t } = useTranslation(); const addRef = useRef(null); const setRef = useRef(null); @@ -48,6 +55,22 @@ function Header ({ children, className = '', showAdd, showBackArrow, showSetting [] ); + const _onChangeFilter = useCallback((filter: string) => { + setFilter(filter); + onFilter && onFilter(filter); + }, [onFilter]); + + const _toggleSearch = useCallback( + (): void => { + if (isSearchOpen) { + _onChangeFilter(''); + } + + setShowSearch((isSearchOpen) => !isSearchOpen); + }, + [_onChangeFilter, isSearchOpen] + ); + return (
@@ -73,6 +96,25 @@ function Header ({ children, className = '', showAdd, showBackArrow, showSetting } {text || 'polkadot{.js}'}
+ {showSearch && ( +
+ {isSearchOpen && ( + ('Search by name or network...')} + value={filter} + withReset + /> + )} + +
+ )}
{showAdd && (
` } } - .popupMenus { - align-self: center; + .popupMenus, .searchBarWrapper { + align-self: center; + } - .popupToggle { - display: inline-block; - vertical-align: middle; - - &:last-child { - margin-right: 24px; - } + .searchBarWrapper { + flex: 1; + display: flex; + justify-content: end; + align-items: center;; + .searchIcon { + margin-right: 8px; + &:hover { cursor: pointer; } } - - .popupToggle+.popupToggle { - margin-left: 8px; + } + + .popupToggle { + display: inline-block; + vertical-align: middle; + + &:last-child { + margin-right: 24px; } + + &:hover { + cursor: pointer; + } + } + + .inputFilter { + width: 100% + } + + .popupToggle+.popupToggle { + margin-left: 8px; } } - .plusIcon, .cogIcon { + .plusIcon, .cogIcon, .searchIcon { color: ${theme.iconNeutralColor}; &.selected {