Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tw 565 tech unify select component for all selects in app #913

Merged
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5baadb1
Added new select to settings
herkoss Jun 2, 2023
68def48
TW-565 WIP Added padding on component
herkoss Jun 6, 2023
d8671e3
TW-565 WIP
herkoss Jun 7, 2023
f4c3826
Merge branch 'development' of github.com:madfish-solutions/templewall…
herkoss Jun 7, 2023
ca97d38
TW-565 WIP
herkoss Jun 7, 2023
3ee6add
TW-565 WIP
herkoss Jun 7, 2023
0c048b7
TW-565 Fixed linter errors
herkoss Jun 8, 2023
9c548b1
TW-565 Fixed comments
herkoss Jun 13, 2023
6054e32
TW-565 Fixed errors
herkoss Jun 13, 2023
d6315ec
TW-565 Finished select
herkoss Jun 23, 2023
e692803
TW-565 Removed uuid
herkoss Jun 23, 2023
961b311
Merge branch 'development' of github.com:madfish-solutions/templewall…
herkoss Jun 23, 2023
293086a
TW-565 Fixed testid
herkoss Jun 23, 2023
c9dde24
TW-565 Fixed comments
herkoss Jun 26, 2023
b6ea0e5
TW-565 Fixed inline styles
herkoss Jun 27, 2023
a6c5413
TW-565 Fixed conflicts
herkoss Jul 6, 2023
0d0528f
TW-565 Fixed comments
herkoss Jul 10, 2023
e167f9c
TW-565 Fixed testids on send modal
herkoss Jul 11, 2023
49b1f88
TW-565 FIxed testId on input
herkoss Jul 11, 2023
bce740a
Merge branch 'development' of github.com:madfish-solutions/templewall…
herkoss Jul 11, 2023
af4da45
TW-565 Fixed testId
herkoss Jul 13, 2023
6ba8fe8
TW-565 Fixed debounce
herkoss Jul 19, 2023
8ec3760
TW-565 Fixed exhange rates
herkoss Jul 24, 2023
67b6b88
TW-565 Fixed items aligning
herkoss Jul 31, 2023
40e78b2
Merge branch 'development' of github.com:madfish-solutions/templewall…
herkoss Aug 29, 2023
7c2d16a
TW-565 FIxed conflicts
herkoss Aug 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions src/app/hooks/use-app-env-style.hook.ts

This file was deleted.

5 changes: 1 addition & 4 deletions src/app/pages/Home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,7 @@ const ActionButton: FC<ActionButtonProps> = ({
>
<Icon className={classNames('w-6 h-auto', disabled ? 'stroke-gray' : 'stroke-accent-orange')} />
</div>
<span
style={{ fontSize: 11 }}
className={classNames('text-center', disabled ? 'text-gray-20' : 'text-gray-910')}
>
<span className={classNames('text-center text-xxs', disabled ? 'text-gray-20' : 'text-gray-910')}>
{label}
</span>
</>
Expand Down
78 changes: 48 additions & 30 deletions src/app/templates/AssetSelect/AssetSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React, { FC, useCallback } from 'react';
import React, { FC, useCallback, useMemo, useState } from 'react';

import classNames from 'clsx';
import { isEqual } from 'lodash';
import { useDebounce } from 'use-debounce';

import Money from 'app/atoms/Money';
import { useTokensMetadataSelector } from 'app/store/tokens-metadata/selectors';
import { AssetIcon } from 'app/templates/AssetIcon';
import Balance from 'app/templates/Balance';
import IconifiedSelect, { IconifiedSelectOptionRenderProps } from 'app/templates/IconifiedSelect';
import InFiat from 'app/templates/InFiat';
import { setTestID, setAnotherSelector } from 'lib/analytics';
import { T, t } from 'lib/i18n';
Expand All @@ -13,6 +16,8 @@ import { useAccount } from 'lib/temple/front';
import { searchAssetsWithNoMeta } from 'lib/temple/front/assets';

import { AssetItemContent } from '../AssetItemContent';
import { DropdownSelect } from '../DropdownSelect/DropdownSelect';
import { InputContainer } from '../InputContainer/InputContainer';
import { SendFormSelectors } from '../SendForm/selectors';
import { IAsset } from './interfaces';
import { getSlug } from './utils';
Expand All @@ -28,13 +33,24 @@ interface AssetSelectProps {
};
}

const renderOptionContent = (asset: IAsset, selected: boolean) => (
<AssetOptionContent asset={asset} selected={selected} />
);

const AssetSelect: FC<AssetSelectProps> = ({ value, assets, onChange, className, testIDs }) => {
const allTokensMetadata = useTokensMetadataSelector();

const [searchString, setSearchString] = useState<string>('');
const [searchStringDebounced] = useDebounce(searchString, 300);

const searchItems = useCallback(
(searchString: string) => searchAssetsWithNoMeta(searchString, assets, allTokensMetadata, getSlug),
[assets, allTokensMetadata]
);
const searchedOptions = useMemo(
() => (searchStringDebounced ? searchItems(searchStringDebounced) : assets),
[searchItems, searchStringDebounced, assets]
);

const handleChange = useCallback(
(asset: IAsset) => {
Expand All @@ -44,31 +60,32 @@ const AssetSelect: FC<AssetSelectProps> = ({ value, assets, onChange, className,
);

return (
<IconifiedSelect
BeforeContent={AssetSelectTitle}
FieldContent={AssetFieldContent}
OptionContent={AssetOptionContent}
getKey={getSlug}
onChange={handleChange}
options={assets}
value={value}
noItemsText={t('noAssetsFound')}
className={className}
fieldStyle={{ minHeight: '4.5rem' }}
search={{
placeholder: t('swapTokenSearchInputPlaceholder'),
filterItems: searchItems,
inputTestID: testIDs?.searchInput
}}
testID={testIDs?.main}
/>
<InputContainer className={className} header={<AssetSelectTitle />}>
<DropdownSelect
DropdownFaceContent={<AssetFieldContent asset={value} />}
searchProps={{
testId: testIDs?.searchInput,
searchValue: searchString,
onSearchChange: event => setSearchString(event.target.value)
}}
testIds={{ dropdownTestId: testIDs?.main }}
dropdownButtonClassName="p-2 h-18"
optionsProps={{
options: searchedOptions,
noItemsText: t('noAssetsFound'),
getKey: option => getSlug(option),
onOptionChange: handleChange,
renderOptionContent: asset => renderOptionContent(asset, isEqual(asset, value))
}}
/>
</InputContainer>
);
};

export default AssetSelect;

const AssetSelectTitle: FC = () => (
<h2 className="mb-4 leading-tight flex flex-col">
<h2 className="leading-tight flex flex-col">
<span className="text-base font-semibold text-gray-700">
<T id="asset" />
</span>
Expand All @@ -79,15 +96,13 @@ const AssetSelectTitle: FC = () => (
</h2>
);

type AssetSelectOptionRenderProps = IconifiedSelectOptionRenderProps<IAsset>;

const AssetFieldContent: FC<AssetSelectOptionRenderProps> = ({ option }) => {
const AssetFieldContent: FC<{ asset: IAsset }> = ({ asset }) => {
const account = useAccount();
const assetSlug = getSlug(option);
const assetSlug = getSlug(asset);
const metadata = useAssetMetadata(assetSlug);

return (
<>
<div className="flex items-center">
<AssetIcon assetSlug={assetSlug} className="mr-3" size={48} />

<Balance assetSlug={assetSlug} address={account.publicKeyHash}>
Expand All @@ -112,16 +127,19 @@ const AssetFieldContent: FC<AssetSelectOptionRenderProps> = ({ option }) => {
</div>
)}
</Balance>
</>
</div>
);
};

const AssetOptionContent: FC<AssetSelectOptionRenderProps> = ({ option }) => {
const slug = getSlug(option);
const AssetOptionContent: FC<{ asset: IAsset; selected: boolean }> = ({ asset, selected }) => {
const slug = getSlug(asset);

return (
<div
className="flex items-center w-full py-1.5"
className={classNames(
'flex items-center w-full py-1.5 px-2 h-15',
selected ? 'bg-gray-200' : 'hover:bg-gray-100'
)}
{...setTestID(SendFormSelectors.assetDropDownItem)}
{...setAnotherSelector('slug', slug)}
>
Expand Down
214 changes: 214 additions & 0 deletions src/app/templates/DropdownSelect/DropdownSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import React, { ChangeEventHandler, ReactNode, FC, Dispatch, SetStateAction } from 'react';

import { isDefined } from '@rnw-community/shared';
import classNames from 'clsx';

import AssetField from 'app/atoms/AssetField';
import DropdownWrapper from 'app/atoms/DropdownWrapper';
import Spinner from 'app/atoms/Spinner/Spinner';
import { ReactComponent as ChevronDownIcon } from 'app/icons/chevron-down.svg';
import { ReactComponent as SearchIcon } from 'app/icons/search.svg';
import { AnalyticsEventCategory, setTestID, useAnalytics } from 'lib/analytics';
import { t } from 'lib/i18n';
import Popper from 'lib/ui/Popper';
import { sameWidthModifiers } from 'lib/ui/same-width-modifiers';

interface Props<T> {
DropdownFaceContent: ReactNode;
Input?: ReactNode;
optionsListClassName?: string;
dropdownButtonClassName?: string;
searchProps: SelectSearchProps;
optionsProps: SelectOptionsPropsBase<T>;
testIds?: {
dropdownTestId?: string;
};
}

export const DropdownSelect = <T extends unknown>({
Input,
searchProps,
optionsProps,
testIds,
DropdownFaceContent,
optionsListClassName,
dropdownButtonClassName
}: Props<T>) => {
const isInputDefined = isDefined(Input);
const { trackEvent } = useAnalytics();

const trackDropdownClick = () => {
if (testIds?.dropdownTestId) {
trackEvent(testIds?.dropdownTestId, AnalyticsEventCategory.DropdownOpened);
}
};

return (
<Popper
placement="bottom"
strategy="fixed"
modifiers={sameWidthModifiers}
fallbackPlacementsEnabled={false}
popup={({ opened, setOpened }) => (
<SelectOptions
optionsListClassName={optionsListClassName}
opened={opened}
setOpened={setOpened}
{...optionsProps}
/>
)}
>
{({ ref, opened, toggleOpened }) => (
<div ref={ref as unknown as React.RefObject<HTMLDivElement>} {...setTestID(testIds?.dropdownTestId)}>
{opened ? (
<SelectSearch {...searchProps} className={dropdownButtonClassName} />
) : (
<div className="box-border w-full flex items-center justify-between border rounded-md border-gray-300 overflow-hidden max-h-18">
<button
className={classNames(
'flex gap-2 items-center max-h-18',
isInputDefined ? 'border-r border-gray-300' : 'w-full justify-between',
dropdownButtonClassName
)}
onClick={() => {
toggleOpened();
trackDropdownClick();
}}
>
{DropdownFaceContent}
<ChevronDownIcon className="text-gray-600 stroke-current stroke-2 h-4 w-4" />
</button>
{Input}
</div>
)}
</div>
)}
</Popper>
);
};
interface SelectOptionsPropsBase<Type> {
options: Array<Type>;
noItemsText: ReactNode;
isLoading?: boolean;
optionsListClassName?: string;
getKey: (option: Type) => string;
onOptionChange: (newValue: Type) => void;
renderOptionContent: (option: Type) => ReactNode;
}
interface SelectOptionsProps<Type> extends SelectOptionsPropsBase<Type> {
opened: boolean;
setOpened: Dispatch<SetStateAction<boolean>>;
}

const SelectOptions = <Type extends unknown>({
opened,
options,
noItemsText,
isLoading,
optionsListClassName,
getKey,
onOptionChange,
setOpened,
renderOptionContent
}: SelectOptionsProps<Type>) => {
const handleOptionClick = (newValue: Type) => {
onOptionChange(newValue);
setOpened(false);
};

return (
<DropdownWrapper
opened={opened}
className="origin-top overflow-x-hidden overflow-y-auto"
style={{
maxHeight: '15.125rem',
backgroundColor: 'white',
borderColor: '#e2e8f0'
}}
>
{(options.length === 0 || isLoading) && (
<div className="my-8 flex flex-col items-center justify-center text-gray-500">
{isLoading ? (
<Spinner className="w-12" theme="primary" />
) : (
<p className="flex items-center justify-center text-gray-600 text-base font-light">
<span>{noItemsText}</span>
</p>
)}
</div>
)}

<ul className={optionsListClassName}>
{options.map(option => (
<li key={getKey(option)}>
<button className="w-full" disabled={(option as any).disabled} onClick={() => handleOptionClick(option)}>
{renderOptionContent(option)}
</button>
</li>
))}
</ul>
</DropdownWrapper>
);
};

interface SelectSearchProps {
testId?: string;
className?: string;
searchValue: string;
tokenIdValue?: string;
showTokenIdInput?: boolean;
onSearchChange: ChangeEventHandler<HTMLInputElement>;
onTokenIdChange?: (newValue: number | string | undefined) => void;
}
const SelectSearch: FC<SelectSearchProps> = ({
testId,
className,
searchValue,
tokenIdValue,
showTokenIdInput = false,
onSearchChange,
onTokenIdChange
}) => {
return (
<div
className={classNames(
'w-full flex items-center transition ease-in-out duration-200 w-full border rounded-md border-orange-500 bg-gray-100 max-h-18',
className
)}
>
<div className="items-center mr-3">
<SearchIcon className={classNames('w-6 h-auto text-gray-500 stroke-current stroke-2')} />
</div>

<div className="text-lg flex flex-1 items-stretch">
<div className="flex-1 flex items-stretch mr-2">
<input
autoFocus
value={searchValue}
className="w-full bg-transparent text-xl text-gray-700 placeholder-gray-500"
placeholder={t('swapTokenSearchInputPlaceholder')}
onChange={onSearchChange}
{...setTestID(testId)}
/>
</div>

{showTokenIdInput && (
<div className="w-24 flex items-stretch border-l border-gray-300">
<AssetField
autoFocus
value={tokenIdValue}
assetDecimals={0}
fieldWrapperBottomMargin={false}
placeholder={t('tokenId')}
style={{ borderRadius: 0 }}
containerStyle={{ flexDirection: 'row' }}
containerClassName="items-stretch"
className="text-lg border-none bg-opacity-0 focus:shadow-none"
onChange={onTokenIdChange}
/>
</div>
)}
</div>
</div>
);
};
Loading
Loading