Skip to content

Commit 9a8bd51

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 e909bbb commit 9a8bd51

File tree

13 files changed

+241
-107
lines changed

13 files changed

+241
-107
lines changed

frontends/web/src/components/accountselector/accountselector.module.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
}
5151

5252
.select :global(.react-select__option--is-selected) .balance {
53-
color: var(--color-alt);
53+
color: var(--color-default);
5454
}
5555

5656
.select :global(.react-select__control) {
@@ -81,7 +81,7 @@
8181
}
8282

8383
.select :global(.react-select__option--is-selected) .selectLabelText {
84-
color: var(--color-alt);
84+
color: var(--color-default);
8585
}
8686

8787
.valueContainer > img {
Lines changed: 3 additions & 0 deletions
Loading

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

Lines changed: 2 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 selectedCheckLightSVG from './assets/icons/selected-check-light.svg';
5556
import style from './icon.module.css';
5657

5758
export const ExpandOpen = () => (
@@ -156,6 +157,7 @@ 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+
export const SelectedCheckLight = (props: ImgProps) => (<img src={selectedCheckLightSVG} draggable={false} {...props} />);
159161
/**
160162
* @deprecated Alert is only used for BitBox01 use `Warning` icon instead
161163
*/

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,31 @@ export interface SharedProps {
3333
btcUnit?: BtcUnit;
3434
}
3535

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

3862
export const store = new Store<SharedProps>({
3963
active: 'USD',
@@ -179,4 +203,9 @@ function Conversion({
179203
);
180204
}
181205

206+
export const formattedCurrencies = currenciesWithDisplayName.map((fiat) => ({ label: `${fiat.displayName} (${fiat.currency})`, value: fiat.currency }));
207+
208+
const valueLabel = currenciesWithDisplayName.find(fiat => fiat.currency === store.state.active)?.displayName;
209+
export const defaultValueLabel = valueLabel ? `${valueLabel} (${store.state.active})` : store.state.active;
210+
182211
export const FiatConversion = share<SharedProps, TProvidedProps>(store)(Conversion);

frontends/web/src/routes/buy/components/countryselect.module.css

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,6 @@
6161
margin-left: 6px;
6262
}
6363

64-
.select :global(.react-select__option--is-selected) .selectLabelText {
65-
color: var(--color-alt);
66-
}
67-
6864
.singleValueContainer {
6965
align-items: center;
7066
display: flex;

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

Lines changed: 3 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -14,77 +14,19 @@
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, formattedCurrencies } 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 }));
8123
return (
8224
<SettingsItem
8325
collapseOnSmall
8426
settingName="Active Currencies"
8527
secondaryText="These additional currencies can be toggled through on your account page."
8628
extraComponent={
87-
<ReactSelect
29+
<ActiveCurrenciesDropdown
8830
options={formattedCurrencies}
8931
active={active}
9032
selected={selected}

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@
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 { defaultValueLabel, formattedCurrencies, 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-
2523
return (
2624
<SettingsItem
2725
settingName="Default Currency"
@@ -36,7 +34,10 @@ export const DefaultCurrencyDropdownSetting = () => {
3634
selectFiat(fiat);
3735
}
3836
}}
39-
defaultValue={{ label: store.state.active, value: store.state.active }}
37+
defaultValue={{
38+
label: defaultValueLabel,
39+
value: store.state.active
40+
}}
4041
/>
4142
}
4243
/>

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: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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+
padding-right: 2px;
33+
}
34+
35+
/*
36+
displays '...' as a pseudo component of the 3rd currency and only when there's more than 3 selected.
37+
:nth-last-child(2) is used for the same reason as above
38+
*/
39+
.select:not(.hideMultiSelect) :global(.react-select__multi-value):nth-child(3):not(:nth-last-child(2))::after {
40+
content: '\002026';
41+
}
42+
43+
.select.hideMultiSelect :global(.react-select__multi-value) {
44+
display: none;
45+
}
46+
47+
.select :global(.react-select__value-container--is-multi) {
48+
height: var(--item-height-xsmall);
49+
}
50+
51+
.defaultCurrency:hover {
52+
cursor: not-allowed;
53+
}
54+
55+
.defaultLabel {
56+
font-size: var(--size-small);
57+
margin: 0;
58+
text-transform: capitalize;
59+
}
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)