feat: address composite field (#4492)
Added new Address field input type. --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
import { ChangeEvent } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { StyledInput } from '@/ui/field/input/components/TextInput';
|
||||
import { StyledTextInput as UIStyledTextInput } from '@/ui/field/input/components/TextInput';
|
||||
import { ComputeNodeDimensions } from '@/ui/utilities/dimensions/components/ComputeNodeDimensions';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
|
||||
@ -23,7 +23,7 @@ const StyledDoubleTextContainer = styled.div`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const StyledTextInput = styled(StyledInput)`
|
||||
const StyledTextInput = styled(UIStyledTextInput)`
|
||||
margin: 0 ${({ theme }) => theme.spacing(0.5)};
|
||||
padding: 0;
|
||||
width: ${({ width }) => (width ? `${width}px` : 'auto')};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
@ -10,6 +10,7 @@ import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/Dropdow
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||
|
||||
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
|
||||
|
||||
@ -43,6 +44,7 @@ const StyledControlContainer = styled.div<{ disabled?: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
box-sizing: border-box;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ disabled, theme }) =>
|
||||
disabled ? theme.font.color.tertiary : theme.font.color.primary};
|
||||
@ -88,6 +90,8 @@ export const Select = <Value extends string | number | null>({
|
||||
value,
|
||||
withSearchInput,
|
||||
}: SelectProps<Value>) => {
|
||||
const selectContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const theme = useTheme();
|
||||
const [searchInputValue, setSearchInputValue] = useState('');
|
||||
|
||||
@ -109,6 +113,15 @@ export const Select = <Value extends string | number | null>({
|
||||
|
||||
const { closeDropdown } = useDropdown(dropdownId);
|
||||
|
||||
const { useListenClickOutside } = useClickOutsideListener(dropdownId);
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [selectContainerRef],
|
||||
callback: () => {
|
||||
closeDropdown();
|
||||
},
|
||||
});
|
||||
|
||||
const selectControl = (
|
||||
<StyledControlContainer disabled={isDisabled}>
|
||||
<StyledControlLabel>
|
||||
@ -133,6 +146,7 @@ export const Select = <Value extends string | number | null>({
|
||||
fullWidth={fullWidth}
|
||||
tabIndex={0}
|
||||
onBlur={onBlur}
|
||||
ref={selectContainerRef}
|
||||
>
|
||||
{!!label && <StyledLabel>{label}</StyledLabel>}
|
||||
{isDisabled ? (
|
||||
|
||||
@ -20,20 +20,6 @@ import { useCombinedRefs } from '~/hooks/useCombinedRefs';
|
||||
|
||||
import { InputHotkeyScope } from '../types/InputHotkeyScope';
|
||||
|
||||
export type TextInputComponentProps = Omit<
|
||||
InputHTMLAttributes<HTMLInputElement>,
|
||||
'onChange' | 'onKeyDown'
|
||||
> & {
|
||||
className?: string;
|
||||
label?: string;
|
||||
onChange?: (text: string) => void;
|
||||
fullWidth?: boolean;
|
||||
disableHotkeys?: boolean;
|
||||
error?: string;
|
||||
RightIcon?: IconComponent;
|
||||
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div<Pick<TextInputComponentProps, 'fullWidth'>>`
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
@ -110,6 +96,21 @@ const StyledTrailingIcon = styled.div`
|
||||
|
||||
const INPUT_TYPE_PASSWORD = 'password';
|
||||
|
||||
export type TextInputComponentProps = Omit<
|
||||
InputHTMLAttributes<HTMLInputElement>,
|
||||
'onChange' | 'onKeyDown'
|
||||
> & {
|
||||
className?: string;
|
||||
label?: string;
|
||||
onChange?: (text: string) => void;
|
||||
fullWidth?: boolean;
|
||||
disableHotkeys?: boolean;
|
||||
error?: string;
|
||||
RightIcon?: IconComponent;
|
||||
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
onBlur?: () => void;
|
||||
};
|
||||
|
||||
const TextInputComponent = (
|
||||
{
|
||||
className,
|
||||
@ -163,6 +164,7 @@ const TextInputComponent = (
|
||||
inputRef.current?.blur();
|
||||
},
|
||||
InputHotkeyScope.TextInput,
|
||||
{ enabled: !disableHotkeys },
|
||||
);
|
||||
|
||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { IconComponentProps } from '@/ui/display/icon/types/IconComponent';
|
||||
import { SELECT_COUNTRY_DROPDOWN_ID } from '@/ui/input/components/internal/country/constants/SelectCountryDropdownId';
|
||||
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
|
||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||
|
||||
export const CountrySelect = ({
|
||||
selectedCountryName,
|
||||
onChange,
|
||||
}: {
|
||||
selectedCountryName: string;
|
||||
onChange: (countryCode: string) => void;
|
||||
}) => {
|
||||
const countries = useCountries();
|
||||
|
||||
const options: SelectOption<string>[] = useMemo(() => {
|
||||
return countries.map<SelectOption<string>>(({ countryName, Flag }) => ({
|
||||
label: countryName,
|
||||
value: countryName,
|
||||
Icon: (props: IconComponentProps) =>
|
||||
Flag({ width: props.size, height: props.size }), // TODO : improve this ?
|
||||
}));
|
||||
}, [countries]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
fullWidth
|
||||
dropdownId={SELECT_COUNTRY_DROPDOWN_ID}
|
||||
options={options}
|
||||
label="COUNTRY"
|
||||
withSearchInput
|
||||
onChange={onChange}
|
||||
value={selectedCountryName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export const SELECT_COUNTRY_DROPDOWN_ID = 'select-country-picker';
|
||||
@ -0,0 +1,37 @@
|
||||
import { useMemo } from 'react';
|
||||
import { hasFlag } from 'country-flag-icons';
|
||||
import * as Flags from 'country-flag-icons/react/3x2';
|
||||
import { getCountries, getCountryCallingCode } from 'libphonenumber-js';
|
||||
|
||||
import { Country } from '@/ui/input/components/internal/types/Country';
|
||||
|
||||
export const useCountries = () => {
|
||||
return useMemo<Country[]>(() => {
|
||||
const regionNamesInEnglish = new Intl.DisplayNames(['en'], {
|
||||
type: 'region',
|
||||
});
|
||||
|
||||
const countryCodes = getCountries();
|
||||
|
||||
return countryCodes.reduce<Country[]>((result, countryCode) => {
|
||||
const countryName = regionNamesInEnglish.of(countryCode);
|
||||
|
||||
if (!countryName) return result;
|
||||
|
||||
if (!hasFlag(countryCode)) return result;
|
||||
|
||||
const Flag = Flags[countryCode];
|
||||
|
||||
const callingCode = getCountryCallingCode(countryCode);
|
||||
|
||||
result.push({
|
||||
countryCode,
|
||||
countryName,
|
||||
callingCode,
|
||||
Flag,
|
||||
});
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}, []);
|
||||
};
|
||||
@ -1,19 +1,17 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { getCountries, getCountryCallingCode } from 'react-phone-number-input';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { hasFlag } from 'country-flag-icons';
|
||||
import * as Flags from 'country-flag-icons/react/3x2';
|
||||
import { CountryCallingCode } from 'libphonenumber-js';
|
||||
|
||||
import { IconChevronDown, IconWorld } from '@/ui/display/icon';
|
||||
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
|
||||
import { Country } from '@/ui/input/components/internal/types/Country';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { CountryPickerHotkeyScope } from '../types/CountryPickerHotkeyScope';
|
||||
|
||||
import { CountryPickerDropdownSelect } from './CountryPickerDropdownSelect';
|
||||
import { PhoneCountryPickerDropdownSelect } from './PhoneCountryPickerDropdownSelect';
|
||||
|
||||
import 'react-phone-number-input/style.css';
|
||||
|
||||
@ -57,14 +55,7 @@ const StyledIconContainer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export type Country = {
|
||||
countryCode: string;
|
||||
countryName: string;
|
||||
callingCode: CountryCallingCode;
|
||||
Flag: Flags.FlagComponent;
|
||||
};
|
||||
|
||||
export const CountryPickerDropdownButton = ({
|
||||
export const PhoneCountryPickerDropdownButton = ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
@ -82,34 +73,7 @@ export const CountryPickerDropdownButton = ({
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const countries = useMemo<Country[]>(() => {
|
||||
const regionNamesInEnglish = new Intl.DisplayNames(['en'], {
|
||||
type: 'region',
|
||||
});
|
||||
|
||||
const countryCodes = getCountries();
|
||||
|
||||
return countryCodes.reduce<Country[]>((result, countryCode) => {
|
||||
const countryName = regionNamesInEnglish.of(countryCode);
|
||||
|
||||
if (!countryName) return result;
|
||||
|
||||
if (!hasFlag(countryCode)) return result;
|
||||
|
||||
const Flag = Flags[countryCode];
|
||||
|
||||
const callingCode = getCountryCallingCode(countryCode);
|
||||
|
||||
result.push({
|
||||
countryCode,
|
||||
countryName,
|
||||
callingCode,
|
||||
Flag,
|
||||
});
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}, []);
|
||||
const countries = useCountries();
|
||||
|
||||
useEffect(() => {
|
||||
const country = countries.find(({ countryCode }) => countryCode === value);
|
||||
@ -132,7 +96,7 @@ export const CountryPickerDropdownButton = ({
|
||||
</StyledDropdownButtonContainer>
|
||||
}
|
||||
dropdownComponents={
|
||||
<CountryPickerDropdownSelect
|
||||
<PhoneCountryPickerDropdownSelect
|
||||
countries={countries}
|
||||
selectedCountry={selectedCountry}
|
||||
onChange={handleChange}
|
||||
@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Country } from '@/ui/input/components/internal/types/Country';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
@ -8,8 +9,6 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar';
|
||||
|
||||
import { Country } from './CountryPickerDropdownButton';
|
||||
|
||||
import 'react-phone-number-input/style.css';
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
@ -27,7 +26,7 @@ const StyledIconContainer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export const CountryPickerDropdownSelect = ({
|
||||
export const PhoneCountryPickerDropdownSelect = ({
|
||||
countries,
|
||||
selectedCountry,
|
||||
onChange,
|
||||
@ -0,0 +1,9 @@
|
||||
import * as Flags from 'country-flag-icons/react/3x2';
|
||||
import { CountryCallingCode } from 'libphonenumber-js';
|
||||
|
||||
export type Country = {
|
||||
countryCode: string;
|
||||
countryName: string;
|
||||
callingCode: CountryCallingCode;
|
||||
Flag: Flags.FlagComponent;
|
||||
};
|
||||
Reference in New Issue
Block a user