Skip to content

Commit 37bf9a8

Browse files
jaruesinkJake Ruesink
andcommitted
Refactor phone input component for improved usability and styling
- Update phone input stories to use consistent labels and props - Change 'international' prop to 'isInternational' for clarity - Enhance form submission handling and success message verification - Adjust tests to reflect new input behavior and validation - Improve styling and structure of phone input field for better user experience Co-authored-by: Jake Ruesink <jake@lambdacurry.com>
1 parent b5cd28f commit 37bf9a8

6 files changed

Lines changed: 195 additions & 255 deletions

File tree

apps/docs/src/remix-hook-form/phone-input.stories.tsx

Lines changed: 46 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hoo
88
import { z } from 'zod';
99
import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub';
1010

11+
const successMessageRegex = /Form submitted successfully/;
12+
1113
// Define a schema for phone number validation
1214
const formSchema = z.object({
1315
usaPhone: z.string().min(1, 'USA phone number is required'),
@@ -37,16 +39,14 @@ const ControlledPhoneInputExample = () => {
3739
<div className="grid gap-8">
3840
<PhoneInput
3941
name="usaPhone"
40-
label="USA Phone Number"
42+
label="Phone Number"
4143
description="Enter a US phone number"
42-
defaultCountry="US"
43-
international={false}
4444
/>
4545
<PhoneInput
4646
name="internationalPhone"
4747
label="International Phone Number"
4848
description="Enter an international phone number"
49-
international={true}
49+
isInternational={true}
5050
/>
5151
</div>
5252
<Button type="submit" className="mt-8">
@@ -65,8 +65,8 @@ const handleFormSubmission = async (request: Request) => {
6565
return { errors };
6666
}
6767

68-
return {
69-
message: `Form submitted successfully! USA: ${data.usaPhone}, International: ${data.internationalPhone}`
68+
return {
69+
message: `Form submitted successfully! USA: ${data.usaPhone}, International: ${data.internationalPhone}`,
7070
};
7171
};
7272

@@ -75,6 +75,12 @@ const meta: Meta<typeof PhoneInput> = {
7575
component: PhoneInput,
7676
parameters: { layout: 'centered' },
7777
tags: ['autodocs'],
78+
} satisfies Meta<typeof PhoneInput>;
79+
80+
export default meta;
81+
type Story = StoryObj<typeof meta>;
82+
83+
export const Default: Story = {
7884
decorators: [
7985
withReactRouterStubDecorator({
8086
routes: [
@@ -86,12 +92,6 @@ const meta: Meta<typeof PhoneInput> = {
8692
],
8793
}),
8894
],
89-
} satisfies Meta<typeof PhoneInput>;
90-
91-
export default meta;
92-
type Story = StoryObj<typeof meta>;
93-
94-
export const Default: Story = {
9595
parameters: {
9696
docs: {
9797
description: {
@@ -125,16 +125,14 @@ const ControlledPhoneInputExample = () => {
125125
<div className="grid gap-8">
126126
<PhoneInput
127127
name="usaPhone"
128-
label="USA Phone Number"
128+
label="Phone Number"
129129
description="Enter a US phone number"
130-
defaultCountry="US"
131-
international={false}
132130
/>
133131
<PhoneInput
134132
name="internationalPhone"
135133
label="International Phone Number"
136134
description="Enter an international phone number"
137-
international={true}
135+
isInternational
138136
/>
139137
</div>
140138
<Button type="submit" className="mt-8">
@@ -152,21 +150,21 @@ const ControlledPhoneInputExample = () => {
152150
const canvas = within(canvasElement);
153151

154152
await step('Verify initial state', async () => {
155-
// Verify phone input fields are present
156-
const usaPhoneLabel = canvas.getByLabelText('USA Phone Number');
157-
const internationalPhoneLabel = canvas.getByLabelText('International Phone Number');
158-
153+
// Wait for inputs to be mounted and associated with their labels
154+
const usaPhoneLabel = await canvas.findByLabelText('Phone Number');
155+
const internationalPhoneLabel = await canvas.findByLabelText('International Phone Number');
156+
159157
expect(usaPhoneLabel).toBeInTheDocument();
160158
expect(internationalPhoneLabel).toBeInTheDocument();
161159

162-
// Verify submit button is present
163-
const submitButton = canvas.getByRole('button', { name: 'Submit' });
160+
// Wait for submit button to be present
161+
const submitButton = await canvas.findByRole('button', { name: 'Submit' });
164162
expect(submitButton).toBeInTheDocument();
165163
});
166164

167165
await step('Test validation errors on invalid submission', async () => {
168166
// Submit form without entering phone numbers
169-
const submitButton = canvas.getByRole('button', { name: 'Submit' });
167+
const submitButton = await canvas.findByRole('button', { name: 'Submit' });
170168
await userEvent.click(submitButton);
171169

172170
// Verify validation error messages appear
@@ -175,27 +173,36 @@ const ControlledPhoneInputExample = () => {
175173
});
176174

177175
await step('Test successful form submission with valid phone numbers', async () => {
178-
// Enter valid phone numbers
179-
const usaPhoneInput = canvas.getByLabelText('USA Phone Number');
180-
const internationalPhoneInput = canvas.getByLabelText('International Phone Number');
176+
// Enter valid phone numbers (await the inputs before typing)
177+
const usaPhoneInput = await canvas.findByLabelText('Phone Number');
178+
const internationalPhoneInput = await canvas.findByLabelText('International Phone Number');
181179

182-
// Enter a US phone number
180+
// Enter a US phone number (should format to (202) 555-0123)
183181
await userEvent.type(usaPhoneInput, '2025550123');
184-
185-
// Enter an international phone number (UK format)
182+
183+
// Enter an international phone number (UK example digits; component will normalize & format with + and spaces)
186184
await userEvent.type(internationalPhoneInput, '7911123456');
187185

188186
// Submit form
189-
const submitButton = canvas.getByRole('button', { name: 'Submit' });
187+
const submitButton = await canvas.findByRole('button', { name: 'Submit' });
190188
await userEvent.click(submitButton);
191189

192-
// Verify success message
193-
await expect(canvas.findByText(/Form submitted successfully/)).resolves.toBeInTheDocument();
190+
// Verify success message (regex matches the prefix of the success text)
191+
await expect(canvas.findByText(successMessageRegex)).resolves.toBeInTheDocument();
194192
});
195193
},
196194
};
197195

198196
export const WithCustomStyling: Story = {
197+
decorators: [
198+
withReactRouterStubDecorator({
199+
routes: [
200+
{
201+
path: '/',
202+
},
203+
],
204+
}),
205+
],
199206
render: () => {
200207
const fetcher = useFetcher<{ message: string }>();
201208
const methods = useRemixForm<FormData>({
@@ -219,85 +226,16 @@ export const WithCustomStyling: Story = {
219226
name="usaPhone"
220227
label="Custom Styled Phone Input"
221228
description="With custom styling applied"
222-
defaultCountry="US"
223229
className="border-2 border-blue-500 p-4 rounded-lg"
224230
inputClassName="bg-gray-100"
225-
selectClassName="bg-gray-100 border-blue-300"
226-
/>
227-
</div>
228-
<Button type="submit" className="mt-8">
229-
Submit
230-
</Button>
231-
</fetcher.Form>
232-
</RemixFormProvider>
233-
);
234-
},
235-
parameters: {
236-
docs: {
237-
description: {
238-
story: 'Phone input with custom styling applied.',
239-
},
240-
},
241-
},
242-
};
243-
244-
export const WithDifferentDefaultCountries: Story = {
245-
render: () => {
246-
const fetcher = useFetcher<{ message: string }>();
247-
const methods = useRemixForm<{
248-
usPhone: string;
249-
ukPhone: string;
250-
canadaPhone: string;
251-
australiaPhone: string;
252-
}>({
253-
resolver: zodResolver(
254-
z.object({
255-
usPhone: z.string().optional(),
256-
ukPhone: z.string().optional(),
257-
canadaPhone: z.string().optional(),
258-
australiaPhone: z.string().optional(),
259-
})
260-
),
261-
defaultValues: {
262-
usPhone: '',
263-
ukPhone: '',
264-
canadaPhone: '',
265-
australiaPhone: '',
266-
},
267-
fetcher,
268-
submitConfig: {
269-
action: '/',
270-
method: 'post',
271-
},
272-
});
273-
274-
return (
275-
<RemixFormProvider {...methods}>
276-
<fetcher.Form onSubmit={methods.handleSubmit}>
277-
<div className="grid gap-8">
278-
<PhoneInput
279-
name="usPhone"
280-
label="US Phone Number"
281-
defaultCountry="US"
282-
international={true}
283231
/>
284232
<PhoneInput
285-
name="ukPhone"
286-
label="UK Phone Number"
287-
defaultCountry="GB"
288-
international={true}
289-
/>
290-
<PhoneInput
291-
name="canadaPhone"
292-
label="Canada Phone Number"
293-
defaultCountry="CA"
294-
international={true}
295-
/>
296-
<PhoneInput
297-
name="australiaPhone"
298-
label="Australia Phone Number"
299-
defaultCountry="AU"
300-
international={true}
233+
name="internationalPhone"
234+
label="Custom Styled Intl Phone Input"
235+
description="With custom styling applied"
236+
isInternational
237+
className="border-2 border-blue-500 p-4 rounded-lg"
238+
inputClassName="bg-gray-100"
301239
/>
302240
</div>
303241
<Button type="submit" className="mt-8">
@@ -310,9 +248,8 @@ export const WithDifferentDefaultCountries: Story = {
310248
parameters: {
311249
docs: {
312250
description: {
313-
story: 'Phone inputs with different default countries.',
251+
story: 'Phone input with custom styling applied for US and International modes.',
314252
},
315253
},
316254
},
317255
};
318-

apps/docs/src/remix-hook-form/phone-input.test.tsx

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,13 @@ const TestPhoneInputForm = ({
5353
name="usaPhone"
5454
label="USA Phone Number"
5555
description="Enter a US phone number"
56-
defaultCountry="US"
57-
international={false}
5856
components={customComponents}
5957
/>
6058
<PhoneInput
6159
name="internationalPhone"
6260
label="International Phone Number"
6361
description="Enter an international phone number"
64-
international={true}
62+
isInternational
6563
components={customComponents}
6664
/>
6765
<Button type="submit">Submit</Button>
@@ -86,9 +84,6 @@ describe('PhoneInput Component', () => {
8684
// Check for descriptions
8785
expect(screen.getByText('Enter a US phone number')).toBeInTheDocument();
8886
expect(screen.getByText('Enter an international phone number')).toBeInTheDocument();
89-
90-
// Check for country select
91-
expect(screen.getAllByLabelText('Country code')).toHaveLength(2);
9287
});
9388

9489
it('displays validation errors when provided', async () => {
@@ -106,34 +101,36 @@ describe('PhoneInput Component', () => {
106101
});
107102

108103
describe('Input Behavior', () => {
109-
it('allows entering a phone number', async () => {
104+
it('formats and caps US number at 10 digits', async () => {
110105
const user = userEvent.setup();
111106
render(<TestPhoneInputForm />);
112107

113-
const usaPhoneInput = screen.getByLabelText('USA Phone Number');
114-
115-
// Type a US phone number
116-
await user.type(usaPhoneInput, '2025550123');
117-
118-
// Check that the input contains the number
108+
const usaPhoneInput = screen.getByLabelText('USA Phone Number') as HTMLInputElement;
109+
110+
// Type more than 10 digits
111+
await user.type(usaPhoneInput, '2025550123456');
112+
113+
// Display should be formatted and capped: (202) 555-0123
119114
await waitFor(() => {
120-
expect(usaPhoneInput).toHaveValue('2025550123');
115+
expect(usaPhoneInput.value).toBe('(202) 555-0123');
121116
});
122117
});
123118

124-
it('allows selecting a different country', async () => {
119+
it('accepts international number with + and inserts spaces', async () => {
125120
const user = userEvent.setup();
126121
render(<TestPhoneInputForm />);
127122

128-
// Get the country select for international phone
129-
const countrySelects = screen.getAllByLabelText('Country code');
130-
const internationalCountrySelect = countrySelects[1];
131-
132-
// Change country to UK (GB)
133-
await user.selectOptions(internationalCountrySelect, 'GB');
134-
135-
// Check that the select has the new value
136-
expect(internationalCountrySelect).toHaveValue('GB');
123+
const intlInput = screen.getByLabelText('International Phone Number') as HTMLInputElement;
124+
125+
// Type digits without +; component should normalize to + and format
126+
await user.type(intlInput, '7911123456');
127+
128+
await waitFor(() => {
129+
expect(intlInput.value.startsWith('+')).toBe(true);
130+
// Digits (without non-digits) should match what was typed with leading country code
131+
const digitsOnly = intlInput.value.replace(/\D/g, '');
132+
expect(digitsOnly.endsWith('7911123456')).toBe(true);
133+
});
137134
});
138135
});
139136

@@ -162,18 +159,13 @@ describe('PhoneInput Component', () => {
162159

163160
const usaPhoneLabel = screen.getByText('USA Phone Number');
164161
const internationalPhoneLabel = screen.getByText('International Phone Number');
165-
162+
166163
expect(usaPhoneLabel).toBeInTheDocument();
167164
expect(internationalPhoneLabel).toBeInTheDocument();
168-
165+
169166
// Verify labels are properly associated with inputs
170167
expect(screen.getByLabelText('USA Phone Number')).toBeInTheDocument();
171168
expect(screen.getByLabelText('International Phone Number')).toBeInTheDocument();
172-
173-
// Verify country selects have proper aria-labels
174-
const countrySelects = screen.getAllByLabelText('Country code');
175-
expect(countrySelects).toHaveLength(2);
176169
});
177170
});
178171
});
179-

packages/components/src/remix-hook-form/phone-input.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,3 @@ export const PhoneInput = function RemixPhoneInput(props: PhoneInputProps & { re
2626
};
2727

2828
PhoneInput.displayName = 'PhoneInput';
29-

0 commit comments

Comments
 (0)