diff --git a/src/components/PhoneInput/PhoneInput.spec.tsx b/src/components/PhoneInput/PhoneInput.spec.tsx index 5410c9795..279d02daa 100644 --- a/src/components/PhoneInput/PhoneInput.spec.tsx +++ b/src/components/PhoneInput/PhoneInput.spec.tsx @@ -42,6 +42,39 @@ describe('PhoneInput', () => { expect(document.activeElement).toEqual(screen.getByLabelText('Phone Number')); }); + it('should only render countries from allowedCountries when provided', async () => { + render(); + + fireEvent.keyDown(screen.getByText(defaultCountry.dialCode), { keyCode: 40 }); + + expect(await screen.findByText('Andorra +376')).toBeInTheDocument(); + expect(screen.queryByText(/Afghanistan/)).not.toBeInTheDocument(); + }); + + it('should not render the selected country value when it is not part of allowedCountries', () => { + render(); + + expect(screen.queryByText(defaultCountry.dialCode)).not.toBeInTheDocument(); + }); + + it('should not render the selected country value when it is not a valid country', () => { + const invalidCountry = { value: 'POTATO', label: 'Potato', dialCode: '+0' }; + render(); + + expect(screen.queryByText('Potato')).not.toBeInTheDocument(); + expect(screen.queryByText('+0')).not.toBeInTheDocument(); + }); + + it('should warn when the selected country is not among the available options', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + render(); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('is not among the available options')); + + consoleErrorSpy.mockRestore(); + }); + it('should call the change handler when typing in the national number input', () => { const mockCountryChangeHandler = jest.fn(); const mockTextChangeHandler = jest.fn(); diff --git a/src/components/PhoneInput/PhoneInput.tsx b/src/components/PhoneInput/PhoneInput.tsx index 59a01fabe..f8a47d913 100644 --- a/src/components/PhoneInput/PhoneInput.tsx +++ b/src/components/PhoneInput/PhoneInput.tsx @@ -1,5 +1,6 @@ import React, { useRef } from 'react'; import styled from 'styled-components'; +import warning from 'warning'; import { compose, layout, @@ -64,6 +65,10 @@ interface PhoneInputProps * Pass props directly to the internal SelectList component used to show prefixes. Any value from the `SelectList` component props are allowed, but props from the `PhoneInput` take precedence */ selectListProps?: SelectListProps; + /** + * Restricts the country list to only these ISO codes (e.g. ['DE', 'FR']). When omitted, all countries are shown. + */ + allowedCountries?: ReadonlyArray; } const Box = styled.div` @@ -83,6 +88,20 @@ const PhoneInput: React.FC = ({ const containerRef = useRef(); const spaceBetweenInputs = variant === 'boxed' ? '0.25rem' : '0.75rem'; + const countries = props.allowedCountries + ? COUNTRIES.filter(it => props.allowedCountries.includes(it.value)) + : COUNTRIES; + + // Avoid rendering a invalid option/country that isn't amongst allowedCountries + const selectedCountry = countries.find(it => it.value === props.country?.value) && props.country; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + warning( + !props.country || Boolean(selectedCountry), + `[@freenow/wave] PhoneInput received a \`country\` ("${props.country?.value}") that is not among the ` + + 'available options and will not be displayed. Ensure it is included in `allowedCountries`.' + ); + const handleCountrySelection = value => { if (props.onCountryChange) { props.onCountryChange(value); @@ -97,9 +116,9 @@ const PhoneInput: React.FC = ({ {...selectListProps} id={`${props.id}-area-code`} name={`${props.name}-area-code`} - value={props.country} + value={selectedCountry} onChange={handleCountrySelection} - options={COUNTRIES.map(it => ({ ...it, label: `${it.label} ${it.dialCode}` }))} + options={countries.map(it => ({ ...it, label: `${it.label} ${it.dialCode}` }))} placeholder="" width="8rem" components={{ diff --git a/src/components/PhoneInput/docs/PhoneInput.stories.tsx b/src/components/PhoneInput/docs/PhoneInput.stories.tsx index 7947553ee..84a2fcb85 100644 --- a/src/components/PhoneInput/docs/PhoneInput.stories.tsx +++ b/src/components/PhoneInput/docs/PhoneInput.stories.tsx @@ -69,6 +69,13 @@ export const Preselected: Story = { } }; +export const AllowedCountries: Story = { + ...Default, + args: { + allowedCountries: ['DE', 'ES', 'FR', 'GB', 'AD'] + } +}; + export const BottomLined: Story = { ...Default, args: { diff --git a/src/components/PhoneInput/docs/PhoneInput.storybook.mdx b/src/components/PhoneInput/docs/PhoneInput.storybook.mdx index 26963a2b5..84dc0bcf4 100644 --- a/src/components/PhoneInput/docs/PhoneInput.storybook.mdx +++ b/src/components/PhoneInput/docs/PhoneInput.storybook.mdx @@ -8,7 +8,7 @@ import * as PhoneInputStories from './PhoneInput.stories'; The `PhoneInput` is a form component for inputting a phone number in international format with usage of country selector. -The component consists of two controls: a select to pick a prefix and an input to type in the phone number. The select shows all the country codes available world-wide. +The component consists of two controls: a select to pick a prefix and an input to type in the phone number. The select shows all the country codes available world-wide by default, or a restricted subset when the `allowedCountries` prop is provided.