Feat: Adjust the overlay style for changing the phone number's country (#1876)
* switched to dropdown menu component * Use latest dropdown container --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -0,0 +1,3 @@
|
|||||||
|
export enum CountryPickerHotkeyScope {
|
||||||
|
CountryPicker = 'country-picker',
|
||||||
|
}
|
||||||
@ -0,0 +1,146 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { getCountries, getCountryCallingCode } from 'react-phone-number-input';
|
||||||
|
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 { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
|
||||||
|
import { useDropdown } from '@/ui/dropdown/hooks/useDropdown';
|
||||||
|
import { IconChevronDown } from '@/ui/icon';
|
||||||
|
|
||||||
|
import { IconWorld } from '../constants/icons';
|
||||||
|
import { CountryPickerHotkeyScope } from '../Types/CountryPickerHotkeyScope';
|
||||||
|
|
||||||
|
import { CountryPickerDropdownSelect } from './CountryPickerDropdownSelect';
|
||||||
|
|
||||||
|
import 'react-phone-number-input/style.css';
|
||||||
|
|
||||||
|
type StyledDropdownButtonProps = {
|
||||||
|
isUnfolded: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StyledDropdownButtonContainer = styled.div<StyledDropdownButtonProps>`
|
||||||
|
align-items: center;
|
||||||
|
background: ${({ theme }) => theme.background.primary};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.xs};
|
||||||
|
color: ${({ color }) => color ?? 'none'};
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
filter: ${(props) => (props.isUnfolded ? 'brightness(0.95)' : 'none')};
|
||||||
|
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding-right: ${({ 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)};
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 16px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export type Country = {
|
||||||
|
countryCode: string;
|
||||||
|
countryName: string;
|
||||||
|
callingCode: CountryCallingCode;
|
||||||
|
Flag: Flags.FlagComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CountryPickerDropdownButton = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
onChange: (countryCode: string) => void;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [selectedCountry, setSelectedCountry] = useState<Country>();
|
||||||
|
|
||||||
|
const { isDropdownOpen, closeDropdown } = useDropdown({
|
||||||
|
dropdownId: 'country-picker',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange = (countryCode: string) => {
|
||||||
|
onChange(countryCode);
|
||||||
|
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;
|
||||||
|
}, []);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const country = countries.find(({ countryCode }) => countryCode === value);
|
||||||
|
if (country) {
|
||||||
|
setSelectedCountry(country);
|
||||||
|
}
|
||||||
|
}, [countries, value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu
|
||||||
|
dropdownId="country-picker"
|
||||||
|
dropdownHotkeyScope={{ scope: CountryPickerHotkeyScope.CountryPicker }}
|
||||||
|
clickableComponent={
|
||||||
|
<StyledDropdownButtonContainer isUnfolded={isDropdownOpen}>
|
||||||
|
<StyledIconContainer>
|
||||||
|
{selectedCountry ? <selectedCountry.Flag /> : <IconWorld />}
|
||||||
|
<IconChevronDown size={theme.icon.size.sm} />
|
||||||
|
</StyledIconContainer>
|
||||||
|
</StyledDropdownButtonContainer>
|
||||||
|
}
|
||||||
|
dropdownComponents={
|
||||||
|
<CountryPickerDropdownSelect
|
||||||
|
countries={countries}
|
||||||
|
selectedCountry={selectedCountry}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
dropdownOffset={{ x: -60, y: -34 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
|
||||||
|
import { DropdownMenuSearchInput } from '@/ui/dropdown/components/DropdownMenuSearchInput';
|
||||||
|
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
|
||||||
|
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
|
||||||
|
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
|
||||||
|
import { MenuItemSelectAvatar } from '@/ui/menu-item/components/MenuItemSelectAvatar';
|
||||||
|
|
||||||
|
import { Country } from './CountryPickerDropdownButton';
|
||||||
|
|
||||||
|
import 'react-phone-number-input/style.css';
|
||||||
|
|
||||||
|
const StyledIconContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
display: flex;
|
||||||
|
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||||
|
|
||||||
|
svg {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 16px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledDropdownMenuContainer = styled.ul`
|
||||||
|
left: 0;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const CountryPickerDropdownSelect = ({
|
||||||
|
countries,
|
||||||
|
selectedCountry,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
countries: Country[];
|
||||||
|
selectedCountry?: Country;
|
||||||
|
onChange: (countryCode: string) => void;
|
||||||
|
}) => {
|
||||||
|
const [searchFilter, setSearchFilter] = useState<string>('');
|
||||||
|
|
||||||
|
const filteredCountries = useMemo(
|
||||||
|
() =>
|
||||||
|
countries.filter(({ countryName }) =>
|
||||||
|
countryName
|
||||||
|
.toLocaleLowerCase()
|
||||||
|
.includes(searchFilter.toLocaleLowerCase()),
|
||||||
|
),
|
||||||
|
[countries, searchFilter],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledDropdownMenuContainer data-select-disable>
|
||||||
|
<StyledDropdownMenu width={'240px'}>
|
||||||
|
<DropdownMenuSearchInput
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={(event) => setSearchFilter(event.currentTarget.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<StyledDropdownMenuSeparator />
|
||||||
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
|
{filteredCountries?.length === 0 ? (
|
||||||
|
<MenuItem text="No result" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{selectedCountry && (
|
||||||
|
<MenuItemSelectAvatar
|
||||||
|
key={selectedCountry.countryCode}
|
||||||
|
selected={true}
|
||||||
|
onClick={() => onChange(selectedCountry.countryCode)}
|
||||||
|
text={`${selectedCountry.countryName} (+${selectedCountry.callingCode})`}
|
||||||
|
avatar={
|
||||||
|
<StyledIconContainer>
|
||||||
|
<selectedCountry.Flag />
|
||||||
|
</StyledIconContainer>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{filteredCountries.map(
|
||||||
|
({ countryCode, countryName, callingCode, Flag }) =>
|
||||||
|
selectedCountry?.countryCode === countryCode ? null : (
|
||||||
|
<MenuItemSelectAvatar
|
||||||
|
key={countryCode}
|
||||||
|
selected={selectedCountry?.countryCode === countryCode}
|
||||||
|
onClick={() => onChange(countryCode)}
|
||||||
|
text={`${countryName} (+${callingCode})`}
|
||||||
|
avatar={
|
||||||
|
<StyledIconContainer>
|
||||||
|
<Flag />
|
||||||
|
</StyledIconContainer>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItemsContainer>
|
||||||
|
</StyledDropdownMenu>
|
||||||
|
</StyledDropdownMenuContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -2,8 +2,13 @@ import { useEffect, useRef, useState } from 'react';
|
|||||||
import ReactPhoneNumberInput from 'react-phone-number-input';
|
import ReactPhoneNumberInput from 'react-phone-number-input';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
|
||||||
|
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||||
|
|
||||||
import { useRegisterInputEvents } from '../hooks/useRegisterInputEvents';
|
import { useRegisterInputEvents } from '../hooks/useRegisterInputEvents';
|
||||||
|
|
||||||
|
import { CountryPickerDropdownButton } from './CountryPickerDropdownButton';
|
||||||
|
|
||||||
import 'react-phone-number-input/style.css';
|
import 'react-phone-number-input/style.css';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -17,39 +22,9 @@ const StyledContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)`
|
const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)`
|
||||||
--PhoneInput-color--focus: transparent;
|
|
||||||
--PhoneInputCountryFlag-borderColor--focus: transparent;
|
|
||||||
--PhoneInputCountrySelect-marginRight: ${({ theme }) => theme.spacing(2)};
|
|
||||||
--PhoneInputCountrySelectArrow-color: ${({ theme }) =>
|
|
||||||
theme.font.color.tertiary};
|
|
||||||
--PhoneInputCountrySelectArrow-opacity: 1;
|
|
||||||
font-family: ${({ theme }) => theme.font.family};
|
font-family: ${({ theme }) => theme.font.family};
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|
||||||
.PhoneInputCountry {
|
|
||||||
--PhoneInputCountryFlag-height: 12px;
|
|
||||||
--PhoneInputCountryFlag-width: 16px;
|
|
||||||
border-right: 1px solid ${({ theme }) => theme.border.color.light};
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-left: ${({ theme }) => theme.spacing(2)};
|
|
||||||
}
|
|
||||||
|
|
||||||
.PhoneInputCountryIcon {
|
|
||||||
background: none;
|
|
||||||
border-radius: ${({ theme }) => theme.border.radius.xs};
|
|
||||||
box-shadow: none;
|
|
||||||
margin-right: 1px;
|
|
||||||
overflow: hidden;
|
|
||||||
&:focus {
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.PhoneInputCountrySelectArrow {
|
|
||||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
|
||||||
}
|
|
||||||
|
|
||||||
.PhoneInputInput {
|
.PhoneInputInput {
|
||||||
background: ${({ theme }) => theme.background.transparent.secondary};
|
background: ${({ theme }) => theme.background.transparent.secondary};
|
||||||
border: none;
|
border: none;
|
||||||
@ -111,12 +86,17 @@ export const PhoneInput = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer ref={wrapperRef}>
|
<StyledContainer ref={wrapperRef}>
|
||||||
<StyledCustomPhoneInput
|
<RecoilScope CustomRecoilScopeContext={DropdownRecoilScopeContext}>
|
||||||
autoFocus={autoFocus}
|
<StyledCustomPhoneInput
|
||||||
placeholder="Phone number"
|
autoFocus={autoFocus}
|
||||||
value={value}
|
placeholder="Phone number"
|
||||||
onChange={setInternalValue}
|
value={value}
|
||||||
/>
|
onChange={setInternalValue}
|
||||||
|
international={true}
|
||||||
|
withCountryCallingCode={true}
|
||||||
|
countrySelectComponent={CountryPickerDropdownButton}
|
||||||
|
/>
|
||||||
|
</RecoilScope>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user