Skip to content

Commit 325775d

Browse files
committed
frontend: styled multi select for Active Currencies Dropdown / Select
to improve our UI, we'd like to style our Active Currencies multi dropdown. In this commit we also refactored it, along with the single dropdown component.
1 parent 17b231b commit 325775d

File tree

12 files changed

+239
-99
lines changed

12 files changed

+239
-99
lines changed

frontends/web/src/api/account.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ export type CoinUnit = 'BTC' | 'sat' | 'LTC' | 'ETH' | 'TBTC' | 'tsat' | 'TLTC'
2929

3030
export type ERC20TokenUnit = 'USDT' | 'USDC' | 'LINK' | 'BAT' | 'MKR' | 'ZRX' | 'WBTC' | 'PAXG' | 'SAI' | 'DAI';
3131

32+
export type FiatWithDisplayName = {
33+
currency: Fiat,
34+
displayName: string
35+
}
36+
3237
export type Terc20Token = {
3338
code: string;
3439
name: string;
Lines changed: 3 additions & 0 deletions
Loading

frontends/web/src/components/icon/icon.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import saveSVG from './assets/icons/save.svg';
5252
import saveLightSVG from './assets/icons/save-light.svg';
5353
import starSVG from './assets/icons/star.svg';
5454
import starInactiveSVG from './assets/icons/star-inactive.svg';
55+
import selectedCheckLight from './assets/icons/selected-check-light.svg';
5556
import style from './icon.module.css';
5657

5758
export const ExpandOpen = () => (
@@ -156,6 +157,8 @@ export const Save = (props: ImgProps) => (<img src={saveSVG} draggable={false} {
156157
export const SaveLight = (props: ImgProps) => (<img src={saveLightSVG} draggable={false} {...props} />);
157158
export const Star = (props: ImgProps) => (<img src={starSVG} draggable={false} {...props} />);
158159
export const StarInactive = (props: ImgProps) => (<img src={starInactiveSVG} draggable={false} {...props} />);
160+
// check component for cusotm multi-dropdown select
161+
export const SelectedCheckLight = (props: ImgProps) => (<img src={selectedCheckLight} draggable={false} {...props} />);
159162
/**
160163
* @deprecated Alert is only used for BitBox01 use `Warning` icon instead
161164
*/

frontends/web/src/components/rates/rates.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { Fiat, ConversionUnit, IAmount } from '../../api/account';
18+
import { Fiat, ConversionUnit, IAmount, FiatWithDisplayName } from '../../api/account';
1919
import { BtcUnit } from '../../api/coins';
2020
import { reinitializeAccounts } from '../../api/backend';
2121
import { share } from '../../decorators/share';
@@ -34,6 +34,25 @@ export interface SharedProps {
3434
}
3535

3636
export const currencies: Fiat[] = ['AUD', 'BRL', 'CAD', 'CHF', 'CNY', 'EUR', 'GBP', 'HKD', 'ILS', 'JPY', 'KRW', 'NOK', 'RUB', 'SEK', 'SGD', 'USD', 'BTC'];
37+
export const currenciesWithDisplayName: FiatWithDisplayName[] = [
38+
{ currency: 'AUD', displayName: 'Australian dollar' },
39+
{ currency: 'BRL', displayName: 'Brazilian real' },
40+
{ currency: 'CAD', displayName: 'Canadian dollar' },
41+
{ currency: 'CHF', displayName: 'Swiss franc' },
42+
{ currency: 'CNY', displayName: 'Chinese yuan' },
43+
{ currency: 'EUR', displayName: 'Euro' },
44+
{ currency: 'GBP', displayName: 'British pound' },
45+
{ currency: 'HKD', displayName: 'Hong Kong dollar' },
46+
{ currency: 'ILS', displayName: 'Israeli new shekel' },
47+
{ currency: 'JPY', displayName: 'Japanese yen' },
48+
{ currency: 'KRW', displayName: 'South Korean won' },
49+
{ currency: 'NOK', displayName: 'Norwegian krone' },
50+
{ currency: 'RUB', displayName: 'Russian ruble' },
51+
{ currency: 'SEK', displayName: 'Swedish krona' },
52+
{ currency: 'SGD', displayName: 'Singapore dollar' },
53+
{ currency: 'USD', displayName: 'United States dollar' },
54+
{ currency: 'BTC', displayName: 'Bitcoin' }
55+
];
3756

3857
export const store = new Store<SharedProps>({
3958
active: 'USD',

frontends/web/src/routes/new-settings/components/appearance/activeCurrenciesDropdownSetting.tsx

Lines changed: 4 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -14,77 +14,20 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { useEffect, useState } from 'react';
18-
import Select, { ActionMeta, MultiValue, MultiValueRemoveProps, components } from 'react-select';
19-
import { currencies, store, selectFiat, unselectFiat, SharedProps } from '../../../../components/rates/rates';
17+
import { store, SharedProps, currenciesWithDisplayName } from '../../../../components/rates/rates';
2018
import { SettingsItem } from '../settingsItem/settingsItem';
21-
import { Fiat } from '../../../../api/account';
2219
import { share } from '../../../../decorators/share';
23-
24-
type SelectOption = {
25-
label: Fiat;
26-
value: Fiat;
27-
}
28-
29-
type TSelectProps = {
30-
options: SelectOption[];
31-
} & SharedProps;
32-
33-
const ReactSelect = ({ options, active, selected }: TSelectProps) => {
34-
const [selectedCurrencies, setSelectedCurrencies] = useState<SelectOption[]>([]);
35-
36-
useEffect(() => {
37-
if (selected.length > 0) {
38-
const formattedSelectedCurrencies = selected.map(currency => ({ label: currency, value: currency }));
39-
setSelectedCurrencies(formattedSelectedCurrencies);
40-
}
41-
}, [selected]);
42-
43-
const MultiValueRemove = (props: MultiValueRemoveProps<SelectOption>) => {
44-
const currency = props.data.value;
45-
return (
46-
currency !== active ?
47-
<components.MultiValueRemove {...props}>
48-
{'X'}
49-
</components.MultiValueRemove>
50-
: null
51-
);
52-
};
53-
54-
return (
55-
<Select
56-
classNamePrefix="react-select"
57-
isSearchable
58-
isClearable={false}
59-
components={{ MultiValueRemove }}
60-
isMulti
61-
value={selectedCurrencies}
62-
onChange={(selectedFiats: MultiValue<SelectOption>, meta: ActionMeta<SelectOption>) => {
63-
switch (meta.action) {
64-
case 'remove-value':
65-
if (selectedFiats.length > 0) {
66-
const unselectedFiat = meta.removedValue.value;
67-
unselectFiat(unselectedFiat);
68-
}
69-
break;
70-
case 'select-option':
71-
const selectedFiat = selectedFiats[selectedFiats.length - 1].value as Fiat;
72-
selectFiat(selectedFiat);
73-
}
74-
}}
75-
options={options}
76-
/>);
77-
};
20+
import { ActiveCurrenciesDropdown } from '../dropdowns/activecurrenciesdropdown';
7821

7922
const ActiveCurrenciesDropdownSetting = ({ selected, active }: SharedProps) => {
80-
const formattedCurrencies = currencies.map((currency) => ({ label: currency, value: currency }));
23+
const formattedCurrencies = currenciesWithDisplayName.map((fiat) => ({ label: `${fiat.displayName} (${fiat.currency})`, value: fiat.currency }));
8124
return (
8225
<SettingsItem
8326
collapseOnSmall
8427
settingName="Active Currencies"
8528
secondaryText="These additional currencies can be toggled through on your account page."
8629
extraComponent={
87-
<ReactSelect
30+
<ActiveCurrenciesDropdown
8831
options={formattedCurrencies}
8932
active={active}
9033
selected={selected}

frontends/web/src/routes/new-settings/components/appearance/defaultCurrencyDropdownSetting.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { currencies, selectFiat, setActiveFiat, store } from '../../../../components/rates/rates';
18-
import { SingleDropdown } from '../singledropdown/singledropdown';
17+
import { currenciesWithDisplayName, selectFiat, setActiveFiat, store } from '../../../../components/rates/rates';
18+
import { SingleDropdown } from '../dropdowns/singledropdown';
1919
import { SettingsItem } from '../settingsItem/settingsItem';
2020
import { Fiat } from '../../../../api/account';
2121

2222
export const DefaultCurrencyDropdownSetting = () => {
23-
const formattedCurrencies = currencies.map((currency) => ({ label: currency, value: currency }));
24-
23+
const formattedCurrencies = currenciesWithDisplayName.map((fiat) => ({ label: `${fiat.displayName} (${fiat.currency})`, value: fiat.currency }));
24+
const defaultValueLabel = currenciesWithDisplayName.find(fiat => fiat.currency === store.state.active)?.displayName;
2525
return (
2626
<SettingsItem
2727
settingName="Default Currency"
@@ -36,7 +36,10 @@ export const DefaultCurrencyDropdownSetting = () => {
3636
selectFiat(fiat);
3737
}
3838
}}
39-
defaultValue={{ label: store.state.active, value: store.state.active }}
39+
defaultValue={{
40+
label: defaultValueLabel ? `${defaultValueLabel} (${store.state.active})` : store.state.active,
41+
value: store.state.active
42+
}}
4043
/>
4144
}
4245
/>

frontends/web/src/routes/new-settings/components/appearance/languageDropdownSetting.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { SettingsItem } from '../settingsItem/settingsItem';
1818
import { useTranslation } from 'react-i18next';
1919
import { TLanguagesList } from '../../../../components/language/types';
2020
import { getSelectedIndex } from '../../../../utils/language';
21-
import { SingleDropdown } from '../singledropdown/singledropdown';
21+
import { SingleDropdown } from '../dropdowns/singledropdown';
2222

2323
const defaultLanguages: TLanguagesList = [
2424
{ code: 'ar', display: 'العربية' },
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
.select :global(.react-select__input-container) {
2+
position: absolute;
3+
top: -2px;
4+
left: 6px;
5+
}
6+
7+
.select :global(.react-select__option) {
8+
display: flex;
9+
flex-direction: row;
10+
align-items: center;
11+
justify-content: space-between;
12+
}
13+
14+
/*displays only first 3 currencies, the rest gets hidden*/
15+
.select :global(.react-select__multi-value):nth-child(n + 4) {
16+
display: none;
17+
}
18+
19+
20+
/*
21+
displays ', ' as a pseudo component for all currency component
22+
except the last displayed component (the 3rd currency component).
23+
24+
The reason for :nth-last-child(2) instead of just :last-child is because there's an extra
25+
component created by react-select after the actual last currency component.
26+
27+
So, we're targetting the second to last component here,
28+
which is the last selected currency in the DOM.
29+
*/
30+
.select:not(.hideMultiSelect) :global(.react-select__multi-value):not(:nth-last-child(2))::after {
31+
content: ', ';
32+
}
33+
34+
/*
35+
displays '...' as a pseudo component of the 3rd currency and only when there's more than 3 selected.
36+
:nth-last-child(2) is used for the same reason as above
37+
*/
38+
.select:not(.hideMultiSelect) :global(.react-select__multi-value):nth-child(3):not(:nth-last-child(2))::after {
39+
content: ' ...';
40+
}
41+
42+
.select.hideMultiSelect :global(.react-select__multi-value) {
43+
display: none;
44+
}
45+
46+
.select :global(.react-select__value-container--is-multi) {
47+
height: var(--item-height-xsmall);
48+
}
49+
50+
.defaultCurrency:hover {
51+
cursor: not-allowed;
52+
}
53+
54+
.defaultLabel {
55+
font-size: var(--size-small);
56+
margin: 0;
57+
text-transform: capitalize;
58+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { useEffect, useState } from 'react';
2+
import Select, { ActionMeta, DropdownIndicatorProps, OptionProps, components } from 'react-select';
3+
import { selectFiat, unselectFiat, SharedProps } from '../../../../components/rates/rates';
4+
import { Fiat } from '../../../../api/account';
5+
import { SelectedCheckLight } from '../../../../components/icon';
6+
import dropdownStyles from './dropdowns.module.css';
7+
import activeCurrenciesDropdownStyle from './activecurrenciesdropdown.module.css';
8+
import { useTranslation } from 'react-i18next';
9+
10+
type SelectOption = {
11+
label: String;
12+
value: Fiat;
13+
}
14+
15+
type TSelectProps = {
16+
options: SelectOption[];
17+
} & SharedProps;
18+
19+
// a multi-select dropdown
20+
export const ActiveCurrenciesDropdown = ({
21+
options,
22+
active: defaultCurrency, // active here actually means default, thus aliasing it
23+
selected
24+
}: TSelectProps) => {
25+
const [selectedCurrencies, setSelectedCurrencies] = useState<SelectOption[]>([]);
26+
const [search, setSearch] = useState('');
27+
const { t } = useTranslation();
28+
29+
useEffect(() => {
30+
if (selected.length > 0) {
31+
const formattedSelectedCurrencies = selected.map(currency => ({ label: currency, value: currency }));
32+
setSelectedCurrencies(formattedSelectedCurrencies);
33+
}
34+
}, [selected]);
35+
36+
const DropdownIndicator = (props: DropdownIndicatorProps<SelectOption, true>) => {
37+
return (
38+
<components.DropdownIndicator {...props}>
39+
<div className={dropdownStyles.dropdown} />
40+
</components.DropdownIndicator>
41+
);
42+
};
43+
44+
const Option = (props: OptionProps<SelectOption, true>) => {
45+
const { label, value } = props.data;
46+
const selected = selectedCurrencies.findIndex(currency => currency.value === value) >= 0;
47+
const isDefaultCurrency = defaultCurrency === value;
48+
return (
49+
<components.Option {...props} className={`${isDefaultCurrency ? activeCurrenciesDropdownStyle.defaultCurrency : ''}`}>
50+
<span>{label}</span>
51+
{isDefaultCurrency ? <p className={activeCurrenciesDropdownStyle.defaultLabel}>{t('fiat.default')}</p> : null}
52+
{selected && !isDefaultCurrency ? <SelectedCheckLight /> : null}
53+
</components.Option>
54+
);
55+
};
56+
return (
57+
<Select
58+
className={`
59+
${dropdownStyles.select}
60+
${activeCurrenciesDropdownStyle.select}
61+
${search.length > 0 ? activeCurrenciesDropdownStyle.hideMultiSelect : ''}
62+
`}
63+
classNamePrefix="react-select"
64+
isSearchable
65+
isClearable={false}
66+
components={{ DropdownIndicator, IndicatorSeparator: () => null, MultiValueRemove: () => null, Option }}
67+
isMulti
68+
closeMenuOnSelect={false}
69+
hideSelectedOptions={false}
70+
value={[...selectedCurrencies].reverse()}
71+
onInputChange={(newValue) => setSearch(newValue)}
72+
onChange={(_, meta: ActionMeta<SelectOption>) => {
73+
switch (meta.action) {
74+
case 'select-option':
75+
if (meta.option) {
76+
selectFiat(meta.option.value);
77+
}
78+
break;
79+
case 'deselect-option':
80+
if (meta.option && meta.option.value !== defaultCurrency) {
81+
unselectFiat(meta.option.value);
82+
}
83+
}
84+
85+
}}
86+
options={options}
87+
/>);
88+
};

0 commit comments

Comments
 (0)