intl-tel-input 27.1.2 → 27.2.0

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.
@@ -2975,7 +2975,7 @@ var UI = class _UI {
2975
2975
  this.#selectedCountryEl.focus();
2976
2976
  }
2977
2977
  }
2978
- if (!this.#options.countrySearch && REGEX.HIDDEN_SEARCH_CHAR.test(e.key)) {
2978
+ if (!this.#options.countrySearch && e.target !== this.telInputEl && REGEX.HIDDEN_SEARCH_CHAR.test(e.key)) {
2979
2979
  e.stopPropagation();
2980
2980
  if (queryTimer) {
2981
2981
  clearTimeout(queryTimer);
@@ -3483,6 +3483,8 @@ var Iti = class _Iti {
3483
3483
  #isActive = true;
3484
3484
  #abortController;
3485
3485
  #numerals;
3486
+ //* Tracks whether the user has typed/pasted their own formatting chars, so AYT-formatting should back off.
3487
+ #userOverrideFormatting = false;
3486
3488
  #autoCountryDeferred;
3487
3489
  #utilsDeferred;
3488
3490
  constructor(input, customOptions = {}) {
@@ -3672,176 +3674,211 @@ var Iti = class _Iti {
3672
3674
  this.#bindKeydownListener();
3673
3675
  this.#bindPasteListener();
3674
3676
  }
3675
- #bindInputListener() {
3676
- const {
3677
- strictMode,
3678
- formatAsYouType,
3679
- separateDialCode,
3680
- allowDropdown,
3681
- countrySearch
3682
- } = this.#options;
3683
- let userOverrideFormatting = false;
3684
- if (REGEX.ALPHA_UNICODE.test(this.#getTelInputValue())) {
3685
- userOverrideFormatting = true;
3677
+ //* Android workaround for handling plus when separateDialCode enabled (as impossible to handle with keydown/keyup, for which e.key always returns "Unidentified", see https://stackoverflow.com/q/59584061/217866)
3678
+ #handleAndroidPlusKey(inputValue) {
3679
+ this.#removeJustTypedChar(inputValue);
3680
+ this.#openDropdownWithPlus();
3681
+ }
3682
+ //* Android strictMode workaround: the keydown-based filter can't block these because e.key is "Unidentified" on Android virtual keyboards, so strip them here on input.
3683
+ #handleAndroidStrictReject(inputValue, rejectedInput) {
3684
+ const newCaretPos = this.#removeJustTypedChar(inputValue);
3685
+ this.#ui.telInputEl.setSelectionRange(newCaretPos, newCaretPos);
3686
+ this.#dispatchEvent(EVENTS.STRICT_REJECT, {
3687
+ source: "key",
3688
+ rejectedInput,
3689
+ reason: "invalid"
3690
+ });
3691
+ }
3692
+ //* Format the input value using libphonenumber's AYT formatter, preserving caret position (called after an input event).
3693
+ #formatAsYouType(inputValue, isDeleteForwards) {
3694
+ const currentCaretPos = this.#ui.telInputEl.selectionStart || 0;
3695
+ const valueBeforeCaret = inputValue.substring(0, currentCaretPos);
3696
+ const relevantCharsBeforeCaret = valueBeforeCaret.replace(
3697
+ REGEX.NON_PLUS_NUMERIC_GLOBAL,
3698
+ ""
3699
+ ).length;
3700
+ const fullNumber = this.#getFullNumber();
3701
+ const formattedValue = formatNumberAsYouType(
3702
+ fullNumber,
3703
+ inputValue,
3704
+ intlTelInput.utils,
3705
+ this.#selectedCountry,
3706
+ this.#options.separateDialCode
3707
+ );
3708
+ const newCaretPos = computeNewCaretPosition(
3709
+ relevantCharsBeforeCaret,
3710
+ formattedValue,
3711
+ currentCaretPos,
3712
+ isDeleteForwards
3713
+ );
3714
+ this.#setTelInputValue(formattedValue);
3715
+ this.#ui.telInputEl.setSelectionRange(newCaretPos, newCaretPos);
3716
+ }
3717
+ //* If separateDialCode AND typed dial code (e.g. from paste or autofill, or from typing a dial code when countrySearch disabled), then remove the typed dial code.
3718
+ //* Only strip when a full dial code is actually present — otherwise a lone typed "+" (or partial prefix) would get erased.
3719
+ #stripTypedDialCode(inputValue) {
3720
+ if (inputValue.startsWith("+") && this.#selectedCountry && this.#getDialCode(inputValue)) {
3721
+ const cleanNumber = stripSeparateDialCode(
3722
+ inputValue,
3723
+ true,
3724
+ true,
3725
+ this.#selectedCountry
3726
+ );
3727
+ this.#setTelInputValue(cleanNumber);
3686
3728
  }
3687
- const handleInputEvent = (e) => {
3688
- if (e?.detail && e.detail["isCountryChange"]) {
3689
- return;
3690
- }
3691
- const inputValue = this.#getTelInputValue();
3692
- if (this.#isAndroid && e?.data === "+" && separateDialCode && allowDropdown && countrySearch) {
3693
- this.#removeJustTypedChar(inputValue);
3694
- this.#openDropdownWithPlus();
3695
- return;
3696
- }
3697
- if (this.#isAndroid && strictMode && (e?.data === " " || e?.data === "-" || e?.data === ".")) {
3698
- const newCaretPos = this.#removeJustTypedChar(inputValue);
3699
- this.#ui.telInputEl.setSelectionRange(newCaretPos, newCaretPos);
3700
- this.#dispatchEvent(EVENTS.STRICT_REJECT, {
3701
- source: "key",
3702
- rejectedInput: e.data,
3703
- reason: "invalid"
3704
- });
3705
- return;
3706
- }
3707
- if (this.#updateCountryFromNumber(inputValue)) {
3708
- this.#dispatchCountryChangeEvent();
3709
- }
3710
- const isFormattingChar = e?.data && REGEX.NON_PLUS_NUMERIC.test(e.data);
3711
- const isPaste = e?.inputType === INPUT_TYPES.PASTE && inputValue;
3712
- if (isFormattingChar || isPaste && !strictMode) {
3713
- userOverrideFormatting = true;
3714
- } else if (!REGEX.NON_PLUS_NUMERIC.test(inputValue)) {
3715
- userOverrideFormatting = false;
3716
- }
3717
- const isSetNumber = e?.detail && e.detail["isSetNumber"];
3718
- const isAscii = this.#numerals.isAscii();
3719
- if (formatAsYouType && !userOverrideFormatting && !isSetNumber && isAscii) {
3720
- const currentCaretPos = this.#ui.telInputEl.selectionStart || 0;
3721
- const valueBeforeCaret = inputValue.substring(0, currentCaretPos);
3722
- const relevantCharsBeforeCaret = valueBeforeCaret.replace(
3723
- REGEX.NON_PLUS_NUMERIC_GLOBAL,
3724
- ""
3725
- ).length;
3726
- const isDeleteForwards = e?.inputType === INPUT_TYPES.DELETE_FORWARD;
3727
- const fullNumber = this.#getFullNumber();
3728
- const formattedValue = formatNumberAsYouType(
3729
- fullNumber,
3730
- inputValue,
3731
- intlTelInput.utils,
3732
- this.#selectedCountry,
3733
- separateDialCode
3734
- );
3735
- const newCaretPos = computeNewCaretPosition(
3736
- relevantCharsBeforeCaret,
3737
- formattedValue,
3738
- currentCaretPos,
3739
- isDeleteForwards
3740
- );
3741
- this.#setTelInputValue(formattedValue);
3742
- this.#ui.telInputEl.setSelectionRange(newCaretPos, newCaretPos);
3743
- }
3744
- if (separateDialCode && inputValue.startsWith("+") && this.#selectedCountry?.dialCode) {
3745
- const cleanNumber = stripSeparateDialCode(
3746
- inputValue,
3747
- true,
3748
- separateDialCode,
3749
- this.#selectedCountry
3750
- );
3751
- this.#setTelInputValue(cleanNumber);
3752
- }
3753
- };
3729
+ }
3730
+ #bindInputListener() {
3731
+ this.#userOverrideFormatting = REGEX.ALPHA_UNICODE.test(
3732
+ this.#getTelInputValue()
3733
+ );
3754
3734
  this.#ui.telInputEl.addEventListener(
3755
3735
  "input",
3756
- handleInputEvent,
3736
+ this.#handleInputEvent,
3757
3737
  {
3758
3738
  signal: this.#abortController.signal
3759
3739
  }
3760
3740
  );
3761
3741
  }
3742
+ //* On input event: (1) Update selected country, (2) Format-as-you-type.
3743
+ //* Note that this fires AFTER the input is updated.
3744
+ #handleInputEvent = (e) => {
3745
+ const { strictMode, formatAsYouType, separateDialCode, allowDropdown, countrySearch } = this.#options;
3746
+ const detail = e?.detail;
3747
+ if (detail?.["isCountryChange"]) {
3748
+ return;
3749
+ }
3750
+ const inputValue = this.#getTelInputValue();
3751
+ if (this.#isAndroid && e?.data === "+" && separateDialCode && allowDropdown && countrySearch) {
3752
+ this.#handleAndroidPlusKey(inputValue);
3753
+ return;
3754
+ }
3755
+ if (this.#isAndroid && strictMode && (e?.data === " " || e?.data === "-" || e?.data === ".")) {
3756
+ this.#handleAndroidStrictReject(inputValue, e.data);
3757
+ return;
3758
+ }
3759
+ if (this.#updateCountryFromNumber(inputValue)) {
3760
+ this.#dispatchCountryChangeEvent();
3761
+ }
3762
+ const isFormattingChar = e?.data && REGEX.NON_PLUS_NUMERIC.test(e.data);
3763
+ const isPaste = e?.inputType === INPUT_TYPES.PASTE && inputValue;
3764
+ if (isFormattingChar || isPaste && !strictMode) {
3765
+ this.#userOverrideFormatting = true;
3766
+ } else if (!REGEX.NON_PLUS_NUMERIC.test(inputValue)) {
3767
+ this.#userOverrideFormatting = false;
3768
+ }
3769
+ if (formatAsYouType && !this.#userOverrideFormatting && !detail?.["isSetNumber"] && this.#numerals.isAscii()) {
3770
+ this.#formatAsYouType(
3771
+ inputValue,
3772
+ e?.inputType === INPUT_TYPES.DELETE_FORWARD
3773
+ );
3774
+ }
3775
+ if (separateDialCode) {
3776
+ this.#stripTypedDialCode(inputValue);
3777
+ }
3778
+ };
3762
3779
  #bindKeydownListener() {
3763
- const { strictMode, separateDialCode, allowDropdown, countrySearch } = this.#options;
3780
+ const { strictMode, separateDialCode } = this.#options;
3764
3781
  if (!strictMode && !separateDialCode) {
3765
3782
  return;
3766
3783
  }
3767
- const handleKeydownEvent = (e) => {
3768
- if (!e.key || e.key.length !== 1 || e.altKey || e.ctrlKey || e.metaKey) {
3769
- return;
3770
- }
3771
- if (separateDialCode && allowDropdown && countrySearch && e.key === "+") {
3772
- e.preventDefault();
3773
- this.#openDropdownWithPlus();
3774
- return;
3775
- }
3776
- if (!strictMode) {
3777
- return;
3778
- }
3779
- const inputValue = this.#getTelInputValue();
3780
- const alreadyHasPlus = inputValue.startsWith("+");
3781
- const isInitialPlus = !alreadyHasPlus && this.#ui.telInputEl.selectionStart === 0 && e.key === "+";
3782
- const normalisedKey = this.#numerals.normalise(e.key);
3783
- const isNumeric = /^[0-9]$/.test(normalisedKey);
3784
- const isAllowedChar = separateDialCode ? isNumeric : isInitialPlus || isNumeric;
3785
- const input = this.#ui.telInputEl;
3786
- const selStart = input.selectionStart;
3787
- const selEnd = input.selectionEnd;
3788
- const before = inputValue.slice(0, selStart ?? void 0);
3789
- const after = inputValue.slice(selEnd ?? void 0);
3790
- const newValue = before + normalisedKey + after;
3791
- const newFullNumber = this.#buildFullNumber(newValue);
3792
- let hasExceededMaxLength = false;
3793
- if (intlTelInput.utils && this.#maxCoreNumberLength) {
3794
- const coreNumber = intlTelInput.utils.getCoreNumber(
3795
- newFullNumber,
3796
- this.#selectedCountry?.iso2
3797
- );
3798
- hasExceededMaxLength = coreNumber.length > this.#maxCoreNumberLength;
3799
- }
3800
- const newCountry = this.#resolveCountryChangeFromNumber(newFullNumber);
3801
- const isChangingDialCode = newCountry !== null;
3802
- if (!isAllowedChar || hasExceededMaxLength && !isChangingDialCode && !isInitialPlus) {
3803
- this.#dispatchEvent(EVENTS.STRICT_REJECT, {
3804
- source: "key",
3805
- rejectedInput: e.key,
3806
- reason: !isAllowedChar ? "invalid" : "max-length"
3807
- });
3808
- e.preventDefault();
3809
- }
3810
- };
3811
- this.#ui.telInputEl.addEventListener("keydown", handleKeydownEvent, {
3784
+ this.#ui.telInputEl.addEventListener("keydown", this.#handleKeydownEvent, {
3812
3785
  signal: this.#abortController.signal
3813
3786
  });
3814
3787
  }
3788
+ //* On keydown event: (1) if strictMode then prevent invalid characters, (2) if separateDialCode then handle plus key
3789
+ //* Note that this fires BEFORE the input is updated.
3790
+ #handleKeydownEvent = (e) => {
3791
+ const { strictMode, separateDialCode, allowDropdown, countrySearch } = this.#options;
3792
+ if (!e.key || e.key.length !== 1 || e.altKey || e.ctrlKey || e.metaKey) {
3793
+ return;
3794
+ }
3795
+ if (separateDialCode && allowDropdown && countrySearch && e.key === "+") {
3796
+ e.preventDefault();
3797
+ this.#openDropdownWithPlus();
3798
+ return;
3799
+ }
3800
+ if (!strictMode) {
3801
+ return;
3802
+ }
3803
+ const inputValue = this.#getTelInputValue();
3804
+ const alreadyHasPlus = inputValue.startsWith("+");
3805
+ const isInitialPlus = !alreadyHasPlus && this.#ui.telInputEl.selectionStart === 0 && e.key === "+";
3806
+ const normalisedKey = this.#numerals.normalise(e.key);
3807
+ const isNumeric = /^[0-9]$/.test(normalisedKey);
3808
+ const isAllowedChar = separateDialCode ? isNumeric : isInitialPlus || isNumeric;
3809
+ const input = this.#ui.telInputEl;
3810
+ const selStart = input.selectionStart;
3811
+ const selEnd = input.selectionEnd;
3812
+ const before = inputValue.slice(0, selStart ?? void 0);
3813
+ const after = inputValue.slice(selEnd ?? void 0);
3814
+ const newValue = before + normalisedKey + after;
3815
+ const newFullNumber = this.#buildFullNumber(newValue);
3816
+ let hasExceededMaxLength = false;
3817
+ if (intlTelInput.utils && this.#maxCoreNumberLength) {
3818
+ const coreNumber = intlTelInput.utils.getCoreNumber(
3819
+ newFullNumber,
3820
+ this.#selectedCountry?.iso2
3821
+ );
3822
+ hasExceededMaxLength = coreNumber.length > this.#maxCoreNumberLength;
3823
+ }
3824
+ const newCountry = this.#resolveCountryChangeFromNumber(newFullNumber);
3825
+ const isChangingDialCode = newCountry !== null;
3826
+ if (!isAllowedChar || hasExceededMaxLength && !isChangingDialCode && !isInitialPlus) {
3827
+ this.#dispatchEvent(EVENTS.STRICT_REJECT, {
3828
+ source: "key",
3829
+ rejectedInput: e.key,
3830
+ reason: !isAllowedChar ? "invalid" : "max-length"
3831
+ });
3832
+ e.preventDefault();
3833
+ }
3834
+ };
3815
3835
  #bindPasteListener() {
3816
3836
  if (!this.#options.strictMode) {
3817
3837
  return;
3818
3838
  }
3819
- const handlePasteEvent = (e) => {
3820
- e.preventDefault();
3821
- const input = this.#ui.telInputEl;
3822
- const selStart = input.selectionStart;
3823
- const selEnd = input.selectionEnd;
3824
- const inputValue = this.#getTelInputValue();
3825
- const before = inputValue.slice(0, selStart ?? void 0);
3826
- const after = inputValue.slice(selEnd ?? void 0);
3827
- const iso2 = this.#selectedCountry?.iso2;
3828
- const pastedRaw = e.clipboardData.getData("text");
3829
- const pasted = this.#numerals.normalise(pastedRaw);
3830
- const initialCharSelected = selStart === 0 && selEnd > 0;
3831
- const allowLeadingPlus = !inputValue.startsWith("+") || initialCharSelected;
3832
- const allowedChars = pasted.replace(REGEX.NON_PLUS_NUMERIC_GLOBAL, "");
3833
- const hasLeadingPlus = allowedChars.startsWith("+");
3834
- const numerics = allowedChars.replace(/\+/g, "");
3835
- const sanitised = hasLeadingPlus && allowLeadingPlus ? `+${numerics}` : numerics;
3836
- let newValue = before + sanitised + after;
3837
- let rejectReason = sanitised !== pasted ? "invalid" : null;
3838
- if (newValue.length > 5 && intlTelInput.utils) {
3839
- let coreNumber = intlTelInput.utils.getCoreNumber(newValue, iso2);
3840
- while (coreNumber.length === 0 && newValue.length > 0) {
3841
- newValue = newValue.slice(0, -1);
3842
- coreNumber = intlTelInput.utils.getCoreNumber(newValue, iso2);
3843
- }
3844
- if (!coreNumber) {
3839
+ this.#ui.telInputEl.addEventListener("paste", this.#handlePasteEvent, {
3840
+ signal: this.#abortController.signal
3841
+ });
3842
+ }
3843
+ #handlePasteEvent = (e) => {
3844
+ e.preventDefault();
3845
+ const input = this.#ui.telInputEl;
3846
+ const selStart = input.selectionStart;
3847
+ const selEnd = input.selectionEnd;
3848
+ const inputValue = this.#getTelInputValue();
3849
+ const before = inputValue.slice(0, selStart ?? void 0);
3850
+ const after = inputValue.slice(selEnd ?? void 0);
3851
+ const iso2 = this.#selectedCountry?.iso2;
3852
+ const pastedRaw = e.clipboardData.getData("text");
3853
+ const pasted = this.#numerals.normalise(pastedRaw);
3854
+ const initialCharSelected = selStart === 0 && selEnd > 0;
3855
+ const allowLeadingPlus = !inputValue.startsWith("+") || initialCharSelected;
3856
+ const allowedChars = pasted.replace(REGEX.NON_PLUS_NUMERIC_GLOBAL, "");
3857
+ const hasLeadingPlus = allowedChars.startsWith("+");
3858
+ const numerics = allowedChars.replace(/\+/g, "");
3859
+ const sanitised = hasLeadingPlus && allowLeadingPlus ? `+${numerics}` : numerics;
3860
+ let newValue = before + sanitised + after;
3861
+ let rejectReason = sanitised !== pasted ? "invalid" : null;
3862
+ if (newValue.length > 5 && intlTelInput.utils) {
3863
+ let coreNumber = intlTelInput.utils.getCoreNumber(newValue, iso2);
3864
+ while (coreNumber.length === 0 && newValue.length > 0) {
3865
+ newValue = newValue.slice(0, -1);
3866
+ coreNumber = intlTelInput.utils.getCoreNumber(newValue, iso2);
3867
+ }
3868
+ if (!coreNumber) {
3869
+ this.#dispatchEvent(EVENTS.STRICT_REJECT, {
3870
+ source: "paste",
3871
+ rejectedInput: pastedRaw,
3872
+ reason: "max-length"
3873
+ });
3874
+ return;
3875
+ }
3876
+ if (this.#maxCoreNumberLength && coreNumber.length > this.#maxCoreNumberLength) {
3877
+ if (input.selectionEnd === inputValue.length) {
3878
+ const trimLength = coreNumber.length - this.#maxCoreNumberLength;
3879
+ newValue = newValue.slice(0, newValue.length - trimLength);
3880
+ rejectReason = "max-length";
3881
+ } else {
3845
3882
  this.#dispatchEvent(EVENTS.STRICT_REJECT, {
3846
3883
  source: "paste",
3847
3884
  rejectedInput: pastedRaw,
@@ -3849,37 +3886,20 @@ var Iti = class _Iti {
3849
3886
  });
3850
3887
  return;
3851
3888
  }
3852
- if (this.#maxCoreNumberLength && coreNumber.length > this.#maxCoreNumberLength) {
3853
- if (input.selectionEnd === inputValue.length) {
3854
- const trimLength = coreNumber.length - this.#maxCoreNumberLength;
3855
- newValue = newValue.slice(0, newValue.length - trimLength);
3856
- rejectReason = "max-length";
3857
- } else {
3858
- this.#dispatchEvent(EVENTS.STRICT_REJECT, {
3859
- source: "paste",
3860
- rejectedInput: pastedRaw,
3861
- reason: "max-length"
3862
- });
3863
- return;
3864
- }
3865
- }
3866
3889
  }
3867
- this.#setTelInputValue(newValue);
3868
- const caretPos = selStart + sanitised.length;
3869
- input.setSelectionRange(caretPos, caretPos);
3870
- input.dispatchEvent(new InputEvent("input", { bubbles: true }));
3871
- if (rejectReason) {
3872
- this.#dispatchEvent(EVENTS.STRICT_REJECT, {
3873
- source: "paste",
3874
- rejectedInput: pastedRaw,
3875
- reason: rejectReason
3876
- });
3877
- }
3878
- };
3879
- this.#ui.telInputEl.addEventListener("paste", handlePasteEvent, {
3880
- signal: this.#abortController.signal
3881
- });
3882
- }
3890
+ }
3891
+ this.#setTelInputValue(newValue);
3892
+ const caretPos = selStart + sanitised.length;
3893
+ input.setSelectionRange(caretPos, caretPos);
3894
+ input.dispatchEvent(new InputEvent("input", { bubbles: true }));
3895
+ if (rejectReason) {
3896
+ this.#dispatchEvent(EVENTS.STRICT_REJECT, {
3897
+ source: "paste",
3898
+ rejectedInput: pastedRaw,
3899
+ reason: rejectReason
3900
+ });
3901
+ }
3902
+ };
3883
3903
  //* Adhere to the input's maxlength attr.
3884
3904
  #truncateToMaxLength(number) {
3885
3905
  const max = Number(this.#ui.telInputEl.getAttribute("maxlength"));
@@ -4521,7 +4541,7 @@ var intlTelInput = Object.assign(
4521
4541
  attachUtils,
4522
4542
  startedLoadingUtils: false,
4523
4543
  startedLoadingAutoCountry: false,
4524
- version: "27.1.2"
4544
+ version: "27.2.0"
4525
4545
  }
4526
4546
  );
4527
4547
  var intlTelInput_default = intlTelInput;
@@ -10857,6 +10877,9 @@ var IntlTelInput = forwardRef(function IntlTelInput2({
10857
10877
  onChangeCountry = noop,
10858
10878
  onChangeValidity = noop,
10859
10879
  onChangeErrorCode = noop,
10880
+ onOpenCountryDropdown,
10881
+ onCloseCountryDropdown,
10882
+ onStrictReject,
10860
10883
  usePreciseValidation = false,
10861
10884
  inputProps = {},
10862
10885
  disabled = void 0,
@@ -10870,6 +10893,13 @@ var IntlTelInput = forwardRef(function IntlTelInput2({
10870
10893
  const lastEmittedCountryRef = useRef(void 0);
10871
10894
  const lastEmittedValidityRef = useRef(void 0);
10872
10895
  const lastEmittedErrorCodeRef = useRef(void 0);
10896
+ const pendingUpdateRef = useRef(false);
10897
+ const onOpenCountryDropdownRef = useRef(onOpenCountryDropdown);
10898
+ const onCloseCountryDropdownRef = useRef(onCloseCountryDropdown);
10899
+ const onStrictRejectRef = useRef(onStrictReject);
10900
+ onOpenCountryDropdownRef.current = onOpenCountryDropdown;
10901
+ onCloseCountryDropdownRef.current = onCloseCountryDropdown;
10902
+ onStrictRejectRef.current = onStrictReject;
10873
10903
  useImperativeHandle(ref, () => ({
10874
10904
  getInstance: () => itiRef.current,
10875
10905
  getInput: () => inputRef.current
@@ -10888,6 +10918,10 @@ var IntlTelInput = forwardRef(function IntlTelInput2({
10888
10918
  if (!itiRef.current?.isActive()) {
10889
10919
  return;
10890
10920
  }
10921
+ if (!intlTelInput_default.utils) {
10922
+ pendingUpdateRef.current = true;
10923
+ return;
10924
+ }
10891
10925
  const num = itiRef.current.getNumber() ?? "";
10892
10926
  const countryIso = itiRef.current.getSelectedCountryData()?.iso2 ?? "";
10893
10927
  if (num !== lastEmittedNumberRef.current) {
@@ -10916,16 +10950,40 @@ var IntlTelInput = forwardRef(function IntlTelInput2({
10916
10950
  usePreciseValidation
10917
10951
  ]);
10918
10952
  useEffect(() => {
10919
- if (inputRef.current) {
10920
- itiRef.current = intlTelInput_default(inputRef.current, initOptions);
10921
- }
10953
+ const inputEl = inputRef.current;
10954
+ if (!inputEl) {
10955
+ return void 0;
10956
+ }
10957
+ itiRef.current = intlTelInput_default(inputEl, initOptions);
10958
+ const handleOpen = () => onOpenCountryDropdownRef.current?.();
10959
+ const handleClose = () => onCloseCountryDropdownRef.current?.();
10960
+ const handleStrictReject = (e) => {
10961
+ const { source, rejectedInput, reason } = e.detail;
10962
+ onStrictRejectRef.current?.(source, rejectedInput, reason);
10963
+ };
10964
+ inputEl.addEventListener("open:countrydropdown", handleOpen);
10965
+ inputEl.addEventListener("close:countrydropdown", handleClose);
10966
+ inputEl.addEventListener("strict:reject", handleStrictReject);
10922
10967
  return () => {
10968
+ inputEl.removeEventListener("open:countrydropdown", handleOpen);
10969
+ inputEl.removeEventListener("close:countrydropdown", handleClose);
10970
+ inputEl.removeEventListener("strict:reject", handleStrictReject);
10923
10971
  itiRef.current?.destroy();
10924
10972
  };
10925
10973
  }, []);
10926
10974
  useEffect(() => {
10927
- itiRef.current?.promise.then(seedInitialState);
10928
- }, [seedInitialState]);
10975
+ itiRef.current?.promise.then(() => {
10976
+ if (!itiRef.current?.isActive()) {
10977
+ return;
10978
+ }
10979
+ if (pendingUpdateRef.current) {
10980
+ pendingUpdateRef.current = false;
10981
+ update();
10982
+ } else {
10983
+ seedInitialState();
10984
+ }
10985
+ });
10986
+ }, [seedInitialState, update]);
10929
10987
  useEffect(() => {
10930
10988
  if (itiRef.current && disabled !== void 0) {
10931
10989
  itiRef.current.setDisabled(disabled);
package/svelte/README.md CHANGED
@@ -3,3 +3,12 @@
3
3
  A Svelte 5 component for the [intl-tel-input](https://github.com/jackocnr/intl-tel-input) JavaScript plugin. View the [source code](https://github.com/jackocnr/intl-tel-input/blob/master/svelte/src/IntlTelInput.svelte).
4
4
 
5
5
  [Explore docs »](https://intl-tel-input.com/docs/svelte-component)
6
+
7
+ ## Running the demos locally
8
+
9
+ 1. Initialise the submodules: `git submodule update --init --recursive`
10
+ 2. Install dependencies: `npm install`
11
+ 3. Build: `npm run build`
12
+ 4. Run a demo: `npm run svelte:demo` and copy the given URL into your browser.
13
+
14
+ This defaults to the validation demo — to run a different one, set the `DEMO` env var, e.g. `DEMO=simple npm run svelte:demo`. View the full list of [available demos](https://github.com/jackocnr/intl-tel-input/tree/master/svelte/demo).
@@ -20,9 +20,18 @@
20
20
  onChangeCountry,
21
21
  onChangeValidity,
22
22
  onChangeErrorCode,
23
+ onOpenCountryDropdown,
24
+ onCloseCountryDropdown,
25
+ onStrictReject,
23
26
  ...initOptions
24
27
  } = $props() as Props;
25
28
 
29
+ type StrictRejectDetail = {
30
+ source: "key" | "paste";
31
+ rejectedInput: string;
32
+ reason: "invalid" | "max-length";
33
+ };
34
+
26
35
  // State
27
36
  let inputElement: HTMLInputElement | undefined = $state();
28
37
  let instance: Iti | undefined = $state();
@@ -31,6 +40,8 @@
31
40
  let lastEmittedValidity: boolean | undefined = $state();
32
41
  let lastEmittedErrorCode: number | null | undefined = $state();
33
42
  let hasInitialized = $state(false);
43
+ // if an input event fires before utils has loaded, we defer the update until the promise resolves
44
+ let pendingUpdate = false;
34
45
 
35
46
  // Validation helper
36
47
  const isValid = (): boolean | null => {
@@ -43,6 +54,11 @@
43
54
  // Update handlers
44
55
  const updateValidity = () => {
45
56
  if (!instance) return;
57
+ // if utils has not loaded yet, isValidNumber/getValidationError will throw. defer until the promise resolves.
58
+ if (!intlTelInput.utils) {
59
+ pendingUpdate = true;
60
+ return;
61
+ }
46
62
  const isCurrentlyValid = isValid();
47
63
  if (isCurrentlyValid === null) return;
48
64
 
@@ -64,6 +80,11 @@
64
80
  if (!instance?.isActive()) {
65
81
  return;
66
82
  }
83
+ // if utils has not loaded yet, getNumber will throw. defer until the promise resolves.
84
+ if (!intlTelInput.utils) {
85
+ pendingUpdate = true;
86
+ return;
87
+ }
67
88
  const number = instance.getNumber() ?? "";
68
89
  if (number !== lastEmittedNumber) {
69
90
  lastEmittedNumber = number;
@@ -84,6 +105,13 @@
84
105
  updateValue();
85
106
  };
86
107
 
108
+ const handleOpenDropdown = (): void => onOpenCountryDropdown?.();
109
+ const handleCloseDropdown = (): void => onCloseCountryDropdown?.();
110
+ const handleStrictReject = (e: Event): void => {
111
+ const { source, rejectedInput, reason } = (e as CustomEvent<StrictRejectDetail>).detail;
112
+ onStrictReject?.(source, rejectedInput, reason);
113
+ };
114
+
87
115
  // Lifecycle
88
116
  onMount(() => {
89
117
  if (inputElement) {
@@ -91,6 +119,10 @@
91
119
  if (disabled) instance.setDisabled(disabled);
92
120
  if (readonly) instance.setReadonly(readonly);
93
121
 
122
+ inputElement.addEventListener("open:countrydropdown", handleOpenDropdown);
123
+ inputElement.addEventListener("close:countrydropdown", handleCloseDropdown);
124
+ inputElement.addEventListener("strict:reject", handleStrictReject);
125
+
94
126
  lastEmittedCountry = instance.getSelectedCountryData()?.iso2 ?? "";
95
127
  hasInitialized = true;
96
128
 
@@ -98,19 +130,29 @@
98
130
  instance.promise.then(() => {
99
131
  if (!instance?.isActive()) return;
100
132
  if (initialValue) instance.setNumber(initialValue);
101
- lastEmittedNumber = instance.getNumber() ?? "";
102
- const initialValid = isValid();
103
- if (initialValid !== null) {
104
- lastEmittedValidity = !!initialValid;
105
- lastEmittedErrorCode = initialValid ? null : instance.getValidationError();
133
+ // if an input event fired during the utils-loading gap, replay it now so the skipped emissions fire.
134
+ // otherwise seed silently so we don't fire change callbacks on initial mount.
135
+ if (pendingUpdate) {
136
+ pendingUpdate = false;
137
+ updateCountry();
138
+ } else {
139
+ lastEmittedNumber = instance.getNumber() ?? "";
140
+ const initialValid = isValid();
141
+ if (initialValid !== null) {
142
+ lastEmittedValidity = !!initialValid;
143
+ lastEmittedErrorCode = initialValid ? null : instance.getValidationError();
144
+ }
106
145
  }
107
- // update all state values now that initialisation has finished (updateCountry calls updateValue which calls updateValidity)
108
- updateCountry();
109
146
  });
110
147
  }
111
148
  });
112
149
 
113
150
  onDestroy(() => {
151
+ if (inputElement) {
152
+ inputElement.removeEventListener("open:countrydropdown", handleOpenDropdown);
153
+ inputElement.removeEventListener("close:countrydropdown", handleCloseDropdown);
154
+ inputElement.removeEventListener("strict:reject", handleStrictReject);
155
+ }
114
156
  instance?.destroy();
115
157
  });
116
158
 
@@ -4,6 +4,9 @@ import type { Component } from "svelte";
4
4
  import type { Iti, SomeOptions } from "intl-tel-input";
5
5
  import intlTelInput from "intl-tel-input";
6
6
 
7
+ export type StrictRejectSource = "key" | "paste";
8
+ export type StrictRejectReason = "invalid" | "max-length";
9
+
7
10
  export type Props = SomeOptions & {
8
11
  disabled?: boolean;
9
12
  readonly?: boolean;
@@ -15,6 +18,9 @@ export type Props = SomeOptions & {
15
18
  onChangeCountry?: (iso2: string) => void;
16
19
  onChangeValidity?: (isValid: boolean) => void;
17
20
  onChangeErrorCode?: (errorCode: number | null) => void;
21
+ onOpenCountryDropdown?: () => void;
22
+ onCloseCountryDropdown?: () => void;
23
+ onStrictReject?: (source: StrictRejectSource, rejectedInput: string, reason: StrictRejectReason) => void;
18
24
  };
19
25
 
20
26
  export { intlTelInput };