mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-03-03 05:55:03 +01:00
fix(suite): scroll to selected option in select
This commit is contained in:
@@ -1,18 +1,5 @@
|
||||
import {
|
||||
ComponentProps,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import ReactSelect, {
|
||||
Props as ReactSelectProps,
|
||||
SelectInstance,
|
||||
StylesConfig,
|
||||
components as reactSelectComponents,
|
||||
} from 'react-select';
|
||||
import { ReactNode, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import ReactSelect, { Props as ReactSelectProps, SelectInstance, StylesConfig } from 'react-select';
|
||||
|
||||
import styled, { CSSObject, DefaultTheme, css, useTheme } from 'styled-components';
|
||||
|
||||
@@ -26,6 +13,11 @@ import {
|
||||
zIndices,
|
||||
} from '@trezor/theme';
|
||||
|
||||
import { FrameProps } from '../../../utils/frameProps';
|
||||
import { TransientProps } from '../../../utils/transientProps';
|
||||
import { useElevation } from '../../ElevationContext/ElevationContext';
|
||||
import { DROPDOWN_MENU, menuStyle } from '../../Menu/menuStyle';
|
||||
import { Spinner } from '../../loaders/Spinner/Spinner';
|
||||
import {
|
||||
FormCell,
|
||||
FormCellProps,
|
||||
@@ -43,11 +35,6 @@ import {
|
||||
} from './customComponents';
|
||||
import { useDetectPortalTarget } from './useDetectPortalTarget';
|
||||
import { useOnKeyDown } from './useOnKeyDown';
|
||||
import { FrameProps } from '../../../utils/frameProps';
|
||||
import { TransientProps } from '../../../utils/transientProps';
|
||||
import { useElevation } from '../../ElevationContext/ElevationContext';
|
||||
import { DROPDOWN_MENU, menuStyle } from '../../Menu/menuStyle';
|
||||
import { Spinner } from '../../loaders/Spinner/Spinner';
|
||||
|
||||
const reactSelectClassNamePrefix = 'react-select';
|
||||
|
||||
@@ -303,25 +290,6 @@ export type SelectProps = KeyPressScrollProps &
|
||||
'data-testid'?: string;
|
||||
};
|
||||
|
||||
type MenuProps = ComponentProps<typeof reactSelectComponents.Menu> & {
|
||||
selectRef: React.RefObject<SelectInstance<Option, boolean>>;
|
||||
scrollIndex?: number;
|
||||
};
|
||||
|
||||
const Menu = ({ selectRef, scrollIndex, ...rest }: MenuProps) => {
|
||||
useEffect(() => {
|
||||
if (scrollIndex) {
|
||||
const menuList = selectRef.current?.menuListRef;
|
||||
const option = menuList?.children[scrollIndex];
|
||||
option?.scrollIntoView({ behavior: 'instant' });
|
||||
}
|
||||
}, [selectRef, scrollIndex]);
|
||||
|
||||
return <reactSelectComponents.Menu {...rest} />;
|
||||
};
|
||||
|
||||
const OPTION_INDEX_NAME = 'optionIndex';
|
||||
|
||||
export const Select = ({
|
||||
isClean = false,
|
||||
label,
|
||||
@@ -346,34 +314,14 @@ export const Select = ({
|
||||
const formCellProps = pickFormCellProps(rest);
|
||||
const { isDisabled } = formCellProps;
|
||||
const isRenderedInModal = menuPortalTarget !== null;
|
||||
const [scrollIndex, setScrollIndex] = useState<number>(0);
|
||||
|
||||
const isGrouped =
|
||||
Array.isArray(rest.options) && rest.options.some(opt => 'label' in opt && 'options' in opt);
|
||||
|
||||
const indexedOptions = useMemo(() => {
|
||||
if (isGrouped) {
|
||||
return rest.options?.map((group, index) => ({
|
||||
...group,
|
||||
options: group.options.map((option: Option) => ({
|
||||
...option,
|
||||
[OPTION_INDEX_NAME]: index,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
return rest.options?.map((option, index) => ({ ...option, [OPTION_INDEX_NAME]: index }));
|
||||
}, [rest.options, isGrouped]);
|
||||
const [selectedOption, setSelectedOption] = useState<Option | undefined>(rest.value);
|
||||
|
||||
const handleOnChange = useCallback<Required<ReactSelectProps<Option>>['onChange']>(
|
||||
(value, { action }) => {
|
||||
if (value) {
|
||||
const optionIndex = value[OPTION_INDEX_NAME] as number;
|
||||
const option = { ...value };
|
||||
delete option[OPTION_INDEX_NAME];
|
||||
|
||||
onChange?.(option as Option, selectRef.current);
|
||||
setScrollIndex(optionIndex);
|
||||
onChange?.(value, selectRef.current);
|
||||
setSelectedOption(value);
|
||||
|
||||
if (!isMenuOpen && action === 'select-option') {
|
||||
selectRef.current?.blur();
|
||||
@@ -385,11 +333,6 @@ export const Select = ({
|
||||
[onChange, isMenuOpen],
|
||||
);
|
||||
|
||||
const MenuWithProps = useCallback(
|
||||
(props: any) => <Menu {...props} selectRef={selectRef} scrollIndex={scrollIndex} />,
|
||||
[selectRef, scrollIndex],
|
||||
);
|
||||
|
||||
/**
|
||||
* This memoization is necessary to prevent jumping of the Select
|
||||
* to the corder. This may happen when parent component re-renders
|
||||
@@ -401,13 +344,12 @@ export const Select = ({
|
||||
<Control {...controlProps} data-testid={dataTest} />
|
||||
),
|
||||
Option: (optionProps: OptionComponentProps) => (
|
||||
<Option {...optionProps} data-testid={dataTest} />
|
||||
<Option {...optionProps} data-testid={dataTest} selectedOption={selectedOption} />
|
||||
),
|
||||
GroupHeading,
|
||||
Menu: MenuWithProps,
|
||||
...components,
|
||||
}),
|
||||
[components, dataTest, MenuWithProps],
|
||||
[components, dataTest, selectedOption],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -442,7 +384,6 @@ export const Select = ({
|
||||
placeholder={placeholder || ''}
|
||||
{...rest}
|
||||
components={memoizedComponents}
|
||||
options={indexedOptions}
|
||||
/>
|
||||
|
||||
{isLoading && (
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { useRef } from 'react';
|
||||
import { ControlProps, GroupHeadingProps, OptionProps, components } from 'react-select';
|
||||
import { useDeepCompareEffect } from 'react-use';
|
||||
|
||||
import { deepEqual } from '@trezor/utils';
|
||||
|
||||
import type { Option as OptionType } from './Select';
|
||||
|
||||
@@ -22,23 +26,37 @@ export const Control = ({ 'data-testid': dataTest, ...controlProps }: ControlCom
|
||||
|
||||
export interface OptionComponentProps extends OptionProps<OptionType, boolean> {
|
||||
'data-testid'?: string;
|
||||
selectedOption?: any;
|
||||
}
|
||||
|
||||
export const Option = ({ 'data-testid': dataTest, ...optionProps }: OptionComponentProps) => (
|
||||
<components.Option
|
||||
{...optionProps}
|
||||
innerProps={
|
||||
{
|
||||
...optionProps.innerProps,
|
||||
'data-testid': `${dataTest}/option/${
|
||||
typeof optionProps.data.value === 'string'
|
||||
? optionProps.data.value
|
||||
: optionProps.label
|
||||
}`,
|
||||
} as OptionProps<OptionType, boolean>['innerProps']
|
||||
export const Option = ({
|
||||
selectedOption,
|
||||
'data-testid': dataTest,
|
||||
...props
|
||||
}: OptionComponentProps) => {
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
if (deepEqual(props.data, selectedOption)) {
|
||||
ref.current?.scrollIntoView();
|
||||
}
|
||||
/>
|
||||
);
|
||||
}, [{}]);
|
||||
|
||||
return (
|
||||
<components.Option
|
||||
{...props}
|
||||
innerRef={ref as any}
|
||||
innerProps={
|
||||
{
|
||||
...props.innerProps,
|
||||
'data-testid': `${dataTest}/option/${
|
||||
typeof props.data.value === 'string' ? props.data.value : props.label
|
||||
}`,
|
||||
} as OptionProps<OptionType, boolean>['innerProps']
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const GroupHeading = (groupHeadingProps: GroupHeadingProps<OptionType>) => (
|
||||
<components.GroupHeading {...groupHeadingProps} />
|
||||
|
||||
@@ -3,12 +3,17 @@ import { createFilter } from 'react-select';
|
||||
|
||||
import { FiatCurrencyCode } from 'invity-api';
|
||||
|
||||
import type { TradingExchangeFormProps, TradingSellFormProps } from '@suite-common/trading';
|
||||
import {
|
||||
type TradingExchangeFormProps,
|
||||
type TradingSellFormProps,
|
||||
selectTradingLoadingAndTimestamp,
|
||||
} from '@suite-common/trading';
|
||||
import { Row, Select, Text } from '@trezor/components';
|
||||
import { spacings } from '@trezor/theme';
|
||||
|
||||
import { Translation } from 'src/components/suite';
|
||||
import { AccountTypeBadge } from 'src/components/suite/AccountTypeBadge';
|
||||
import { useSelector } from 'src/hooks/suite';
|
||||
import { useTradingBuildAccountGroups } from 'src/hooks/wallet/trading/form/common/useTradingBuildAccountGroups';
|
||||
import { useTradingFiatValues } from 'src/hooks/wallet/trading/form/common/useTradingFiatValues';
|
||||
import { useTradingFormContext } from 'src/hooks/wallet/trading/form/useTradingCommonForm';
|
||||
@@ -37,6 +42,8 @@ export const TradingFormInputAccount = <
|
||||
} = useTradingFormContext<TradingTradeSellExchangeType>();
|
||||
const optionGroups = useTradingBuildAccountGroups(type);
|
||||
|
||||
const { isLoading } = useSelector(selectTradingLoadingAndTimestamp);
|
||||
|
||||
const { control, getValues } = methods;
|
||||
const selectedOption = getValues(accountSelectName) as
|
||||
| TradingAccountOptionsGroupOptionProps
|
||||
@@ -53,6 +60,8 @@ export const TradingFormInputAccount = <
|
||||
render={({ field: { value } }) => (
|
||||
<Select
|
||||
value={value}
|
||||
isDisabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
labelLeft={label && <Translation id={label} />}
|
||||
options={optionGroups}
|
||||
onChange={async (selected: TradingAccountOptionsGroupOptionProps) => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
cryptoIdToNetwork,
|
||||
isCryptoIdForNativeToken,
|
||||
parseCryptoId,
|
||||
selectTradingLoadingAndTimestamp,
|
||||
tradingActions,
|
||||
useTradingInfo,
|
||||
} from '@suite-common/trading';
|
||||
@@ -29,7 +30,7 @@ import {
|
||||
import { spacings } from '@trezor/theme';
|
||||
|
||||
import { Translation } from 'src/components/suite';
|
||||
import { useDispatch, useTranslation } from 'src/hooks/suite';
|
||||
import { useDispatch, useSelector, useTranslation } from 'src/hooks/suite';
|
||||
import { useTradingFormContext } from 'src/hooks/wallet/trading/form/useTradingCommonForm';
|
||||
import {
|
||||
SelectAssetOptionProps,
|
||||
@@ -97,6 +98,8 @@ export const TradingFormInputCryptoSelect = <
|
||||
const [search, setSearch] = useState('');
|
||||
const { translationString } = useTranslation();
|
||||
|
||||
const { isLoading } = useSelector(selectTradingLoadingAndTimestamp);
|
||||
|
||||
const sendCryptoSelectValue = isTradingExchangeContext(context)
|
||||
? context.getValues()?.sendCryptoSelect?.value
|
||||
: null;
|
||||
@@ -256,7 +259,8 @@ export const TradingFormInputCryptoSelect = <
|
||||
data-testid={dataTestId ?? '@trading/form/select-crypto'}
|
||||
isClearable={false}
|
||||
isMenuOpen={false}
|
||||
isDisabled={isDisabled}
|
||||
isDisabled={isDisabled || isLoading}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,35 @@
|
||||
export const deepEqual = (a: any, b: any): boolean => {
|
||||
if (a === b) return true;
|
||||
|
||||
if (typeof a !== typeof b) return false;
|
||||
if (a === null || b === null) return false;
|
||||
if (typeof a !== 'object') return false;
|
||||
|
||||
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
||||
|
||||
if (Array.isArray(a)) {
|
||||
if (a.length !== b.length) return false;
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (!deepEqual(a[i], b[i])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const keysA = Object.keys(a);
|
||||
const keysB = Object.keys(b);
|
||||
|
||||
if (keysA.length !== keysB.length) return false;
|
||||
|
||||
for (const key of keysA) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
||||
if (!deepEqual(a[key], b[key])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isChanged = (prev?: any, current?: any, filter?: { [k: string]: string[] }) => {
|
||||
// 1. both objects are the same (solves simple types like string, boolean and number)
|
||||
if (prev === current) return false;
|
||||
|
||||
@@ -60,7 +60,6 @@ export const loadInitialDataThunk = createThunk(
|
||||
|
||||
if (isDifferentAccount || !buyInfo) {
|
||||
const buyInfoData = await dispatch(buyThunks.loadInfoThunk()).unwrap();
|
||||
|
||||
dispatch(tradingBuyActions.saveBuyInfo(buyInfoData));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user