fix: country code selector

This commit is contained in:
Koosha Lahouti
2025-09-25 12:58:50 +03:30
parent fff86c5ff1
commit 5dc2ee585b
10 changed files with 7327 additions and 1032 deletions

View File

@@ -13,20 +13,20 @@ import { useMemo, useRef, useState, type RefObject } from 'react';
import { ArrowDown2 } from 'iconsax-react';
import ReactCountryFlag from 'react-country-flag';
import { useTranslation } from 'react-i18next';
import { countries, type Country } from '@/data/countries';
import { countries, type Country } from '../../../data/countries';
import type { CountryCode } from '@/types/commonTypes';
import { Icon } from '@rkheftan/harmony-ui';
import { LTRBox } from '@/components/common/LTRBox';
import { Virtuoso } from 'react-virtuoso';
interface CountryCodeSelectorProps {
show: boolean;
value: string;
onChange: (newValue: string) => void;
value: CountryCode;
onChange: (newValue: CountryCode) => void;
menuAnchor: HTMLElement | null;
onCloseFocusRef: RefObject<HTMLInputElement | null>;
}
/**
* An animated country code adornment that fades and slides into view.
* Its visibility is controlled by the `show` prop.
*/
export function CountryCodeSelector({
show,
value,
@@ -39,23 +39,19 @@ export function CountryCodeSelector({
const open = Boolean(anchorEl);
const searchInputRef = useRef<HTMLInputElement>(null);
const menuWidth = menuAnchor ? menuAnchor.clientWidth : 'auto';
const { t, i18n } = useTranslation();
const { t, i18n } = useTranslation(['country', 'common']);
const selectedCountry =
countries.find((c) => c.phone === value) || countries[0];
const handleClick = () => {
setAnchorEl(menuAnchor);
};
const handleClick = () => setAnchorEl(menuAnchor);
const handleClose = () => {
setTimeout(() => {
setAnchorEl(null);
}, 0);
setTimeout(() => setAnchorEl(null), 0);
setTimeout(() => {
onCloseFocusRef.current?.focus();
}, 100);
setSearchTerm(''); // Reset search on close
setSearchTerm('');
};
const handleSelect = (country: Country) => {
@@ -64,7 +60,6 @@ export function CountryCodeSelector({
};
const handleMenuEntered = () => {
// Focus the input field after the menu has finished opening
searchInputRef.current?.focus();
};
@@ -72,55 +67,51 @@ export function CountryCodeSelector({
() =>
countries.filter(
(country) =>
t(country.label).toLowerCase().includes(searchTerm.toLowerCase()) ||
t(country.label, { ns: 'country' })
.toLowerCase()
.includes(searchTerm.toLowerCase()) ||
country.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
country.phone.includes(searchTerm),
),
[searchTerm, t],
);
const initialIndex = Math.max(
0,
filteredCountries.findIndex((c) => c.phone === value),
);
return (
<InputAdornment
position={i18n.dir() === 'rtl' ? 'start' : 'end'}
sx={{
mx: 0,
}}
sx={{ mx: 0 }}
>
<Box
onClick={handleClick}
sx={{
// Animate width and opacity based on the 'show' prop
width: show ? 'auto' : 0,
opacity: show ? 1 : 0,
transition: (theme) =>
theme.transitions.create(['width', 'opacity'], {
duration: theme.transitions.duration.standard,
}),
// Prevent content from wrapping or spilling out during animation
overflow: 'hidden',
whiteSpace: 'nowrap',
// layout styles
height: '100%',
display: 'flex',
alignItems: 'center',
gap: 0.25,
pl: show ? 0.25 : 0,
'&:hover': {
cursor: 'pointer',
},
'&:hover': { cursor: 'pointer' },
}}
>
{/* This inner Box prevents the content from being squeezed during the transition */}
<ArrowDown2 size="24" variant="Bold" />
<Icon Component={ArrowDown2} size="medium" variant="Bold" />
<Typography
variant="body1"
color="text.primary"
component="div"
sx={{ direction: 'rtl' }} // TODO: need to fixed for both en and fa
sx={{ direction: 'rtl' }}
>
{value}
</Typography>
@@ -131,7 +122,6 @@ export function CountryCodeSelector({
style={{
height: '1.5rem',
width: '1.5rem',
// TODO: Check alignment for better styling definition
marginTop: '-2px',
marginRight: '4px',
}}
@@ -144,23 +134,15 @@ export function CountryCodeSelector({
slotProps={{
list: {
sx: {
// Set the width to match the anchor element's width
width: menuWidth,
height: '18.75rem',
p: 0,
},
},
transition: {
onEntered: handleMenuEntered,
},
}}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
transition: { onEntered: handleMenuEntered },
}}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
>
<Box
sx={{
@@ -175,78 +157,52 @@ export function CountryCodeSelector({
inputRef={searchInputRef}
size="small"
fullWidth
label={t('labels.search')}
label={t('labels.search', { ns: 'common' })}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</Box>
{/* Can improve preformance with using virtual scrolling */}
<Box sx={{ width: '100%', maxHeight: '14.75rem', overflow: 'auto' }}>
{/* Virtualized country list */}
<Box sx={{ width: '100%', height: '14.75rem' }}>
{filteredCountries.length === 0 ? (
<MenuItem disabled>
<ListItem>
<ListItemText>{t('messages.noResualtFound')}</ListItemText>
<ListItemText>
{t('messages.noResualtFound', { ns: 'common' })}
</ListItemText>
</ListItem>
</MenuItem>
) : (
filteredCountries.map((country) => (
<MenuItem
key={country.code}
selected={country.phone === value}
onClick={() => handleSelect(country)}
>
<ListItemIcon>
<ReactCountryFlag
countryCode={country.code}
svg
style={{
height: '1.125rem',
width: '1.125rem',
}}
<Virtuoso
style={{ height: '100%', width: '100%' }}
data={filteredCountries}
initialTopMostItemIndex={initialIndex}
itemContent={(_, country) => (
<MenuItem
key={country.code}
selected={country.phone === value}
onClick={() => handleSelect(country)}
>
<ListItemIcon>
<ReactCountryFlag
countryCode={country.code}
svg
style={{ height: '1.125rem', width: '1.125rem' }}
/>
</ListItemIcon>
<ListItemText
primary={t(country.label, { ns: 'country' })}
/>
</ListItemIcon>
<ListItemText primary={t(country.label)} />
<Typography color="text.secondary">
{country.phone}
</Typography>
</MenuItem>
))
<Typography color="text.secondary">
<LTRBox>{country.phone}</LTRBox>
</Typography>
</MenuItem>
)}
computeItemKey={(_, c) => c.code}
/>
)}
</Box>
{/* virtual scrolling */}
{/* <Virtuoso
style={{ height: '14.75rem' }} // Adjust height to account for the search bar
data={filteredCountries}
components={{
EmptyPlaceholder: () => (
<MenuItem disabled>
<ListItemText>{t('messages.noResultFound')}</ListItemText>
</MenuItem>
),
}}
initialTopMostItemIndex={countries.indexOf(selectedCountry)}
itemContent={(_, country) => (
<MenuItem
key={country.code}
selected={country.phone === value}
onClick={() => handleSelect(country)}
>
<ListItemIcon>
<ReactCountryFlag
countryCode={country.code}
svg
style={{ fontSize: '1.25em', lineHeight: '1.25em' }}
/>
</ListItemIcon>
<ListItemText primary={country.label} />
<Typography variant="body2" color="text.secondary">
{country.phone}
</Typography>
</MenuItem>
)}
/> */}
</Menu>
</Box>
</InputAdornment>