From 6b8e264be349ec610e9fd7df8b28d51743b54fde Mon Sep 17 00:00:00 2001 From: Iris Rossell Vitorica Date: Fri, 21 Aug 2026 10:54:53 +0200 Subject: [PATCH 1/2] feat(phone-input): add allowedCountries prop to PhoneInput Lets consumers restrict the country dropdown to a fixed allowlist of ISO codes. Optional and backward compatible: when omitted, all countries render as before. Co-Authored-By: Claude Sonnet 5 --- src/components/PhoneInput/PhoneInput.spec.tsx | 9 +++++++++ src/components/PhoneInput/PhoneInput.tsx | 10 +++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/components/PhoneInput/PhoneInput.spec.tsx b/src/components/PhoneInput/PhoneInput.spec.tsx index 5410c9795..3aa0afac5 100644 --- a/src/components/PhoneInput/PhoneInput.spec.tsx +++ b/src/components/PhoneInput/PhoneInput.spec.tsx @@ -42,6 +42,15 @@ 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 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..137458a92 100644 --- a/src/components/PhoneInput/PhoneInput.tsx +++ b/src/components/PhoneInput/PhoneInput.tsx @@ -64,6 +64,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 +87,10 @@ 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; + const handleCountrySelection = value => { if (props.onCountryChange) { props.onCountryChange(value); @@ -99,7 +107,7 @@ const PhoneInput: React.FC = ({ name={`${props.name}-area-code`} value={props.country} 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={{ From 06259f2be1032a76c83a73dc35e4b282c586cfee Mon Sep 17 00:00:00 2001 From: Iris Rossell Vitorica Date: Tue, 25 Aug 2026 09:22:14 +0200 Subject: [PATCH 2/2] feat(phone-input): guard selected country against allowedCountries --- src/components/PhoneInput/PhoneInput.spec.tsx | 24 +++++++++++++++++++ src/components/PhoneInput/PhoneInput.tsx | 13 +++++++++- .../PhoneInput/docs/PhoneInput.stories.tsx | 7 ++++++ .../PhoneInput/docs/PhoneInput.storybook.mdx | 2 +- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/components/PhoneInput/PhoneInput.spec.tsx b/src/components/PhoneInput/PhoneInput.spec.tsx index 3aa0afac5..279d02daa 100644 --- a/src/components/PhoneInput/PhoneInput.spec.tsx +++ b/src/components/PhoneInput/PhoneInput.spec.tsx @@ -51,6 +51,30 @@ describe('PhoneInput', () => { 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 137458a92..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, @@ -91,6 +92,16 @@ const PhoneInput: React.FC = ({ ? 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); @@ -105,7 +116,7 @@ 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}` }))} placeholder="" 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.