@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
387 lines (386 loc) • 13 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 classnames from 'classnames';
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 useTranslation from "../../hooks/useTranslation.js";
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 {
label: 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();
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;
}
}
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 joinValue([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();
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,
continuousValidation,
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 ? joinValue([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(/^\+/, '');
const item = dataRef.current.find(item => {
const cdc = item?.country?.cdc;
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(joinValue([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(({
data
}) => {
const countryCode = countryCodeRef.current = data?.selectedKey?.trim() || emptyValue;
currentCountryRef.current = data?.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 cdcVal = /\+\d{1,3}\s{1}\d+/.test(value) ? splitValue(value)[0] : value;
const country = countries.find(({
cdc
}) => cdc === cdcVal);
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: classnames('dnb-forms-field-phone-number', className),
width: 'stretch',
label,
labelDescription,
labelSrOnly,
help: undefined,
...pickSpacingProps(props)
};
return React.createElement(CompositionField, compositionFieldProps, !omitCountryCodeField && React.createElement(Autocomplete, {
className: classnames('dnb-forms-field-phone-number__country-code', countryCodeFieldClassName),
mode: "async",
placeholder: countryCodePlaceholder,
label_direction: "vertical",
label: countryCodeLabel === false ? defaultCountryCodeLabel : countryCodeLabel !== null && countryCodeLabel !== void 0 ? countryCodeLabel : defaultCountryCodeLabel,
label_sr_only: countryCodeLabel === false ? true : undefined,
data: dataRef.current,
value: countryCodeRef.current,
status: hasError ? 'error' : undefined,
disabled: disabled,
on_focus: handleCountryCodeFocus,
on_blur: handleOnBlur,
on_change: handleCountryCodeChange,
on_type: onTypeHandler,
independent_width: true,
search_numbers: true,
keep_selection: true,
selectall: true,
autoComplete: "tel-country-code",
no_animation: props.noAnimation,
size: size
}), React.createElement(StringField, {
className: classnames('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(12).fill(/\d/),
onFocus: handleOnFocus,
onBlur: handleOnBlur,
onChange: handleNumberChange,
value: numberRef.current,
innerRef: 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: continuousValidation || 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,
selected_value: `${country.iso} (${code})`,
search_content: [code, name],
content: [name, code],
country
};
}
function formatCountryCode(value) {
return `+${value}`;
}
function splitValue(value) {
return (typeof value === 'string' ? value.match(/^(\+[^ ]+)? ?(.*)$/) : [undefined, '', '']).slice(1);
}
function joinValue(array) {
return array.filter(Boolean).join(' ');
}
PhoneNumber._supportsSpacingProps = undefined;
export default PhoneNumber;
//# sourceMappingURL=PhoneNumber.js.map