intl-tel-input 18.3.5 → 18.5.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.
package/demo.html CHANGED
@@ -21,8 +21,14 @@
21
21
  // allowDropdown: false,
22
22
  // autoInsertDialCode: true,
23
23
  // autoPlaceholder: "off",
24
- // dropdownContainer: document.body,
24
+ // countrySearch: true,
25
+ // customContainer: "test",
26
+ // customPlaceholder: function(selectedCountryPlaceholder, selectedCountryData) {
27
+ // return "e.g. " + selectedCountryPlaceholder;
28
+ // },
29
+ // dropdownContainer: document.querySelector('#custom-container'),
25
30
  // excludeCountries: ["us"],
31
+ // fixDropdownWidth: true,
26
32
  // formatOnDisplay: false,
27
33
  // geoIpLookup: function(callback) {
28
34
  // fetch("https://ipapi.co/json")
@@ -19,7 +19,7 @@
19
19
  }
20
20
  }
21
21
 
22
- @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
22
+ @media (min-resolution: 2x) {
23
23
  background-size: {{spritesheet.px.width}} {{spritesheet.px.height}};
24
24
  }
25
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intl-tel-input",
3
- "version": "18.3.5",
3
+ "version": "18.5.0",
4
4
  "description": "A JavaScript plugin for entering and validating international telephone numbers",
5
5
  "keywords": [
6
6
  "international",
@@ -60,9 +60,9 @@ $mobilePopupMargin: 30px !default;
60
60
  }
61
61
 
62
62
  // specify types to increase specificity e.g. to override bootstrap v2.3
63
- input,
64
- input[type="text"],
65
- input[type="tel"] {
63
+ input.iti__tel-input,
64
+ input.iti__tel-input[type="text"],
65
+ input.iti__tel-input[type="tel"] {
66
66
  position: relative;
67
67
  // input is bottom level, below selected flag and dropdown
68
68
  z-index: 0;
@@ -129,35 +129,24 @@ $mobilePopupMargin: 30px !default;
129
129
  }
130
130
 
131
131
  // the dropdown
132
- &__country-list {
132
+ &__dropdown-content {
133
133
  position: absolute;
134
134
  // popup so render above everything else
135
135
  z-index: 2;
136
136
 
137
- // override default list styles
138
- list-style: none;
139
-
140
137
  // place menu above the input element
141
138
  &--dropup {
142
139
  bottom: 100%;
143
140
  margin-bottom: (-$borderWidth);
144
141
  }
145
142
 
146
- padding: 0;
147
143
  // margin-left to compensate for the padding on the parent
148
- margin: 0 0 0 (-$borderWidth);
144
+ margin-left: (-$borderWidth);
149
145
 
150
146
  box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.2);
151
147
  background-color: white;
152
148
  border: $borderWidth solid $greyBorder;
153
149
 
154
- // don't let the contents wrap AKA the container will be as wide as the contents
155
- white-space: nowrap;
156
- // except on small screens, where we force the dropdown width to match the input
157
- @media (max-width: 500px) {
158
- white-space: normal;
159
- }
160
-
161
150
  max-height: 200px;
162
151
  overflow-y: scroll;
163
152
 
@@ -167,6 +156,25 @@ $mobilePopupMargin: 30px !default;
167
156
  // Stackoverflow question about it: https://stackoverflow.com/questions/33601165/scrolling-slow-on-mobile-ios-when-using-overflowscroll
168
157
  -webkit-overflow-scrolling: touch;
169
158
  }
159
+ &__search-input {
160
+ width: 100%;
161
+ border-width: 0;
162
+ }
163
+ &__country-list {
164
+ // override default list styles
165
+ list-style: none;
166
+ padding: 0;
167
+ margin: 0;
168
+ }
169
+ &--flexible-dropdown-width &__country-list {
170
+ // don't let the contents wrap AKA the container will be as wide as the contents
171
+ white-space: nowrap;
172
+
173
+ // except on small screens, where we force the dropdown width to match the input
174
+ @media (max-width: 500px) {
175
+ white-space: normal;
176
+ }
177
+ }
170
178
 
171
179
  // dropdown flags need consistent width, so wrap in a container
172
180
  &__flag-box {
@@ -212,9 +220,9 @@ $mobilePopupMargin: 30px !default;
212
220
  // these settings are independent of each other, but both move selected flag to left of input
213
221
  &--allow-dropdown,
214
222
  &--separate-dial-code {
215
- input,
216
- input[type="text"],
217
- input[type="tel"] {
223
+ input.iti__tel-input,
224
+ input.iti__tel-input[type="text"],
225
+ input.iti__tel-input[type="tel"] {
218
226
  padding-right: $inputPadding;
219
227
  padding-left: $selectedFlagArrowWidth + $inputPadding;
220
228
  margin-left: 0;
@@ -245,8 +253,8 @@ $mobilePopupMargin: 30px !default;
245
253
  }
246
254
  }
247
255
  // disable hover state when input is disabled
248
- input[disabled] + .iti__flag-container:hover,
249
- input[readonly] + .iti__flag-container:hover {
256
+ .iti__flag-container:has(+ input[disabled]):hover,
257
+ .iti__flag-container:has(+ input[readonly]):hover {
250
258
  cursor: default;
251
259
  .iti__selected-flag {
252
260
  background-color: transparent;
@@ -284,9 +292,9 @@ $mobilePopupMargin: 30px !default;
284
292
  }
285
293
  }
286
294
 
287
- // overrides for mobile popup (note: .iti-fullscreen-popup class is applied on body)
288
- .iti-fullscreen-popup .iti {
289
- &--container {
295
+ // overrides for mobile popup
296
+ .iti--fullscreen-popup {
297
+ &.iti--container {
290
298
  background-color: rgba(0, 0, 0, 0.5);
291
299
  top: 0;
292
300
  bottom: 0;
@@ -299,11 +307,11 @@ $mobilePopupMargin: 30px !default;
299
307
  flex-direction: column;
300
308
  justify-content: center;
301
309
  }
302
- &__country-list {
310
+ .iti__dropdown-content {
303
311
  max-height: 100%;
304
312
  position: relative; // override needed in order to get full-width working properly
305
313
  }
306
- &__country {
314
+ .iti__country {
307
315
  padding: 10px 10px;
308
316
  // increase line height because dropdown copy is v likely to overflow on mobile and when it does it needs to be well spaced
309
317
  line-height: 1.5em;
@@ -321,7 +329,7 @@ $mobilePopupMargin: 30px !default;
321
329
  background-color: #dbdbdb;
322
330
  background-position: $flagWidth 0;
323
331
 
324
- @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
332
+ @media (min-resolution: 2x) {
325
333
  background-image: url("#{$flagsImagePath}#{$flagsImageName}@2x.#{$flagsImageExtension}#{$flagsImageQuery}");
326
334
  }
327
335
  }
@@ -19,7 +19,7 @@
19
19
  }
20
20
  }
21
21
 
22
- @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
22
+ @media (min-resolution: 2x) {
23
23
  background-size: 5762px 15px;
24
24
  }
25
25
 
@@ -22,6 +22,8 @@ const defaults = {
22
22
  autoInsertDialCode: false,
23
23
  // add a placeholder in the input with an example number for the selected country
24
24
  autoPlaceholder: "polite",
25
+ // add a country search input at the top of the dropdown
26
+ countrySearch: false,
25
27
  // modify the parentClass
26
28
  customContainer: "",
27
29
  // modify the auto placeholder
@@ -30,6 +32,8 @@ const defaults = {
30
32
  dropdownContainer: null,
31
33
  // don't display these countries
32
34
  excludeCountries: [],
35
+ // fix the dropdown width to the input width (rather than being as wide as the longest country name)
36
+ fixDropdownWidth: false,
33
37
  // format the input value during initialisation and on setNumber
34
38
  formatOnDisplay: true,
35
39
  // geoIp lookup function
@@ -126,6 +130,17 @@ class Iti {
126
130
  }
127
131
 
128
132
  _init() {
133
+ // if showing fullscreen popup, do not fix the width
134
+ if (this.options.useFullscreenPopup) {
135
+ this.options.fixDropdownWidth = false;
136
+ this.options.countrySearch = false;
137
+ }
138
+
139
+ // when search enabled, we must fix the width else it would change with different results
140
+ if (this.options.countrySearch) {
141
+ this.options.fixDropdownWidth = true;
142
+ }
143
+
129
144
  // if in nationalMode, do not insert dial codes
130
145
  if (this.options.nationalMode) {
131
146
  this.options.autoInsertDialCode = false;
@@ -144,14 +159,9 @@ class Iti {
144
159
  this.options.showFlags = true;
145
160
  }
146
161
 
147
- if (this.options.useFullscreenPopup) {
148
- // trigger the mobile dropdown css
149
- document.body.classList.add("iti-fullscreen-popup");
150
-
151
- // on mobile, we want a full screen dropdown, so we must append it to the body
152
- if (!this.options.dropdownContainer) {
153
- this.options.dropdownContainer = document.body;
154
- }
162
+ // on mobile, we want a full screen dropdown, so we must append it to the body
163
+ if (this.options.useFullscreenPopup && !this.options.dropdownContainer) {
164
+ this.options.dropdownContainer = document.body;
155
165
  }
156
166
 
157
167
  // check if input has one parent with RTL
@@ -355,6 +365,8 @@ class Iti {
355
365
 
356
366
  // generate all of the markup for the plugin: the selected flag overlay, and the dropdown
357
367
  _generateMarkup() {
368
+ this.telInput.classList.add("iti__tel-input");
369
+
358
370
  // if autocomplete does not exist on the element and its form, then
359
371
  // prevent autocomplete as there's no safe, cross-browser event we can react to, so it can
360
372
  // easily put the plugin in an inconsistent state e.g. the wrong flag selected for the
@@ -372,7 +384,10 @@ class Iti {
372
384
  showFlags,
373
385
  customContainer,
374
386
  hiddenInput,
375
- dropdownContainer
387
+ dropdownContainer,
388
+ fixDropdownWidth,
389
+ useFullscreenPopup,
390
+ countrySearch
376
391
  } = this.options;
377
392
 
378
393
  // containers (mostly for positioning)
@@ -386,6 +401,9 @@ class Iti {
386
401
  if (showFlags) {
387
402
  parentClass += " iti--show-flags";
388
403
  }
404
+ if (!fixDropdownWidth) {
405
+ parentClass += " iti--flexible-dropdown-width";
406
+ }
389
407
  if (customContainer) {
390
408
  parentClass += ` ${customContainer}`;
391
409
  }
@@ -454,14 +472,34 @@ class Iti {
454
472
  this.selectedFlag
455
473
  );
456
474
 
457
- // country dropdown: preferred countries, then divider, then all countries
458
- this.countryList = this._createEl("ul", {
459
- class: "iti__country-list iti__hide",
460
- id: `iti-${this.id}__country-listbox`,
461
- role: "listbox",
462
- "aria-label": "List of countries"
475
+ this.dropdownContent = this._createEl("div", {
476
+ class: "iti__dropdown-content iti__hide"
463
477
  });
464
- if (this.preferredCountries.length) {
478
+
479
+ if (countrySearch) {
480
+ this.searchInput = this._createEl(
481
+ "input",
482
+ {
483
+ type: "text",
484
+ class: "iti__search-input",
485
+ placeholder: "Search"
486
+ },
487
+ this.dropdownContent
488
+ );
489
+ }
490
+
491
+ // country list: preferred countries, then divider, then all countries
492
+ this.countryList = this._createEl(
493
+ "ul",
494
+ {
495
+ class: "iti__country-list",
496
+ id: `iti-${this.id}__country-listbox`,
497
+ role: "listbox",
498
+ "aria-label": "List of countries"
499
+ },
500
+ this.dropdownContent
501
+ );
502
+ if (this.preferredCountries.length && !countrySearch) {
465
503
  this._appendListItems(this.preferredCountries, "iti__preferred", true);
466
504
  this._createEl(
467
505
  "li",
@@ -476,10 +514,15 @@ class Iti {
476
514
 
477
515
  // create dropdownContainer markup
478
516
  if (dropdownContainer) {
479
- this.dropdown = this._createEl("div", { class: "iti iti--container" });
480
- this.dropdown.appendChild(this.countryList);
517
+ const fullscreenClass = useFullscreenPopup
518
+ ? "iti--fullscreen-popup"
519
+ : "";
520
+ this.dropdown = this._createEl("div", {
521
+ class: `iti iti--container ${fullscreenClass}`
522
+ });
523
+ this.dropdown.appendChild(this.dropdownContent);
481
524
  } else {
482
- this.flagsContainer.appendChild(this.countryList);
525
+ this.flagsContainer.appendChild(this.dropdownContent);
483
526
  }
484
527
  }
485
528
 
@@ -502,28 +545,39 @@ class Iti {
502
545
  }
503
546
  }
504
547
 
505
- // add a country <li> to the countryList <ul> container
548
+ // for each of the passed countries: add a country <li> to the countryList <ul> container
506
549
  _appendListItems(countries, className, preferred) {
507
- // we create so many DOM elements, it is faster to build a temp string
508
- // and then add everything to the DOM in one go at the end
509
- let tmp = "";
510
- // for each country
511
550
  for (let i = 0; i < countries.length; i++) {
512
551
  const c = countries[i];
513
552
  const idSuffix = preferred ? "-preferred" : "";
514
- // open the list item
515
- tmp += `<li class='iti__country ${className}' tabIndex='-1' id='iti-${this.id}__item-${c.iso2}${idSuffix}' role='option' data-dial-code='${c.dialCode}' data-country-code='${c.iso2}' aria-selected='false'>`;
553
+
554
+ const listItem = this._createEl(
555
+ "li",
556
+ {
557
+ id: `iti-${this.id}__item-${c.iso2}${idSuffix}`,
558
+ class: `iti__country ${className}`,
559
+ tabindex: "-1",
560
+ role: "option",
561
+ "data-dial-code": c.dialCode,
562
+ "data-country-code": c.iso2,
563
+ "aria-selected": "false"
564
+ },
565
+ this.countryList
566
+ );
567
+ // store this for later use e.g. country search filtering
568
+ c.node = listItem;
569
+
570
+ let content = "";
516
571
  // add the flag
517
572
  if (this.options.showFlags) {
518
- tmp += `<div class='iti__flag-box'><div class='iti__flag iti__${c.iso2}'></div></div>`;
573
+ content += `<div class='iti__flag-box'><div class='iti__flag iti__${c.iso2}'></div></div>`;
519
574
  }
520
575
  // and the country name and dial code
521
- tmp += `<span class='iti__country-name'>${c.name}</span>`;
522
- tmp += `<span class='iti__dial-code'>+${c.dialCode}</span>`;
523
- // close the list item
524
- tmp += "</li>";
576
+ content += `<span class='iti__country-name'>${c.name}</span>`;
577
+ content += `<span class='iti__dial-code'>+${c.dialCode}</span>`;
578
+
579
+ listItem.insertAdjacentHTML("beforeend", content);
525
580
  }
526
- this.countryList.insertAdjacentHTML("beforeend", tmp);
527
581
  }
528
582
 
529
583
  // set the initial state of the input value and the selected flag by:
@@ -625,7 +679,7 @@ class Iti {
625
679
  // close it again
626
680
  this._handleLabelClick = (e) => {
627
681
  // if the dropdown is closed, then focus the input, else ignore the click
628
- if (this.countryList.classList.contains("iti__hide")) {
682
+ if (this.dropdownContent.classList.contains("iti__hide")) {
629
683
  this.telInput.focus();
630
684
  } else {
631
685
  e.preventDefault();
@@ -642,7 +696,7 @@ class Iti {
642
696
  // else let it bubble up to the top ("click-off-to-close" listener)
643
697
  // we cannot just stopPropagation as it may be needed to close another instance
644
698
  if (
645
- this.countryList.classList.contains("iti__hide") &&
699
+ this.dropdownContent.classList.contains("iti__hide") &&
646
700
  !this.telInput.disabled &&
647
701
  !this.telInput.readOnly
648
702
  ) {
@@ -651,14 +705,14 @@ class Iti {
651
705
  };
652
706
  this.selectedFlag.addEventListener("click", this._handleClickSelectedFlag);
653
707
 
654
- // open dropdown list if currently focused
708
+ // open dropdown if selected flag is focused and they press up/down/space/enter
655
709
  this._handleFlagsContainerKeydown = (e) => {
656
- const isDropdownHidden = this.countryList.classList.contains("iti__hide");
710
+ const isDropdownHidden =
711
+ this.dropdownContent.classList.contains("iti__hide");
657
712
 
658
713
  if (
659
714
  isDropdownHidden &&
660
- ["ArrowUp", "Up", "ArrowDown", "Down", " ", "Enter"].indexOf(e.key) !==
661
- -1
715
+ ["ArrowUp", "ArrowDown", " ", "Enter"].includes(e.key)
662
716
  ) {
663
717
  // prevent form from being submitted if "ENTER" was pressed
664
718
  e.preventDefault();
@@ -802,13 +856,20 @@ class Iti {
802
856
 
803
857
  // show the dropdown
804
858
  _showDropdown() {
805
- this.countryList.classList.remove("iti__hide");
859
+ if (this.options.fixDropdownWidth) {
860
+ this.dropdownContent.style.width = `${this.telInput.offsetWidth}px`;
861
+ }
862
+ this.dropdownContent.classList.remove("iti__hide");
806
863
  this.selectedFlag.setAttribute("aria-expanded", "true");
807
864
 
808
865
  this._setDropdownPosition();
809
866
 
810
- // update highlighting and scroll to active list item
811
- if (this.activeItem) {
867
+ if (this.options.countrySearch) {
868
+ // start by highlighting the first item in the list
869
+ this._highlightListItem(this.countryList.firstElementChild, false);
870
+ this.searchInput.focus();
871
+ } else if (this.activeItem) {
872
+ // update highlighting and scroll to active list item
812
873
  this._highlightListItem(this.activeItem, false);
813
874
  this._scrollTo(this.activeItem, true);
814
875
  }
@@ -843,7 +904,7 @@ class Iti {
843
904
  const windowTop =
844
905
  window.pageYOffset || document.documentElement.scrollTop;
845
906
  const inputTop = pos.top + windowTop;
846
- const dropdownHeight = this.countryList.offsetHeight;
907
+ const dropdownHeight = this.dropdownContent.offsetHeight;
847
908
  // dropdownFitsBelow = (dropdownBottom < windowBottom)
848
909
  const dropdownFitsBelow =
849
910
  inputTop + this.telInput.offsetHeight + dropdownHeight <
@@ -853,7 +914,7 @@ class Iti {
853
914
  // by default, the dropdown will be below the input. If we want to position it above the
854
915
  // input, we add the dropup class.
855
916
  this._toggleClass(
856
- this.countryList,
917
+ this.dropdownContent,
857
918
  "iti__country-list--dropup",
858
919
  !dropdownFitsBelow && dropdownFitsAbove
859
920
  );
@@ -932,7 +993,7 @@ class Iti {
932
993
  this._handleClickOffToClose
933
994
  );
934
995
 
935
- // listen for up/down scrolling, enter to select, or letters to jump to country name.
996
+ // listen for up/down scrolling, enter to select, or escape to close
936
997
  // use keydown as keypress doesn't fire for non-char keys and we want to catch if they
937
998
  // just hit down and hold it to scroll down (no keyup event).
938
999
  // listen on the document because that's where key events are triggered if no input has focus
@@ -941,28 +1002,28 @@ class Iti {
941
1002
  this._handleKeydownOnDropdown = (e) => {
942
1003
  // prevent down key from scrolling the whole page,
943
1004
  // and enter key from submitting a form etc
944
- e.preventDefault();
1005
+ if (["ArrowUp", "ArrowDown", "Enter", "Escape"].includes(e.key)) {
1006
+ e.preventDefault();
1007
+ e.stopPropagation();
945
1008
 
946
- // up and down to navigate
947
- if (
948
- e.key === "ArrowUp" ||
949
- e.key === "Up" ||
950
- e.key === "ArrowDown" ||
951
- e.key === "Down"
952
- ) {
953
- this._handleUpDownKey(e.key);
954
- }
955
- // enter to select
956
- else if (e.key === "Enter") {
957
- this._handleEnterKey();
958
- }
959
- // esc to close
960
- else if (e.key === "Escape") {
961
- this._closeDropdown();
1009
+ // up and down to navigate
1010
+ if (e.key === "ArrowUp" || e.key === "ArrowDown") {
1011
+ this._handleUpDownKey(e.key);
1012
+ }
1013
+ // enter to select
1014
+ else if (e.key === "Enter") {
1015
+ this._handleEnterKey();
1016
+ }
1017
+ // esc to close
1018
+ else if (e.key === "Escape") {
1019
+ this._closeDropdown();
1020
+ }
962
1021
  }
1022
+
963
1023
  // alpha chars to perform search
964
1024
  // regex allows one latin alpha char or space, based on https://stackoverflow.com/a/26900132/217866)
965
- else if (/^[a-zA-ZÀ-ÿа-яА-Я ]$/.test(e.key)) {
1025
+ if (!this.options.countrySearch && /^[a-zA-ZÀ-ÿа-яА-Я ]$/.test(e.key)) {
1026
+ e.stopPropagation();
966
1027
  // jump to countries that start with the query string
967
1028
  if (queryTimer) {
968
1029
  clearTimeout(queryTimer);
@@ -976,23 +1037,85 @@ class Iti {
976
1037
  }
977
1038
  };
978
1039
  document.addEventListener("keydown", this._handleKeydownOnDropdown);
1040
+
1041
+ if (this.options.countrySearch) {
1042
+ const doFilter = () => {
1043
+ const inputQuery = this.searchInput.value.trim();
1044
+ if (inputQuery) {
1045
+ this._filterCountries(inputQuery.toLowerCase());
1046
+ } else {
1047
+ this._filterCountries(null, true);
1048
+ }
1049
+ };
1050
+
1051
+ let keyupTimer = null;
1052
+ this._handleSearchChange = () => {
1053
+ // filtering country nodes is expensive (lots of DOM manipulation), so rate limit it
1054
+ if (keyupTimer) {
1055
+ clearTimeout(keyupTimer);
1056
+ }
1057
+ keyupTimer = setTimeout(() => {
1058
+ doFilter();
1059
+ keyupTimer = null;
1060
+ }, 100);
1061
+ };
1062
+ this.searchInput.addEventListener("input", this._handleSearchChange);
1063
+
1064
+ // stop propagation on search input click, so doesn't trigger click-off-to-close listener
1065
+ this.searchInput.addEventListener("click", (e) => e.stopPropagation());
1066
+ }
1067
+ }
1068
+
1069
+ _filterCountries(query, isReset = false) {
1070
+ let isFirst = true;
1071
+ this.countryList.innerHTML = "";
1072
+ for (let i = 0; i < this.countries.length; i++) {
1073
+ const c = this.countries[i];
1074
+ const nameLower = c.name.toLowerCase();
1075
+ const fullDialCode = `+${c.dialCode}`;
1076
+ if (
1077
+ isReset ||
1078
+ nameLower.includes(query) ||
1079
+ fullDialCode.includes(query)
1080
+ ) {
1081
+ this.countryList.appendChild(c.node);
1082
+ // highlight the first item
1083
+ if (isFirst) {
1084
+ this._highlightListItem(c.node, false);
1085
+ isFirst = false;
1086
+ }
1087
+ }
1088
+ }
979
1089
  }
980
1090
 
981
1091
  // highlight the next/prev item in the list (and ensure it is visible)
982
1092
  _handleUpDownKey(key) {
983
1093
  let next =
984
- key === "ArrowUp" || key === "Up"
1094
+ key === "ArrowUp"
985
1095
  ? this.highlightedItem.previousElementSibling
986
1096
  : this.highlightedItem.nextElementSibling;
987
1097
  if (next) {
988
1098
  // skip the divider
989
1099
  if (next.classList.contains("iti__divider")) {
990
1100
  next =
991
- key === "ArrowUp" || key === "Up"
1101
+ key === "ArrowUp"
992
1102
  ? next.previousElementSibling
993
1103
  : next.nextElementSibling;
994
1104
  }
995
- this._highlightListItem(next, true);
1105
+ } else if (this.countryList.childElementCount > 1) {
1106
+ // otherwise, we must be at the end, so loop round again
1107
+ next =
1108
+ key === "ArrowUp"
1109
+ ? this.countryList.lastElementChild
1110
+ : this.countryList.firstElementChild;
1111
+ }
1112
+ if (next) {
1113
+ // if country search enabled, dont lose focus from the search input on up/down
1114
+ const doFocus = !this.options.countrySearch;
1115
+ this._highlightListItem(next, doFocus);
1116
+ if (this.options.countrySearch) {
1117
+ this._scrollTo(next, false);
1118
+ }
996
1119
  }
997
1120
  }
998
1121
 
@@ -1007,9 +1130,7 @@ class Iti {
1007
1130
  _searchForCountry(query) {
1008
1131
  for (let i = 0; i < this.countries.length; i++) {
1009
1132
  if (this._startsWith(this.countries[i].name, query)) {
1010
- const listItem = this.countryList.querySelector(
1011
- `#iti-${this.id}__item-${this.countries[i].iso2}`
1012
- );
1133
+ const listItem = this.countries[i].node;
1013
1134
  // update highlighting and scroll
1014
1135
  this._highlightListItem(listItem, false);
1015
1136
  this._scrollTo(listItem, true);
@@ -1333,7 +1454,7 @@ class Iti {
1333
1454
 
1334
1455
  // close the dropdown and unbind any listeners
1335
1456
  _closeDropdown() {
1336
- this.countryList.classList.add("iti__hide");
1457
+ this.dropdownContent.classList.add("iti__hide");
1337
1458
  this.selectedFlag.setAttribute("aria-expanded", "false");
1338
1459
  this.selectedFlag.removeAttribute("aria-activedescendant");
1339
1460
 
@@ -1342,6 +1463,9 @@ class Iti {
1342
1463
 
1343
1464
  // unbind key events
1344
1465
  document.removeEventListener("keydown", this._handleKeydownOnDropdown);
1466
+ if (this.options.countrySearch) {
1467
+ this.searchInput.removeEventListener("input", this._handleSearchChange);
1468
+ }
1345
1469
  document.documentElement.removeEventListener(
1346
1470
  "click",
1347
1471
  this._handleClickOffToClose
@@ -1367,7 +1491,7 @@ class Iti {
1367
1491
 
1368
1492
  // check if an element is visible within it's container, else scroll until it is
1369
1493
  _scrollTo(element, middle) {
1370
- const container = this.countryList;
1494
+ const container = this.dropdownContent;
1371
1495
  // windowTop from https://stackoverflow.com/a/14384091/217866
1372
1496
  const windowTop = window.pageYOffset || document.documentElement.scrollTop;
1373
1497
  const containerHeight = container.offsetHeight;
@@ -58,10 +58,10 @@ describe("dropdown shortcuts: init plugin (with nationalMode=false, autoInsertDi
58
58
  expect(getListElement()).not.toBeVisible();
59
59
  });
60
60
 
61
- it("pressing up while on the top item does not change the highlighted item", function() {
61
+ it("pressing up while on the top item highlights the bottom item", function() {
62
62
  triggerKeyOnBody("ArrowUp");
63
- var topItem = getListElement().find("li.iti__country:eq(0)");
64
- expect(topItem).toHaveClass("iti__highlight");
63
+ var lastItem = getListElement().find("li.iti__country:last");
64
+ expect(lastItem).toHaveClass("iti__highlight");
65
65
  });
66
66
 
67
67
  it("pressing z highlights Zambia", function() {
@@ -82,23 +82,22 @@ describe("dropdown shortcuts: init plugin (with nationalMode=false, autoInsertDi
82
82
 
83
83
  describe("typing z then i then DOWN", function() {
84
84
 
85
- var lastItem;
86
-
87
85
  beforeEach(function() {
88
- lastItem = getListElement().find("li.iti__country:last");
89
86
  triggerKeyOnBody("z");
90
87
  triggerKeyOnBody("i");
91
88
  triggerKeyOnBody("ArrowDown");
92
89
  });
93
90
 
94
91
  it("highlights the last item, which is Åland Islands", function() {
92
+ var lastItem = getListElement().find("li.iti__country:last");
95
93
  expect(lastItem).toHaveClass("iti__highlight");
96
94
  expect(lastItem.attr("data-country-code")).toEqual("ax");
97
95
  });
98
96
 
99
- it("pressing down while on the last item does not change the highlighted item", function() {
97
+ it("pressing down while on the last item highlights the first item", function() {
100
98
  triggerKeyOnBody("ArrowDown");
101
- expect(lastItem).toHaveClass("iti__highlight");
99
+ var topItem = getListElement().find("li.iti__country:eq(0)");
100
+ expect(topItem).toHaveClass("iti__highlight");
102
101
  });
103
102
  });
104
103