@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
411 lines (410 loc) • 13.8 kB
JavaScript
"use client";
import React, { useMemo, useContext, useCallback, useEffect, useRef } from 'react';
import * as z from 'zod';
import { Autocomplete } from "../../../../components/index.js";
import clsx from 'clsx';
import useCountries from "../SelectCountry/useCountries.js";
import StringField from "../String/index.js";
import CompositionField from "../Composition/index.js";
import { useFieldProps } from "../../hooks/index.js";
import { pickSpacingProps } from "../../../../components/flex/utils.js";
import SharedContext from "../../../../shared/Context.js";
import { countryFilter, getCountryData } from "../SelectCountry/index.js";
import detectCountryCode from "../../../../shared/detectCountryCode.js";
import useTranslation from "../../hooks/useTranslation.js";
import withComponentMarkers from "../../../../shared/helpers/withComponentMarkers.js";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const defaultCountryCode = '+47';
const defaultPlaceholder = '00 00 00 00';
const defaultMask = [/\d/, /\d/, ' ', /\d/, /\d/, ' ', /\d/, /\d/, ' ', /\d/, /\d/];
function PhoneNumber(props = {}) {
var _props$inputRef;
const sharedContext = useContext(SharedContext);
const {
numberLabel: defaultLabel,
countryCodeLabel: defaultCountryCodeLabel,
errorRequired
} = useTranslation().PhoneNumber;
const lang = sharedContext.locale?.split('-')[0];
const countryCodeRef = useRef(props.emptyValue);
const prevCountryCodeRef = useRef(countryCodeRef.current);
const numberRef = useRef(props.emptyValue);
const dataRef = useRef(null);
const langRef = useRef(lang);
const wasFilled = useRef(false);
const currentCountryRef = useRef(undefined);
const errorMessages = useMemo(() => ({
'Field.errorRequired': errorRequired,
'Field.errorPattern': errorRequired,
...props.errorMessages
}), [errorRequired, props.errorMessages]);
const validateRequired = useCallback((value, {
required,
isChanged,
error
}) => {
if (required) {
const [countryCode, phoneNumber] = splitValue(value);
if (countryCode !== prevCountryCodeRef.current) {
if (countryCode) {
prevCountryCodeRef.current = countryCode;
}
return undefined;
}
if (isChanged && !phoneNumber) {
return error;
}
}
return undefined;
}, []);
const fromExternal = useCallback(external => {
if (typeof external === 'string') {
const [countryCode, phoneNumber] = splitValue(external);
if (!countryCode && !phoneNumber && !props.omitCountryCodeField) {
return countryCodeRef.current;
}
if (countryCode && phoneNumber) {
return toE164([countryCode, phoneNumber]);
}
}
return external;
}, [props.omitCountryCodeField]);
const toEvent = useCallback(value => {
const [, phoneNumber] = splitValue(value);
if (!phoneNumber) {
return props.emptyValue;
}
return value;
}, [props.emptyValue]);
const customTransformIn = props.transformIn;
const transformIn = useCallback(value => {
if (customTransformIn) {
const external = customTransformIn(value);
if (typeof external === 'string') {
return external;
}
if (external?.phoneNumber) {
return toE164([external.countryCode, external.phoneNumber]);
}
}
return value;
}, [customTransformIn]);
const provideAdditionalArgs = useCallback(value => {
const [countryCode, phoneNumber] = splitValue(value);
return {
...(!props.omitCountryCodeField ? {
countryCode: countryCode || countryCodeRef.current || undefined
} : {}),
phoneNumber: phoneNumber || numberRef.current || undefined,
iso: currentCountryRef.current?.iso
};
}, [props.omitCountryCodeField]);
const schema = useMemo(() => {
if (props.schema) {
return props.schema;
}
if (!props.pattern) return undefined;
return p => {
let s = z.string();
if (p?.pattern) {
try {
s = s.regex(new RegExp(p.pattern, 'u'), 'Field.errorPattern');
} catch (_e) {}
}
return s;
};
}, [props.schema, props.pattern]);
const defaultProps = {
...(schema ? {
schema
} : {}),
errorMessages
};
const ref = useRef(undefined);
const preparedProps = {
...props,
...defaultProps,
validateRequired,
fromExternal,
toEvent,
provideAdditionalArgs,
transformIn,
inputRef: (_props$inputRef = props.inputRef) !== null && _props$inputRef !== void 0 ? _props$inputRef : ref
};
const {
id,
path,
itemPath,
value,
className,
inputRef,
countryCodeFieldClassName,
numberFieldClassName,
countryCodePlaceholder,
placeholder,
countryCodeLabel,
label,
labelDescription,
numberLabel,
labelSrOnly,
numberMask,
countries: ccFilter = 'Prioritized',
emptyValue,
info,
warning,
size,
error,
hasError,
disabled,
width,
help,
required,
validateInitially,
validateContinuously,
validateUnchanged,
omitCountryCodeField,
setHasFocus,
handleChange,
setDisplayValue,
onCountryCodeChange,
onNumberChange,
filterCountries
} = useFieldProps(preparedProps, {
executeOnChangeRegardlessOfUnchangedValue: true
});
useEffect(() => {
if (path || itemPath) {
const number = inputRef.current?.value;
setDisplayValue(number?.length > 0 ? `${countryCodeRef.current} ${number}` : undefined);
}
}, [inputRef, itemPath, path, setDisplayValue, value]);
const filter = useCallback(country => {
return countryFilter(country, filterCountries, ccFilter);
}, [ccFilter, filterCountries]);
const {
countries
} = useCountries();
const updateCurrentDataSet = useCallback(() => {
dataRef.current = getCountryData({
countries,
lang,
filter: ccFilter === 'Prioritized' && !wasFilled.current ? country => `${formatCountryCode(country.cdc)}` === countryCodeRef.current : filter,
sort: ccFilter,
makeObject
});
}, [countries, lang, ccFilter, filter]);
const prepareEventValues = useCallback(({
countryCode = countryCodeRef.current || emptyValue,
phoneNumber = numberRef.current || emptyValue
} = {}) => {
if (!currentCountryRef.current) {
const cdcVal = countryCode?.replace(/^\+/, '').replace(/-/g, '');
const item = dataRef.current.find(item => {
const cdc = item?.country?.cdc?.replace(/-/g, '');
return cdc === cdcVal;
});
currentCountryRef.current = item?.country;
}
return {
...(!omitCountryCodeField ? {
countryCode
} : {}),
phoneNumber,
iso: currentCountryRef.current?.iso
};
}, [emptyValue, omitCountryCodeField]);
const callOnChange = useCallback(data => {
const eventValues = prepareEventValues(data);
handleChange(toEvent(toE164([eventValues.countryCode, eventValues.phoneNumber])), eventValues);
}, [prepareEventValues, handleChange]);
const callOnBlurOrFocus = useCallback(hasFocus => {
const eventValues = prepareEventValues();
setHasFocus(hasFocus, undefined, eventValues);
}, [prepareEventValues, setHasFocus]);
useMemo(() => {
const [countryCode, phoneNumber] = splitValue(props.value || value);
numberRef.current = phoneNumber;
if (lang !== langRef.current || !wasFilled.current) {
if (!countryCodeRef.current || countryCode) {
countryCodeRef.current = countryCode || defaultCountryCode;
}
langRef.current = lang;
updateCurrentDataSet();
}
}, [value, props.value, lang, updateCurrentDataSet]);
const handleCountryCodeChange = useCallback(event => {
const data = event.data;
const dataObj = data && typeof data === 'object' && 'selectedKey' in data ? data : undefined;
const countryCode = countryCodeRef.current = dataObj?.selectedKey?.trim() || emptyValue;
currentCountryRef.current = dataObj?.country;
if (!numberMask && countryCodeRef.current?.includes(defaultCountryCode) && numberRef.current?.length > 8) {
const truncatedNumber = numberRef.current.substring(0, 8);
callOnChange({
countryCode,
phoneNumber: truncatedNumber
});
onNumberChange?.(truncatedNumber);
} else {
callOnChange({
countryCode
});
}
onCountryCodeChange?.(countryCode);
}, [emptyValue, numberMask, onCountryCodeChange, callOnChange, onNumberChange]);
const handleNumberChange = useCallback(value => {
const phoneNumber = numberRef.current = value || emptyValue;
callOnChange({
phoneNumber
});
onNumberChange?.(phoneNumber);
}, [emptyValue, callOnChange, onNumberChange]);
const handleOnBlur = useCallback(() => {
callOnBlurOrFocus(false);
}, [callOnBlurOrFocus]);
const handleOnFocus = useCallback(() => {
callOnBlurOrFocus(true);
}, [callOnBlurOrFocus]);
const handleCountryCodeFocus = useCallback(({
updateData
}) => {
if (!wasFilled.current) {
wasFilled.current = true;
updateCurrentDataSet();
updateData(dataRef.current);
}
handleOnFocus();
}, [handleOnFocus, updateCurrentDataSet]);
const onTypeHandler = useCallback(({
value,
updateData,
revalidateInputValue,
event
}) => {
if (typeof event?.nativeEvent?.data === 'undefined') {
const detected = detectCountryCode(value);
const cdcVal = detected ? detected.countryCode.replace(/^\+/, '').replace(/-/g, '') : value;
const country = countries.find(({
cdc
}) => cdc.replace(/-/g, '') === cdcVal?.replace(/-/g, ''));
if (country?.cdc) {
const countryCode = countryCodeRef.current = formatCountryCode(country.cdc);
updateCurrentDataSet();
updateData(dataRef.current);
callOnChange({
countryCode
});
window.requestAnimationFrame(() => {
revalidateInputValue();
});
}
}
}, [callOnChange, countries, updateCurrentDataSet]);
const isDefault = countryCodeRef.current?.includes(defaultCountryCode);
const compositionFieldProps = {
id,
className: clsx('dnb-forms-field-phone-number', className),
width: 'stretch',
label,
labelDescription,
labelSrOnly,
help: undefined,
...pickSpacingProps(props)
};
return _jsxs(CompositionField, {
...compositionFieldProps,
children: [!omitCountryCodeField && _jsx(Autocomplete, {
className: clsx('dnb-forms-field-phone-number__country-code', countryCodeFieldClassName),
mode: "async",
placeholder: countryCodePlaceholder,
labelDirection: "vertical",
label: countryCodeLabel === false ? defaultCountryCodeLabel : countryCodeLabel !== null && countryCodeLabel !== void 0 ? countryCodeLabel : defaultCountryCodeLabel,
labelSrOnly: countryCodeLabel === false ? true : undefined,
data: dataRef.current,
value: countryCodeRef.current,
status: hasError ? 'error' : undefined,
disabled: disabled,
onFocus: handleCountryCodeFocus,
onBlur: handleOnBlur,
onChange: handleCountryCodeChange,
onType: onTypeHandler,
independentWidth: true,
searchNumbers: true,
keepSelection: true,
selectAll: true,
autoComplete: "tel-country-code",
noAnimation: props.noAnimation,
size: size
}), _jsx(StringField, {
className: clsx('dnb-forms-field-phone-number__number', numberFieldClassName),
type: "tel",
autoComplete: "tel-national",
emptyValue: emptyValue,
layout: "vertical",
label: numberLabel === false ? defaultLabel : numberLabel !== null && numberLabel !== void 0 ? numberLabel : defaultLabel,
labelSrOnly: numberLabel === false ? true : undefined,
placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : isDefault ? defaultPlaceholder : undefined,
mask: numberMask !== null && numberMask !== void 0 ? numberMask : isDefault ? defaultMask : Array(15).fill(/\d/),
onFocus: handleOnFocus,
onBlur: handleOnBlur,
onChange: handleNumberChange,
value: numberRef.current,
ref: inputRef,
info: info,
warning: warning,
error: error,
disabled: disabled,
width: width === 'stretch' ? 'stretch' : props.omitCountryCodeField && width === 'large' ? 'large' : 'medium',
help: {
...help,
breakout: false,
outset: false
},
required: required,
errorMessages: errorMessages,
validateInitially: validateInitially,
validateContinuously: validateContinuously,
validateUnchanged: validateUnchanged,
inputMode: "tel",
size: size
})]
});
}
function makeObject(country, lang) {
var _country$i18n$lang;
const name = (_country$i18n$lang = country.i18n[lang]) !== null && _country$i18n$lang !== void 0 ? _country$i18n$lang : country.i18n.en;
const code = formatCountryCode(country.cdc);
return {
selectedKey: code,
selectedValue: `${country.iso} (${code})`,
searchContent: [code, name],
content: [name, code],
country
};
}
function formatCountryCode(value) {
return `+${value}`;
}
function splitValue(value) {
if (typeof value !== 'string') {
return [undefined, ''];
}
if (value.includes(' ')) {
return [undefined, ''];
}
const detected = detectCountryCode(value);
if (detected) {
return [detected.countryCode, detected.phoneNumber];
}
if (value.startsWith('+')) {
return [value, ''];
}
return [undefined, value];
}
function toE164(array) {
return array.filter(Boolean).map(part => part.replace(/-/g, '')).join('');
}
withComponentMarkers(PhoneNumber, {
_supportsSpacingProps: undefined
});
export default PhoneNumber;
//# sourceMappingURL=PhoneNumber.js.map