UNPKG

phone-input-lib

Version:

A lightweight, developer-friendly npm library that provides a phone number input component with flags, country codes, and Tailwind support.

816 lines (805 loc) 24.7 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { CSS_PATH: () => CSS_PATH, CountrySelect: () => CountrySelect, FlagIcon: () => FlagIcon, PhoneInput: () => PhoneInput, countries: () => countries_default, mapCountryData: () => mapCountryData, mappedDefaultCountry: () => mappedDefaultCountry }); module.exports = __toCommonJS(index_exports); // src/components/PhoneInput.tsx var import_react2 = require("react"); // src/components/CountrySelect.tsx var import_react = require("react"); // src/data/countries.ts var import_countries_ts = require("countries-ts"); // src/utils/mapCountryData.ts var mapCountryData = (country) => { const mappedData = { dialCode: country.countryCode, flag: country.flag, name: country.label, code: country.code }; return mappedData; }; // src/data/countries.ts var countries = (0, import_countries_ts.listCountries)().map((country) => { return mapCountryData(country); }); var defaultCountry = (0, import_countries_ts.getByCountry)("India"); var mappedDefaultCountry = mapCountryData(defaultCountry); var countries_default = countries; // src/components/FlagIcon.tsx var import_jsx_runtime = require("react/jsx-runtime"); var FlagIcon = ({ country, className = "", showFlags = true }) => { if (!showFlags) { return null; } return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className, role: "img", "aria-label": `${country.name} flag`, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "img", { src: country.flag, alt: country.name, style: { width: "20px", objectFit: "contain" } } ) }); }; // src/components/CountrySelect.tsx var import_jsx_runtime2 = require("react/jsx-runtime"); var sizeStyles = { sm: { trigger: { padding: "0.375rem 0.5rem" }, fontSize: "0.75rem", iconSize: "0.875rem" }, md: { trigger: { padding: "0.5rem 0.75rem" }, fontSize: "0.875rem", iconSize: "1rem" }, lg: { trigger: { padding: "0.75rem 1rem" }, fontSize: "1rem", iconSize: "1.125rem" } }; var styles = { container: { position: "relative", display: "inline-block", width: "100%" }, trigger: { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", // lineHeight: '1.25rem', color: "#374151", backgroundColor: "#ffffff", border: "1px solid #d1d5db", borderRadius: "0.5rem", cursor: "pointer", transition: "all 0.15s ease-in-out", outline: "none", minHeight: "2.5rem" }, triggerHover: { borderColor: "#9ca3af" }, triggerFocused: { borderColor: "#3b82f6", boxShadow: "0 0 0 3px rgba(59, 130, 246, 0.1)" }, triggerError: { borderColor: "#ef4444", boxShadow: "0 0 0 3px rgba(239, 68, 68, 0.1)" }, triggerDisabled: { backgroundColor: "#f9fafb", color: "#9ca3af", cursor: "not-allowed", opacity: 0.6 }, content: { display: "flex", alignItems: "center", gap: "0.5rem", flex: 1, minWidth: 0 }, chevron: { width: "1rem", height: "1rem", transition: "transform 0.2s ease-in-out", fill: "currentColor", flexShrink: 0 }, chevronOpen: { transform: "rotate(180deg)" }, loadingSpinner: { width: "1rem", height: "1rem", border: "2px solid #e5e7eb", borderTop: "2px solid #3b82f6", borderRadius: "50%", animation: "spin 1s linear infinite" }, dropdown: { position: "absolute", top: "100%", left: "0", right: "0", marginTop: "0.25rem", backgroundColor: "#ffffff", border: "1px solid #d1d5db", borderRadius: "0.5rem", boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)", zIndex: 1e3, overflowY: "auto" }, searchContainer: { padding: "0.5rem", borderBottom: "1px solid #e5e7eb", position: "sticky", top: 0, backgroundColor: "#ffffff", zIndex: 1 }, searchInput: { width: "100%", padding: "0.5rem", fontSize: "0.875rem", border: "1px solid #d1d5db", borderRadius: "0.375rem", outline: "none", backgroundColor: "#ffffff" }, searchInputFocused: { borderColor: "#3b82f6", boxShadow: "0 0 0 2px rgba(59, 130, 246, 0.1)" }, optionsList: { maxHeight: "200px", overflowY: "auto" }, option: { display: "flex", alignItems: "center", gap: "0.5rem", padding: "0.5rem 0.75rem", fontSize: "0.875rem", color: "#374151", cursor: "pointer", transition: "background-color 0.15s ease-in-out" }, optionHover: { backgroundColor: "#f3f4f6" }, optionSelected: { backgroundColor: "#eff6ff", color: "#1d4ed8", fontWeight: "500" }, optionText: { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", flex: 1 }, optionDialCode: { color: "#6b7280", fontSize: "0.8rem", flexShrink: 0 }, noResults: { padding: "0.75rem", textAlign: "center", color: "#6b7280", fontSize: "0.875rem" }, placeholder: { color: "#9ca3af" } }; var CountrySelect = ({ value, onChange, defaultValue, className = "", showFlags = true, disabled = false, placeholder = "Select country", searchable = false, error = false, loading = false, size = "md", maxDropdownHeight = 300, showDialCodes = true, filterCountries, "aria-label": ariaLabel, required = false, id, style, name, onFocus, onBlur, onOpen, onClose }) => { const [isOpen, setIsOpen] = (0, import_react.useState)(false); const [hoveredIndex, setHoveredIndex] = (0, import_react.useState)(-1); const [isFocused, setIsFocused] = (0, import_react.useState)(false); const [searchQuery, setSearchQuery] = (0, import_react.useState)(""); const [internalValue, setInternalValue] = (0, import_react.useState)( defaultValue ); const containerRef = (0, import_react.useRef)(null); const searchInputRef = (0, import_react.useRef)(null); const dropdownRef = (0, import_react.useRef)(null); const isControlled = value !== void 0; const currentValue = isControlled ? value : internalValue; const processedCountries = (0, import_react.useMemo)(() => { let processedList = [...countries_default]; if (filterCountries) { processedList = filterCountries(processedList); } processedList.sort((a, b) => { const priorityA = a.priority || 999; const priorityB = b.priority || 999; if (priorityA !== priorityB) { return priorityA - priorityB; } return a.name.localeCompare(b.name); }); return processedList; }, [filterCountries]); const filteredCountries = (0, import_react.useMemo)(() => { if (!searchQuery.trim()) { return processedCountries; } const query = searchQuery.toLowerCase().trim(); return processedCountries.filter( (country) => country.name.toLowerCase().includes(query) || country.code.toLowerCase().includes(query) || country.dialCode.includes(query) ); }, [processedCountries, searchQuery]); const selectedCountry = (0, import_react.useMemo)(() => { return processedCountries.find((country) => country.code === currentValue) || null; }, [processedCountries, currentValue]); const handleValueChange = (0, import_react.useCallback)( (countryCode, country) => { console.log("CountrySelect: handleValueChange called with", { countryCode, country }); if (!isControlled) { setInternalValue(countryCode); } onChange(countryCode, country); }, [isControlled, onChange] ); (0, import_react.useEffect)(() => { const handleClickOutside = (event) => { if (containerRef.current && !containerRef.current.contains(event.target)) { handleClose(); } }; if (isOpen) { document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); } }, [isOpen]); (0, import_react.useEffect)(() => { if (isOpen) { const selectedIndex = filteredCountries.findIndex( (country) => country.code === currentValue ); setHoveredIndex(selectedIndex >= 0 ? selectedIndex : 0); if (searchable && searchInputRef.current) { requestAnimationFrame(() => { var _a; (_a = searchInputRef.current) == null ? void 0 : _a.focus(); }); } } else { setHoveredIndex(-1); setSearchQuery(""); } }, [isOpen, currentValue, filteredCountries, searchable]); const handleOpen = (0, import_react.useCallback)(() => { if (disabled || loading) return; setIsOpen(true); setIsFocused(true); onOpen == null ? void 0 : onOpen(); }, [disabled, loading, onOpen]); const handleClose = (0, import_react.useCallback)(() => { setIsOpen(false); setIsFocused(false); onClose == null ? void 0 : onClose(); }, [onClose]); const handleFocus = (0, import_react.useCallback)(() => { setIsFocused(true); onFocus == null ? void 0 : onFocus(); }, [onFocus]); const handleBlur = (0, import_react.useCallback)(() => { if (!isOpen) { setIsFocused(false); onBlur == null ? void 0 : onBlur(); } }, [isOpen, onBlur]); const handleKeyDown = (0, import_react.useCallback)( (event) => { if (disabled || loading) return; switch (event.key) { case "Enter": case " ": event.preventDefault(); if (isOpen && hoveredIndex >= 0 && filteredCountries[hoveredIndex]) { const selectedCountry2 = filteredCountries[hoveredIndex]; handleValueChange(selectedCountry2.code, selectedCountry2); handleClose(); } else if (!isOpen) { handleOpen(); } break; case "ArrowDown": event.preventDefault(); if (!isOpen) { handleOpen(); } else { setHoveredIndex((prev) => { const nextIndex = prev < filteredCountries.length - 1 ? prev + 1 : 0; scrollToOption(nextIndex); return nextIndex; }); } break; case "ArrowUp": event.preventDefault(); if (!isOpen) { handleOpen(); } else { setHoveredIndex((prev) => { const nextIndex = prev > 0 ? prev - 1 : filteredCountries.length - 1; scrollToOption(nextIndex); return nextIndex; }); } break; case "Escape": event.preventDefault(); handleClose(); break; case "Tab": if (isOpen) { handleClose(); } break; default: if (searchable && !isOpen && event.key.length === 1) { handleOpen(); } break; } }, [ disabled, loading, isOpen, hoveredIndex, filteredCountries, handleValueChange, handleClose, handleOpen, searchable ] ); const scrollToOption = (0, import_react.useCallback)((index) => { if (dropdownRef.current) { const option = dropdownRef.current.querySelector( `[data-option-index="${index}"]` ); if (option) { option.scrollIntoView({ block: "nearest" }); } } }, []); const handleSearchChange = (0, import_react.useCallback)( (event) => { const query = event.target.value; setSearchQuery(query); setHoveredIndex(0); }, [] ); const handleSearchKeyDown = (0, import_react.useCallback)( (event) => { if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Enter" || event.key === "Escape") { handleKeyDown(event); } }, [handleKeyDown] ); const handleTriggerClick = (0, import_react.useCallback)(() => { if (disabled || loading) return; if (isOpen) { handleClose(); } else { handleOpen(); } }, [disabled, loading, isOpen, handleClose, handleOpen]); const handleOptionClick = (0, import_react.useCallback)( (country) => { console.log("Option clicked:", country); handleValueChange(country.code, country); handleClose(); }, [handleValueChange, handleClose] ); const handleOptionHover = (0, import_react.useCallback)((index) => { setHoveredIndex(index); }, []); const sizeConfig = sizeStyles[size]; const triggerStyle = { ...styles.trigger, ...sizeConfig.trigger, fontSize: sizeConfig.fontSize, ...disabled ? styles.triggerDisabled : {}, ...error && !disabled ? styles.triggerError : {}, ...isFocused && !disabled && !error ? styles.triggerFocused : {}, ...style }; const chevronStyle = { ...styles.chevron, width: sizeConfig.iconSize, height: sizeConfig.iconSize, ...isOpen ? styles.chevronOpen : {} }; const dropdownStyle = { ...styles.dropdown, maxHeight: `${maxDropdownHeight}px` }; const getDisplayText = () => { if (!selectedCountry) { return placeholder; } return `(${selectedCountry.dialCode})`; }; return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: ` @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } ` }), /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)( "div", { ref: containerRef, className, style: styles.container, id, children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)( "div", { role: "combobox", "aria-expanded": isOpen, "aria-haspopup": "listbox", "aria-label": ariaLabel || "Select country", "aria-required": required, "aria-invalid": error, tabIndex: disabled ? -1 : 0, style: triggerStyle, onClick: handleTriggerClick, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur, onMouseEnter: () => !disabled && setIsFocused(true), onMouseLeave: () => !isOpen && setIsFocused(false), children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles.content, children: [ showFlags && selectedCountry && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FlagIcon, { country: selectedCountry, showFlags }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "span", { style: { ...styles.optionText, ...selectedCountry ? {} : styles.placeholder, fontSize: sizeConfig.fontSize }, children: getDisplayText() } ) ] }), loading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles.loadingSpinner }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { style: chevronStyle, viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M7 10l5 5 5-5z" }) }) ] } ), isOpen && !disabled && !loading && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "div", { ref: dropdownRef, style: dropdownStyle, role: "listbox", "aria-label": "Country options", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles.optionsList, children: filteredCountries.length > 0 ? filteredCountries.map((country, index) => { const isSelected = country.code === currentValue; const isHovered = index === hoveredIndex; const optionStyle = { ...styles.option, ...isHovered ? styles.optionHover : {}, ...isSelected ? styles.optionSelected : {} }; return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)( "div", { "data-option-index": index, role: "option", "aria-selected": isSelected, style: optionStyle, onClick: () => handleOptionClick(country), onMouseEnter: () => handleOptionHover(index), children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles.optionText, children: country.name }), showDialCodes && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles.optionDialCode, children: country.dialCode }) ] }, country.code ); }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: styles.noResults, children: "No countries found" }) }) } ), /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)( "select", { name, value: currentValue || "", onChange: () => { }, tabIndex: -1, required, style: { position: "absolute", left: "-9999px", opacity: 0, pointerEvents: "none" }, "aria-hidden": "true", children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "", children: placeholder }), processedCountries.map((country) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("option", { value: country.code, children: [ country.name, " (", country.dialCode, ")" ] }, country.code)) ] } ) ] } ) ] }); }; // src/hooks/usePhoneValidation.ts var import_libphonenumber_js = require("libphonenumber-js"); var validatePhone = ({ value, country }) => { try { if (!country) { return { isValid: false, formattedValue: value, nationalFormat: "", internationalFormat: "", e164Format: "", countryCode: "" }; } const code = country.code; const formattedValue = (0, import_libphonenumber_js.formatIncompletePhoneNumber)(value, code); const phoneNumber = (0, import_libphonenumber_js.parseIncompletePhoneNumber)(value); const isValid = (0, import_libphonenumber_js.validatePhoneNumberLength)( `${country.dialCode}${phoneNumber}`, code ); if (phoneNumber) { return { isValid: isValid === "TOO_SHORT" || isValid === void 0, formattedValue, nationalFormat: phoneNumber, internationalFormat: phoneNumber, e164Format: phoneNumber, countryCode: phoneNumber || "" }; } else { return { isValid: false, formattedValue, nationalFormat: "", internationalFormat: "", e164Format: "", countryCode: "" }; } } catch (error) { return { isValid: false, formattedValue: value, nationalFormat: "", internationalFormat: "", e164Format: "", countryCode: "" }; } }; // src/components/PhoneInput.tsx var import_jsx_runtime3 = require("react/jsx-runtime"); var PhoneInput = ({ value = "", onChange, defaultCountry: defaultCountry2 = "US", className = "", inputClassName = "", selectClassName = "", showFlags = true, disabled = false, placeholder = "Phone number", required = false, error = false, id, style, inputId, inputStyle, inputName, selectId, selectStyle, selectName }) => { const getInitialCountry = (0, import_react2.useCallback)(() => { return countries_default.find((c) => c.code === defaultCountry2) || countries_default[0]; }, [defaultCountry2]); const [selectedCountry, setSelectedCountry] = (0, import_react2.useState)(getInitialCountry); const [phoneNumber, setPhoneNumber] = (0, import_react2.useState)(""); const [validationError, setValidationError] = (0, import_react2.useState)(false); const parseIncomingValue = (0, import_react2.useCallback)( (incomingValue) => { if (!incomingValue || incomingValue.trim() === "") { return ""; } if (incomingValue.startsWith("+")) { const dialCodeRegex = new RegExp(`^\\+${selectedCountry.dialCode}\\s*`); const phoneNumberPart = incomingValue.replace(dialCodeRegex, "").trim(); return phoneNumberPart; } return incomingValue.trim(); }, [selectedCountry.dialCode] ); (0, import_react2.useEffect)(() => { const parsedNumber = parseIncomingValue(value); setPhoneNumber(parsedNumber); }, [value, parseIncomingValue]); (0, import_react2.useEffect)(() => { const newCountry = getInitialCountry(); setSelectedCountry(newCountry); }, [getInitialCountry]); const handleCountryChange = (0, import_react2.useCallback)( (countryCode, countryData) => { console.log("PhoneInput: Country change handler called", { countryCode, countryData }); setSelectedCountry(countryData); if (phoneNumber) { const fullValue = `${countryData.dialCode} ${phoneNumber}`; onChange(fullValue); } else { onChange(`${countryData.dialCode}`); } }, [phoneNumber, onChange] ); const handlePhoneNumberChange = (0, import_react2.useCallback)( (e) => { const inputValue = e.target.value; const cleanValue = inputValue.replace(/[^0-9\s\-\(\)\.]/g, ""); setPhoneNumber(cleanValue); const fullValue = cleanValue ? `${selectedCountry.dialCode} ${cleanValue}` : `${selectedCountry.dialCode}`; onChange(fullValue); if (cleanValue) { try { const validationResult = validatePhone({ value: cleanValue, country: selectedCountry }); setValidationError(!validationResult.isValid); } catch (error2) { setValidationError(true); } } else { setValidationError(false); } }, [selectedCountry, onChange] ); console.log("PhoneInput render - Selected Country:", selectedCountry); return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className, id, style, children: [ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)( CountrySelect, { value: selectedCountry.code, onChange: handleCountryChange, className: selectClassName, showFlags, disabled, id: selectId, style: selectStyle, name: selectName, searchable: true, showDialCodes: true, placeholder: "Select country" } ), /* @__PURE__ */ (0, import_jsx_runtime3.jsx)( "input", { type: "tel", value: phoneNumber, onChange: handlePhoneNumberChange, className: `${inputClassName} ${validationError || error ? "error" : ""}`, disabled, placeholder, required, id: inputId, style: inputStyle, name: inputName, autoComplete: "tel" } ) ] }); }; // src/index.ts var CSS_PATH = "./styles/phone-input.css"; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { CSS_PATH, CountrySelect, FlagIcon, PhoneInput, countries, mapCountryData, mappedDefaultCountry }); //# sourceMappingURL=index.js.map