import React, { useCallback, useMemo, useState } from 'react'; import { useLocale, VisuallyHidden } from 'react-aria'; import classNames from 'classnames'; import { polyfillCountryFlagEmojis } from 'country-flag-emoji-polyfill'; import { FieldSet, useTranslations } from '@shoptet/ui-core-web'; import type { Complete, Exact } from '../../../types'; import { destruct } from '../../../utils/destruct'; import { pick } from '../../../utils/pick'; import { DataListItemCell } from '../../DataList/DataListItemCell'; import type { FormRowProps } from '../../FormRow/FormRow'; import { FormRow } from '../../FormRow/FormRow'; import type { FormRowHelperTextOwnProps } from '../../FormRow/FormRowHelperText'; import { InputLoader } from '../../Inputs/InputLoader'; import type { PhoneInputProps } from '../../Inputs/PhoneInput/PhoneInput'; import { PhoneInput } from '../../Inputs/PhoneInput/PhoneInput'; import { SingleSelect } from '../../Inputs/SingleSelect/SingleSelect'; import type { HTMLInputProps } from '../../Inputs/types'; import type { LabelAfterProps } from '../../LabelAfter/LabelAfter'; import { LabelAfter } from '../../LabelAfter/LabelAfter'; import { Text } from '../../Typography/Text/Text'; import { countriesData } from './data/countriesData'; import { dictionary } from './dictionary'; interface PhoneFieldContainerProps extends Omit {} interface PhoneFieldHelperTextProps extends Partial {} interface PhoneFieldInputProps extends Omit {} export interface PhoneFieldOwnProps { preferredCountries?: number[]; value: InputPhoneValueType; onChange: (value: OutputPhoneValueType) => void; onBlur?: (value: OutputPhoneValueType) => void; maxLength?: HTMLInputProps['maxLength']; minLength?: HTMLInputProps['minLength']; } /** @inheritdoc */ export interface PhoneFieldProps extends PhoneFieldContainerProps, PhoneFieldHelperTextProps, PhoneFieldInputProps, PhoneFieldOwnProps { /** * Display loading indicator. */ loading?: boolean; /** * Label displayed after the input element. */ labelAfter?: LabelAfterProps['label']; } type InputPhoneValueType = { value: string; countryCode: string; }; export type OutputPhoneValueType = InputPhoneValueType & { phoneCode: string; }; interface CountryOption { id: number; name: { [lang: string]: string; }; dialCode: string; emoji: string; code: string; } export interface Countries { [countryCode: string]: CountryOption; } function getDialCode(countryCode: string) { return countriesData?.[countryCode]?.dialCode ?? ''; } // note: default preferred 'CZ', 'HU', 'PL', 'RO', 'SK', 'GB' const defaultPreferredCountries = [43, 75, 136, 141, 151, 57]; export const PhoneField = React.forwardRef(function PhoneField( props: PhoneFieldProps, ref ) { const [formRowProps, helperTextProps, ownProps, inputProps] = destruct( props, ['as', 'justify', 'labelHidden', 'required', 'tooltip'], ['error', 'hint', 'warning'], ['preferredCountries', 'value', 'onChange', 'onBlur', 'maxLength', 'minLength'] ); formRowProps satisfies Exact, typeof formRowProps>; helperTextProps satisfies Exact, typeof helperTextProps>; ownProps satisfies Exact, typeof ownProps>; inputProps satisfies Exact< Complete< Omit & Pick >, typeof inputProps >; const { preferredCountries = defaultPreferredCountries, onChange, value } = ownProps; const [fieldValue, setFieldValue] = useState({ ...value, phoneCode: getDialCode(value.countryCode), }); const translations = useTranslations(dictionary); const { locale } = useLocale(); const getCountryLabel = useCallback( (countryTranslations: CountryOption['name']) => { return countryTranslations[locale] ?? countryTranslations['en-US'] ?? ''; }, [locale] ); const sortedCountriesData: Countries = useMemo(() => { return Object.fromEntries( Object.entries(countriesData).toSorted((a, b) => { const nameA = getCountryLabel(a[1].name); const nameB = getCountryLabel(b[1].name); return nameA.localeCompare(nameB, locale); }) ); }, [locale, getCountryLabel]); const preferredOptions = useMemo(() => { return preferredCountries.flatMap(id => { const country = Object.values(countriesData).find(country => country.id === id); return country ? [country] : []; }); }, [preferredCountries]); const restOptions = useMemo(() => { const preferred = new Set(preferredCountries); return Object.values(sortedCountriesData).filter(country => !preferred.has(country.id)); }, [preferredCountries, sortedCountriesData]); const countrySelectOptions: { label: string; value: string; emoji: string | null; items: { value: string; label: string; emoji: string | null; items: never[]; }[]; }[] = React.useMemo(() => { const transformOptions = (options: CountryOption[]) => { return options.map(option => ({ value: option.code, label: `${getCountryLabel(option.name)} ${option.dialCode}`, emoji: option.emoji, items: [], })); }; return [ { label: translations.mostFrequent, value: translations.mostFrequent, emoji: null, items: transformOptions(preferredOptions), }, { label: translations.allCountries, value: translations.allCountries, emoji: null, items: transformOptions(restOptions), }, ]; }, [preferredOptions, restOptions, translations.allCountries, translations.mostFrequent, getCountryLabel]); const handleSelectChange = (value: string | null) => { if (value === null) return; const selection = { value: fieldValue.value, phoneCode: getDialCode(value), countryCode: value, }; handleChange(selection); props?.onBlur?.(selection); }; const handleInputChange = (value: string) => { handleChange({ value: value, phoneCode: fieldValue.phoneCode, countryCode: fieldValue.countryCode, }); }; const handleChange = (phoneFieldValue: OutputPhoneValueType) => { setFieldValue(phoneFieldValue); onChange(phoneFieldValue); }; // On Windows, Emoji flags are not supported const hasPolyfill = polyfillCountryFlagEmojis(); return (
option.value} getOptionLabel={option => option.label} getGroupOptions={option => option.items} label={inputProps.label} renderOption={option => ( {option.emoji ? `${option.emoji} ` : null} {option.label} )} renderValue={(option, labelledById) => ( <> {option.emoji ? `${option.emoji} ` : option.label} {option.label} )} /> {fieldValue.phoneCode} ownProps?.onBlur?.(fieldValue)} disabled={inputProps.disabled} readOnly={inputProps.readOnly} placeholder={inputProps.placeholder} name={inputProps.name} autoFocus={inputProps.autoFocus} minLength={ownProps.minLength} maxLength={ownProps.maxLength} /> {inputProps.labelAfter && }
); });