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
JavaScript
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
;