UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

387 lines (386 loc) 13 kB
"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