diff --git a/packages/docs/docs/02-Usage/01-PhoneInput.md b/packages/docs/docs/02-Usage/01-PhoneInput.md
index 1c14a37..a2baaed 100644
--- a/packages/docs/docs/02-Usage/01-PhoneInput.md
+++ b/packages/docs/docs/02-Usage/01-PhoneInput.md
@@ -62,6 +62,14 @@ description="An array of available countries to select (and guess)"
defaultValue="defaultCountries"
/>
+### `preferredCountries`
+
+
+
### `hideDropdown`
= ({
disabled,
hideDropdown,
countries = defaultCountries,
+ preferredCountries = [],
flags,
renderButtonWrapper,
...styleProps
@@ -186,6 +188,7 @@ export const CountrySelector: React.FC = ({
{
setShowDropdown(false);
diff --git a/src/components/CountrySelector/CountrySelectorDropdown.style.scss b/src/components/CountrySelector/CountrySelectorDropdown.style.scss
index a6f1c5b..fcfd5af 100644
--- a/src/components/CountrySelector/CountrySelectorDropdown.style.scss
+++ b/src/components/CountrySelector/CountrySelectorDropdown.style.scss
@@ -93,6 +93,10 @@ $dropdown-top: var(--react-international-phone-dropdown-left, 44px);
cursor: pointer;
}
+ &--preferred + &:not(&--preferred) {
+ border-top: 1px solid base.$border-color;
+ }
+
&--selected,
&--focused {
background-color: $selected-dropdown-item-background-color;
diff --git a/src/components/CountrySelector/CountrySelectorDropdown.tsx b/src/components/CountrySelector/CountrySelectorDropdown.tsx
index 24f73a4..6312a9f 100644
--- a/src/components/CountrySelector/CountrySelectorDropdown.tsx
+++ b/src/components/CountrySelector/CountrySelectorDropdown.tsx
@@ -1,6 +1,6 @@
import './CountrySelectorDropdown.style.scss';
-import React, { useCallback, useEffect, useRef, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { defaultCountries } from '../../data/countryData';
import { buildClassNames } from '../../style/buildClassNames';
@@ -38,6 +38,7 @@ export interface CountrySelectorDropdownProps
dialCodePrefix?: string;
selectedCountry: CountryIso2;
countries?: CountryData[];
+ preferredCountries?: CountryIso2[];
flags?: CustomFlagImage[];
onSelect?: (country: ParsedCountry) => void;
onClose?: () => void;
@@ -50,6 +51,7 @@ export const CountrySelectorDropdown: React.FC<
dialCodePrefix = '+',
selectedCountry,
countries = defaultCountries,
+ preferredCountries = [],
flags,
onSelect,
onClose,
@@ -58,6 +60,24 @@ export const CountrySelectorDropdown: React.FC<
const listRef = useRef(null);
const lastScrolledCountry = useRef();
+ const preferredCountrySet = useMemo(() => {
+ return new Set(preferredCountries);
+ }, [preferredCountries]);
+
+ const orderedCountries = useMemo(() => {
+ const preferred: CountryData[] = [];
+ const others = [...countries];
+
+ preferredCountries.forEach((iso2) => {
+ const idx = others.findIndex((c) => parseCountry(c).iso2 === iso2);
+ if (idx !== -1) {
+ preferred.push(others.splice(idx, 1)[0]);
+ }
+ });
+
+ return preferred.concat(others);
+ }, [countries, preferredCountries]);
+
const searchRef = useRef<{
updatedAt: Date | undefined;
value: string;
@@ -76,7 +96,7 @@ export const CountrySelectorDropdown: React.FC<
updatedAt: new Date(),
};
- const searchedCountryIndex = countries.findIndex((c) =>
+ const searchedCountryIndex = orderedCountries.findIndex((c) =>
parseCountry(c).name.toLowerCase().startsWith(searchRef.current.value),
);
@@ -88,9 +108,9 @@ export const CountrySelectorDropdown: React.FC<
const getCountryIndex = useCallback(
(country: CountryIso2) => {
- return countries.findIndex((c) => parseCountry(c).iso2 === country);
+ return orderedCountries.findIndex((c) => parseCountry(c).iso2 === country);
},
- [countries],
+ [orderedCountries],
);
const [focusedItemIndex, setFocusedItemIndex] = useState(
@@ -111,7 +131,7 @@ export const CountrySelectorDropdown: React.FC<
);
const moveFocusedItem = (to: 'prev' | 'next' | 'first' | 'last') => {
- const lastPossibleIndex = countries.length - 1;
+ const lastPossibleIndex = orderedCountries.length - 1;
const getNewIndex = (currentIndex: number) => {
if (to === 'prev') return currentIndex - 1;
@@ -133,7 +153,7 @@ export const CountrySelectorDropdown: React.FC<
if (e.key === 'Enter') {
e.preventDefault();
- const focusedCountry = parseCountry(countries[focusedItemIndex]);
+ const focusedCountry = parseCountry(orderedCountries[focusedItemIndex]);
handleCountrySelect(focusedCountry);
return;
}
@@ -180,7 +200,7 @@ export const CountrySelectorDropdown: React.FC<
const scrollToFocusedCountry = useCallback(() => {
if (!listRef.current || focusedItemIndex === undefined) return;
- const focusedCountry = parseCountry(countries[focusedItemIndex]).iso2;
+ const focusedCountry = parseCountry(orderedCountries[focusedItemIndex]).iso2;
if (focusedCountry === lastScrolledCountry.current) return;
const element = listRef.current.querySelector(
@@ -190,7 +210,7 @@ export const CountrySelectorDropdown: React.FC<
scrollToChild(listRef.current, element as HTMLElement);
lastScrolledCountry.current = focusedCountry;
- }, [focusedItemIndex, countries]);
+ }, [focusedItemIndex, orderedCountries]);
// Scroll to focused item on change
useEffect(() => {
@@ -228,10 +248,10 @@ export const CountrySelectorDropdown: React.FC<
onBlur={onClose}
tabIndex={-1}
aria-activedescendant={`react-international-phone__${
- parseCountry(countries[focusedItemIndex]).iso2
+ parseCountry(orderedCountries[focusedItemIndex]).iso2
}-option`}
>
- {countries.map((c, index) => {
+ {orderedCountries.map((c, index) => {
const country = parseCountry(c);
const isSelected = country.iso2 === selectedCountry;
const isFocused = index === focusedItemIndex;
@@ -248,6 +268,7 @@ export const CountrySelectorDropdown: React.FC<
className={buildClassNames({
addPrefix: [
'country-selector-dropdown__list-item',
+ preferredCountrySet.has(country.iso2) && 'country-selector-dropdown__list-item--preferred',
isSelected && 'country-selector-dropdown__list-item--selected',
isFocused && 'country-selector-dropdown__list-item--focused',
],
diff --git a/src/components/PhoneInput/PhoneInput.test.tsx b/src/components/PhoneInput/PhoneInput.test.tsx
index f4bf16f..e4b7529 100644
--- a/src/components/PhoneInput/PhoneInput.test.tsx
+++ b/src/components/PhoneInput/PhoneInput.test.tsx
@@ -898,6 +898,29 @@ describe('PhoneInput', () => {
});
});
+ describe('preferred countries', () => {
+ test('should display preferred countries on top', () => {
+ render(
+ ,
+ );
+
+ expect(getCountrySelectorDropdown().childNodes[0]).toBe(getDropdownOption('us'));
+ expect(getCountrySelectorDropdown().childNodes[1]).toBe(getDropdownOption('gb'));
+ });
+
+ test('should ignore invalid preferred countries', () => {
+ render(
+ ,
+ );
+
+ expect(getCountrySelectorDropdown().childNodes[0]).toBe(getDropdownOption('us'));
+ });
+ });
+
describe('cursor position', () => {
const user = userEvent.setup({ delay: null });
diff --git a/src/components/PhoneInput/PhoneInput.tsx b/src/components/PhoneInput/PhoneInput.tsx
index ffb7545..f5adadc 100644
--- a/src/components/PhoneInput/PhoneInput.tsx
+++ b/src/components/PhoneInput/PhoneInput.tsx
@@ -101,6 +101,7 @@ export const PhoneInput = forwardRef(
value,
onChange,
countries = defaultCountries,
+ preferredCountries = [],
hideDropdown,
showDisabledDialCodeAndPrefix,
flags,
@@ -181,6 +182,7 @@ export const PhoneInput = forwardRef(
flags={flags}
selectedCountry={country.iso2}
countries={countries}
+ preferredCountries={preferredCountries}
disabled={disabled}
hideDropdown={hideDropdown}
{...countrySelectorStyleProps}
diff --git a/src/hooks/usePhoneInput.ts b/src/hooks/usePhoneInput.ts
index d823603..d510a8f 100644
--- a/src/hooks/usePhoneInput.ts
+++ b/src/hooks/usePhoneInput.ts
@@ -26,11 +26,17 @@ export interface UsePhoneInputConfig {
value?: string;
/**
- * @description Array of available countries for guessing
+ * @description Array of available countries for guessing.
* @default defaultCountries // full country list
*/
countries?: CountryData[];
+ /**
+ * @description Countries to display at the top of the list of dropdown options.
+ * @default []
+ */
+ preferredCountries?: CountryIso2[];
+
/**
* @description Prefix for phone value.
* @default "+"
@@ -128,6 +134,7 @@ export const defaultConfig: Required<
disableDialCodeAndPrefix: false,
disableFormatting: false,
countries: defaultCountries,
+ preferredCountries: [],
};
export const usePhoneInput = ({
diff --git a/src/stories/PhoneInput/PhoneInput.stories.tsx b/src/stories/PhoneInput/PhoneInput.stories.tsx
index dca92d6..f31c1d3 100644
--- a/src/stories/PhoneInput/PhoneInput.stories.tsx
+++ b/src/stories/PhoneInput/PhoneInput.stories.tsx
@@ -21,6 +21,7 @@ import { HiddenDialCode } from './stories/HiddenDialCode.story';
import { WithCodePreview } from './stories/WithCodePreview.story';
import { CustomStyles } from './stories/CustomStyles.story';
import { OnlyBalticCountries } from './stories/OnlyBalticCountries.story';
+import { PreferredCountries } from './stories/PreferredCountries.story';
import { WithAutofocus } from './stories/WithAutofocus.story';
import { DisableFormatting } from './stories/DisableFormatting.story';
import { ControlledMode } from './stories/ControlledMode.story';
@@ -36,6 +37,7 @@ export const _HiddenDialCode = HiddenDialCode;
export const _WithCodePreview = WithCodePreview;
export const _CustomStyles = CustomStyles;
export const _OnlyBalticCountries = OnlyBalticCountries;
+export const _PreferredCountries = PreferredCountries;
export const _WithAutofocus = WithAutofocus;
export const _DisableFormatting = DisableFormatting;
export const _ControlledMode = ControlledMode;
diff --git a/src/stories/PhoneInput/stories/PreferredCountries.story.tsx b/src/stories/PhoneInput/stories/PreferredCountries.story.tsx
new file mode 100644
index 0000000..d8401ed
--- /dev/null
+++ b/src/stories/PhoneInput/stories/PreferredCountries.story.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+
+import { PhoneInput } from '../../../index';
+import { PhoneInputStory } from '../PhoneInput.stories';
+
+export const PreferredCountries: PhoneInputStory = {
+ name: 'With Preferred Countries',
+ render: (args) => ,
+ args: {
+ preferredCountries: ['us', 'gb'],
+ },
+};