Added CurrencyFieldInput design (#4254)

* #4123 CurrencyFieldInput design is ready

* resolved comment and currency code

* resolved design comment

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Jeet Desai
2024-03-09 02:00:45 +05:30
committed by GitHub
parent 0c17decfb9
commit 40a3b7d849
7 changed files with 348 additions and 6 deletions

View File

@@ -1,5 +1,5 @@
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
import { TextInput } from '@/ui/field/input/components/TextInput';
import { CurrencyInput } from '@/ui/field/input/components/CurrencyInput';
import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay';
import { useCurrencyField } from '../../hooks/useCurrencyField';
@@ -79,10 +79,18 @@ export const CurrencyFieldInput = ({
});
};
const handleSelect = (newValue: string) => {
setDraftValue({
amount: draftValue?.amount ?? '',
currencyCode: newValue as CurrencyCode,
});
};
return (
<FieldInputOverlay>
<TextInput
<CurrencyInput
value={draftValue?.amount?.toString() ?? ''}
currencyCode={draftValue?.currencyCode ?? CurrencyCode.USD}
autoFocus
placeholder="Currency"
onClickOutside={handleClickOutside}
@@ -90,8 +98,9 @@ export const CurrencyFieldInput = ({
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
onChange={handleChange}
onSelect={handleSelect}
hotkeyScope={hotkeyScope}
/>
</FieldInputOverlay>
);

View File

@@ -1,4 +1,3 @@
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldInputDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
@@ -31,7 +30,7 @@ export const computeDraftValueFromFieldValue = <FieldValue>({
return {
amount: fieldValue?.amountMicros ? fieldValue.amountMicros / 1000000 : '',
currenyCode: CurrencyCode.USD,
currencyCode: fieldValue?.currencyCode ?? '',
} as unknown as FieldInputDraftValue<FieldValue>;
}
if (isFieldRelation(fieldDefinition)) {

View File

@@ -0,0 +1,152 @@
import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { CurrencyPickerDropdownButton } from '@/ui/input/components/internal/currency/components/CurrencyPickerDropdownButton';
import { TEXT_INPUT_STYLE } from '@/ui/theme/constants/TextInputStyle';
export const StyledInput = styled.input`
margin: 0;
${TEXT_INPUT_STYLE}
width: 100%;
padding: ${({ theme }) => `${theme.spacing(0)} ${theme.spacing(1)}`};
`;
const StyledContainer = styled.div`
align-items: center;
border: none;
border-radius: ${({ theme }) => theme.border.radius.sm};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
display: flex;
justify-content: center;
`;
const StyledIcon = styled.div`
align-items: center;
display: flex;
& > svg {
padding-left: ${({ theme }) => theme.spacing(1)};
color: ${({ theme }) => theme.font.color.tertiary};
height: ${({ theme }) => theme.icon.size.md}px;
width: ${({ theme }) => theme.icon.size.md}px;
}
`;
export type CurrencyInputProps = {
placeholder?: string;
autoFocus?: boolean;
value: string;
currencyCode: string;
onEnter: (newText: string) => void;
onEscape: (newText: string) => void;
onTab?: (newText: string) => void;
onShiftTab?: (newText: string) => void;
onClickOutside: (event: MouseEvent | TouchEvent, inputValue: string) => void;
onChange?: (newText: string) => void;
onSelect?: (newText: string) => void;
hotkeyScope: string;
};
type Currency = {
label: string;
value: string;
Icon: any;
};
export const CurrencyInput = ({
autoFocus,
value,
currencyCode,
placeholder,
onEnter,
onEscape,
onTab,
onShiftTab,
onClickOutside,
onChange,
onSelect,
hotkeyScope,
}: CurrencyInputProps) => {
const theme = useTheme();
const [internalText, setInternalText] = useState(value);
const [internalCurrency, setInternalCurrency] = useState<Currency | null>(
null,
);
const wrapperRef = useRef<HTMLInputElement>(null);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setInternalText(event.target.value);
onChange?.(event.target.value);
};
const handleCurrencyChange = (currency: Currency) => {
setInternalCurrency(currency);
onSelect?.(currency.value);
};
useRegisterInputEvents({
inputRef: wrapperRef,
inputValue: internalText,
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
hotkeyScope,
});
const currencies = useMemo<Currency[]>(
() =>
Object.entries(SETTINGS_FIELD_CURRENCY_CODES).map(
([key, { Icon, label }]) => ({
value: key,
Icon,
label,
}),
),
[],
);
useEffect(() => {
const currency = currencies.find(({ value }) => value === currencyCode);
if (currency) {
setInternalCurrency(currency);
}
}, [currencies, currencyCode]);
useEffect(() => {
setInternalText(value);
}, [value]);
const Icon: IconComponent = internalCurrency?.Icon;
return (
<StyledContainer ref={wrapperRef}>
<CurrencyPickerDropdownButton
valueCode={internalCurrency?.value ?? ''}
onChange={handleCurrencyChange}
currencies={currencies}
/>
<StyledIcon>
{Icon && (
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
)}
</StyledIcon>
<StyledInput
autoComplete="off"
placeholder={placeholder}
onChange={handleChange}
autoFocus={autoFocus}
value={value}
/>
</StyledContainer>
);
};

View File

@@ -0,0 +1,108 @@
import { useEffect, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
import { IconChevronDown } from '@/ui/display/icon';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { CurrencyPickerHotkeyScope } from '../types/CurrencyPickerHotkeyScope';
import { CurrencyPickerDropdownSelect } from './CurrencyPickerDropdownSelect';
type StyledDropdownButtonProps = {
isUnfolded: boolean;
};
export const StyledDropdownButtonContainer = styled.div<StyledDropdownButtonProps>`
align-items: center;
color: ${({ color }) => color ?? 'none'};
cursor: pointer;
display: flex;
border-right: ${({ theme }) => `1px solid ${theme.border.color.medium}`};
height: 32px;
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
user-select: none;
&:hover {
filter: brightness(0.95);
}
`;
const StyledIconContainer = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
font-weight: ${({ theme }) => theme.font.weight.medium};
justify-content: center;
svg {
align-items: center;
display: flex;
height: 16px;
justify-content: center;
}
`;
export type Currency = {
label: string;
value: string;
Icon: any;
};
export const CurrencyPickerDropdownButton = ({
valueCode,
onChange,
currencies,
}: {
valueCode: string;
onChange: (currency: Currency) => void;
currencies: Currency[];
}) => {
const theme = useTheme();
const [selectedCurrency, setSelectedCurrency] = useState<Currency>();
const { isDropdownOpen, closeDropdown } = useDropdown(
CurrencyPickerHotkeyScope.CurrencyPicker,
);
const handleChange = (currency: Currency) => {
onChange(currency);
closeDropdown();
};
useEffect(() => {
const currency = currencies.find(({ value }) => value === valueCode);
if (currency) {
setSelectedCurrency(currency);
}
}, [valueCode, currencies]);
return (
<Dropdown
dropdownMenuWidth={200}
dropdownId="currncy-picker-dropdown-id"
dropdownHotkeyScope={{ scope: CurrencyPickerHotkeyScope.CurrencyPicker }}
clickableComponent={
<StyledDropdownButtonContainer isUnfolded={isDropdownOpen}>
<StyledIconContainer>
{selectedCurrency ? selectedCurrency.value : CurrencyCode.USD}
<IconChevronDown size={theme.icon.size.sm} />
</StyledIconContainer>
</StyledDropdownButtonContainer>
}
dropdownComponents={
<CurrencyPickerDropdownSelect
currencies={currencies}
selectedCurrency={selectedCurrency}
onChange={handleChange}
/>
}
dropdownPlacement="bottom-start"
dropdownOffset={{ x: 0, y: 4 }}
/>
);
};

View File

@@ -0,0 +1,71 @@
import { useMemo, useState } from 'react';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar';
import { Currency } from './CurrencyPickerDropdownButton';
export const CurrencyPickerDropdownSelect = ({
currencies,
selectedCurrency,
onChange,
}: {
currencies: Currency[];
selectedCurrency?: Currency;
onChange: (currency: Currency) => void;
}) => {
const [searchFilter, setSearchFilter] = useState<string>('');
const filteredCurrencies = useMemo(
() =>
currencies.filter(
({ value, label }) =>
value
.toLocaleLowerCase()
.includes(searchFilter.toLocaleLowerCase()) ||
label.toLocaleLowerCase().includes(searchFilter.toLocaleLowerCase()),
),
[currencies, searchFilter],
);
return (
<DropdownMenu width="200px" disableBlur>
<DropdownMenuSearchInput
value={searchFilter}
onChange={(event) => setSearchFilter(event.target.value)}
autoFocus
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{filteredCurrencies.length === 0 ? (
<MenuItem text="No result" />
) : (
<>
{selectedCurrency && (
<MenuItemSelectAvatar
key={selectedCurrency.value}
selected={true}
onClick={() => onChange(selectedCurrency)}
text={`${selectedCurrency.label} (${selectedCurrency.value})`}
/>
)}
{filteredCurrencies.map((item) =>
selectedCurrency?.value === item.value ? null : (
<MenuItemSelectAvatar
key={item.value}
selected={selectedCurrency?.value === item.value}
onClick={() => onChange(item)}
text={`${item.label} (${item.value})`}
/>
),
)}
</>
)}
</DropdownMenuItemsContainer>
</DropdownMenu>
);
};

View File

@@ -0,0 +1,3 @@
export enum CurrencyPickerHotkeyScope {
CurrencyPicker = 'currency-picker',
}

View File

@@ -12,7 +12,7 @@ import {
import { StyledMenuItemSelect } from './MenuItemSelect';
type MenuItemSelectAvatarProps = {
avatar: ReactNode;
avatar?: ReactNode;
selected: boolean;
text: string;
className?: string;