@input-kit/phone 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/dist/index.cjs +44 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +44 -16
- package/dist/index.js.map +1 -1
- package/package.json +90 -85
package/README.md
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
# @input-kit/phone
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@input-kit/phone)
|
|
4
|
+
[](https://www.npmjs.com/package/@input-kit/phone)
|
|
5
|
+
[](https://github.com/harshit-d3v/input-kit-phone/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/harshit-d3v/input-kit-phone/blob/main/LICENSE)
|
|
7
|
+
[](https://github.com/harshit-d3v/input-kit-phone)
|
|
8
|
+
|
|
3
9
|
Headless React phone input with a complete world country-code dataset, searchable country selection, and `libphonenumber-js` powered formatting and validation.
|
|
4
10
|
|
|
5
|
-
Source, issues, and contributions: **[github.com/harshit-d3v/input-kit-phone](https://github.com/harshit-d3v/input-kit-phone)**
|
|
11
|
+
Source, issues, and contributions: **[github.com/harshit-d3v/input-kit-phone](https://github.com/harshit-d3v/input-kit-phone)** — if this package saves you time, a ⭐ there helps others find it.
|
|
6
12
|
|
|
7
13
|
## Latest update
|
|
8
14
|
|
|
15
|
+
**0.4.0** — **correctness fixes, please upgrade.** E.164 was built by concatenating the dial code onto whatever was typed, so the national trunk prefix people actually type survived into the international form: UK `07400123456` became `+4407400123456`, and India, Germany, France and Australia were wrong the same way. Around 106 countries were affected, validation still reported these as valid, and North American numbers never were — which is why it went unnoticed. Now resolved through libphonenumber metadata, so countries that genuinely keep a leading zero (Italy) still do. Also fixes: an inline `onValidationChange` causing "Maximum update depth exceeded"; input past the country maximum being discarded instead of truncated, which silently blanked an empty field on paste; a country selection that reverted on international numbers; the country button not announcing the selected country to screen readers; focus dropping to the top of the page when the dropdown closed; and types failing to resolve for CommonJS TypeScript consumers under `node16`/`nodenext`.
|
|
16
|
+
|
|
9
17
|
**0.3.0** — auto-detect respects manual country selection, metadata-based length validation (no more false `too_long` in variable-length countries), input capped at the country's maximum length, `isPhoneTooLong()`, SSR-safe caret handling, dropdown `Home`/`End` + search→list keyboard navigation, unminified published output.
|
|
10
18
|
|
|
11
19
|
**0.2.2** — npm README cleanup: release notes stay inline; removed pointers to repo-only markdown files.
|
|
@@ -33,7 +41,7 @@ npm install @input-kit/phone
|
|
|
33
41
|
|
|
34
42
|
```tsx
|
|
35
43
|
import { PhoneInput } from '@input-kit/phone';
|
|
36
|
-
import '
|
|
44
|
+
import './phone-input.css'; // your own stylesheet — the package ships no CSS
|
|
37
45
|
|
|
38
46
|
function Example() {
|
|
39
47
|
return (
|
package/dist/index.cjs
CHANGED
|
@@ -395,9 +395,11 @@ function addDialCode(phone, country) {
|
|
|
395
395
|
if (!normalized) return "";
|
|
396
396
|
if (normalized === "+") return "";
|
|
397
397
|
if (normalized.startsWith("+")) return normalized;
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
398
|
+
const parsed = libphonenumberJs.parsePhoneNumberFromString(normalized, getCountryCode(country));
|
|
399
|
+
if (parsed) {
|
|
400
|
+
return parsed.number;
|
|
401
|
+
}
|
|
402
|
+
return `${country.dialCode}${cleanPhone(normalized)}`;
|
|
401
403
|
}
|
|
402
404
|
function removeDialCode(phone, country) {
|
|
403
405
|
if (!country) return phone;
|
|
@@ -690,13 +692,21 @@ function usePhoneInput(options = {}) {
|
|
|
690
692
|
() => validatePhoneNumber(phone, country, required, validator),
|
|
691
693
|
[phone, country, required, validator]
|
|
692
694
|
);
|
|
695
|
+
const onValidationChangeRef = react.useRef(onValidationChange);
|
|
696
|
+
useIsomorphicLayoutEffect(() => {
|
|
697
|
+
onValidationChangeRef.current = onValidationChange;
|
|
698
|
+
});
|
|
693
699
|
react.useEffect(() => {
|
|
694
|
-
|
|
695
|
-
}, [validation
|
|
700
|
+
onValidationChangeRef.current?.(validation);
|
|
701
|
+
}, [validation]);
|
|
696
702
|
const localizedCountry = react.useMemo(
|
|
697
703
|
() => localizeCountry(country, locale),
|
|
698
704
|
[country, locale]
|
|
699
705
|
);
|
|
706
|
+
const countryButtonAriaLabel = react.useMemo(
|
|
707
|
+
() => localizedCountry ? `${resolvedLabels.countryButtonAriaLabel}, ${localizedCountry.name} ${localizedCountry.dialCode}` : resolvedLabels.countryButtonAriaLabel,
|
|
708
|
+
[localizedCountry, resolvedLabels.countryButtonAriaLabel]
|
|
709
|
+
);
|
|
700
710
|
const filteredCountries = react.useMemo(
|
|
701
711
|
() => filterCountries(localizedCountries, searchQuery, preferredCountries),
|
|
702
712
|
[localizedCountries, searchQuery, preferredCountries]
|
|
@@ -746,16 +756,20 @@ function usePhoneInput(options = {}) {
|
|
|
746
756
|
(selectedCountry) => {
|
|
747
757
|
const rawCountry = getCountryByCode(selectedCountry.code) ?? selectedCountry;
|
|
748
758
|
manualCountrySelectionRef.current = true;
|
|
759
|
+
const rewritten = phone.trim().startsWith("+") ? addDialCode(removeDialCode(phone, country), rawCountry) : phone;
|
|
760
|
+
if (rewritten !== phone && !isControlled) {
|
|
761
|
+
setInternalPhone(rewritten);
|
|
762
|
+
}
|
|
749
763
|
setCountry(rawCountry);
|
|
750
764
|
setIsOpen(false);
|
|
751
765
|
setSearchQuery("");
|
|
752
766
|
onCountryChange?.(getOutputCountry(rawCountry));
|
|
753
767
|
onChange?.(
|
|
754
|
-
includeDialCode ? addDialCode(
|
|
768
|
+
includeDialCode ? addDialCode(rewritten, rawCountry) : rewritten,
|
|
755
769
|
getOutputCountry(rawCountry)
|
|
756
770
|
);
|
|
757
771
|
},
|
|
758
|
-
[phone, onChange, onCountryChange, includeDialCode, getOutputCountry]
|
|
772
|
+
[phone, country, isControlled, onChange, onCountryChange, includeDialCode, getOutputCountry]
|
|
759
773
|
);
|
|
760
774
|
const toggleDropdown = react.useCallback(() => {
|
|
761
775
|
setIsOpen((prev) => !prev);
|
|
@@ -781,18 +795,23 @@ function usePhoneInput(options = {}) {
|
|
|
781
795
|
const rawValue = event.target.value;
|
|
782
796
|
const caretPosition = event.target.selectionStart ?? rawValue.length;
|
|
783
797
|
const unformatted = rawValue.trim().startsWith("+") ? `+${rawValue.replace(/\D/g, "")}` : unformatPhone(rawValue);
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
const
|
|
798
|
+
const isInternationalEntry = unformatted.startsWith("+") || unformatted.startsWith("00");
|
|
799
|
+
if (!isInternationalEntry && cleanPhone(unformatted).length > cleanPhone(phone).length && isPhoneTooLong(unformatted, country)) {
|
|
800
|
+
const truncated = limitInputLength(unformatted, country);
|
|
801
|
+
const truncatedDisplay = formatPhone(truncated, country);
|
|
802
|
+
event.target.value = truncatedDisplay;
|
|
803
|
+
const restoredCaret = Math.min(Math.max(caretPosition - 1, 0), truncatedDisplay.length);
|
|
787
804
|
if (typeof event.target.setSelectionRange === "function") {
|
|
788
805
|
event.target.setSelectionRange(restoredCaret, restoredCaret);
|
|
789
806
|
}
|
|
807
|
+
pendingCaretDigitIndexRef.current = countDigitsBeforePosition(truncatedDisplay, restoredCaret);
|
|
808
|
+
setPhone(truncated);
|
|
790
809
|
return;
|
|
791
810
|
}
|
|
792
811
|
pendingCaretDigitIndexRef.current = countDigitsBeforePosition(rawValue, caretPosition);
|
|
793
812
|
setPhone(unformatted);
|
|
794
813
|
},
|
|
795
|
-
[setPhone, phone, country
|
|
814
|
+
[setPhone, phone, country]
|
|
796
815
|
);
|
|
797
816
|
const handleFocus = react.useCallback(() => {
|
|
798
817
|
onFocus?.();
|
|
@@ -866,13 +885,13 @@ function usePhoneInput(options = {}) {
|
|
|
866
885
|
onClick: toggleDropdown,
|
|
867
886
|
"aria-expanded": isOpen,
|
|
868
887
|
"aria-haspopup": "listbox",
|
|
869
|
-
"aria-label":
|
|
888
|
+
"aria-label": countryButtonAriaLabel
|
|
870
889
|
},
|
|
871
890
|
countrySelectorProps: {
|
|
872
891
|
onClick: toggleDropdown,
|
|
873
892
|
"aria-expanded": isOpen,
|
|
874
893
|
"aria-haspopup": "listbox",
|
|
875
|
-
"aria-label":
|
|
894
|
+
"aria-label": countryButtonAriaLabel
|
|
876
895
|
},
|
|
877
896
|
dropdownProps: {
|
|
878
897
|
role: "listbox",
|
|
@@ -911,6 +930,7 @@ var PhoneInput = react.forwardRef(
|
|
|
911
930
|
const containerRef = react.useRef(null);
|
|
912
931
|
const inputRef = react.useRef(null);
|
|
913
932
|
const optionRefs = react.useRef([]);
|
|
933
|
+
const countryButtonRef = react.useRef(null);
|
|
914
934
|
const [highlightedIndex, setHighlightedIndex] = react.useState(0);
|
|
915
935
|
const listboxId = react.useId();
|
|
916
936
|
const {
|
|
@@ -953,10 +973,14 @@ var PhoneInput = react.forwardRef(
|
|
|
953
973
|
const blur = react.useCallback(() => {
|
|
954
974
|
inputRef.current?.blur();
|
|
955
975
|
}, []);
|
|
976
|
+
const returnFocusToButton = react.useCallback(() => {
|
|
977
|
+
countryButtonRef.current?.focus();
|
|
978
|
+
}, []);
|
|
956
979
|
const handleCountryClick = react.useCallback((c) => {
|
|
957
980
|
selectCountry(c);
|
|
958
981
|
setHighlightedIndex(0);
|
|
959
|
-
|
|
982
|
+
returnFocusToButton();
|
|
983
|
+
}, [selectCountry, returnFocusToButton]);
|
|
960
984
|
const handleCountryKeyDown = react.useCallback((e, c, index) => {
|
|
961
985
|
switch (e.key) {
|
|
962
986
|
case "Enter":
|
|
@@ -964,6 +988,7 @@ var PhoneInput = react.forwardRef(
|
|
|
964
988
|
e.preventDefault();
|
|
965
989
|
selectCountry(c);
|
|
966
990
|
setHighlightedIndex(0);
|
|
991
|
+
returnFocusToButton();
|
|
967
992
|
break;
|
|
968
993
|
case "ArrowDown":
|
|
969
994
|
e.preventDefault();
|
|
@@ -996,9 +1021,10 @@ var PhoneInput = react.forwardRef(
|
|
|
996
1021
|
case "Escape":
|
|
997
1022
|
e.preventDefault();
|
|
998
1023
|
closeDropdown();
|
|
1024
|
+
returnFocusToButton();
|
|
999
1025
|
break;
|
|
1000
1026
|
}
|
|
1001
|
-
}, [selectCountry, filteredCountries.length, closeDropdown]);
|
|
1027
|
+
}, [selectCountry, filteredCountries.length, closeDropdown, returnFocusToButton]);
|
|
1002
1028
|
react.useEffect(() => {
|
|
1003
1029
|
if (!isOpen) {
|
|
1004
1030
|
return;
|
|
@@ -1050,13 +1076,15 @@ var PhoneInput = react.forwardRef(
|
|
|
1050
1076
|
} else if (e.key === "Escape") {
|
|
1051
1077
|
e.preventDefault();
|
|
1052
1078
|
closeDropdown();
|
|
1079
|
+
returnFocusToButton();
|
|
1053
1080
|
}
|
|
1054
|
-
}, [closeDropdown]);
|
|
1081
|
+
}, [closeDropdown, returnFocusToButton]);
|
|
1055
1082
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className: containerClassName, "data-valid": isValid, children: [
|
|
1056
1083
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1057
1084
|
"button",
|
|
1058
1085
|
{
|
|
1059
1086
|
...countryButtonProps,
|
|
1087
|
+
ref: countryButtonRef,
|
|
1060
1088
|
type: "button",
|
|
1061
1089
|
className: selectorClassName,
|
|
1062
1090
|
disabled,
|