{/* Icon fills use 'currentColor' (from text color) for better accessibility in HCM mode */}
- {type === 'error' &&
@@ -86,7 +101,12 @@ export const Banner = ({
className={classNames(
'shrink-0 self-start hover:backdrop-saturate-150 focus:backdrop-saturate-200',
type === 'error' && 'hover:bg-red-200 focus:bg-red-300',
- type === 'info' && 'hover:bg-blue-100 focus:bg-blue-200',
+ type === 'info' &&
+ !isFancy &&
+ 'hover:bg-blue-100 focus:bg-blue-200',
+ type === 'info' &&
+ isFancy &&
+ 'hover:bg-gradient-to-tr hover:from-blue-700/10 hover:to-purple-600/10 focus:bg-gradient-to-tr focus:from-blue-800/10 focus:to-purple-700/10',
type === 'success' && 'hover:bg-green-400 focus:bg-green-500',
type === 'warning' && 'hover:bg-orange-100 focus:bg-orange-200'
)}
diff --git a/packages/fxa-settings/src/components/Banner/interfaces.ts b/packages/fxa-settings/src/components/Banner/interfaces.ts
index 0e44dc03a33..d3239de30b7 100644
--- a/packages/fxa-settings/src/components/Banner/interfaces.ts
+++ b/packages/fxa-settings/src/components/Banner/interfaces.ts
@@ -20,6 +20,8 @@ export type BannerProps = {
animation?: Animation;
dismissButton?: DismissButtonProps;
link?: BannerLinkProps;
+ isFancy?: boolean;
+ bannerId?: string;
};
export type Animation = {
diff --git a/packages/fxa-settings/src/components/FormPhoneNumber/index.stories.tsx b/packages/fxa-settings/src/components/FormPhoneNumber/index.stories.tsx
index 9ea770a4f5e..0059046175d 100644
--- a/packages/fxa-settings/src/components/FormPhoneNumber/index.stories.tsx
+++ b/packages/fxa-settings/src/components/FormPhoneNumber/index.stories.tsx
@@ -6,22 +6,55 @@ import React from 'react';
import FormPhoneNumber from '.';
import { withLocalization } from 'fxa-react/lib/storybooks';
import { Meta } from '@storybook/react';
+import { action } from '@storybook/addon-actions';
import AppLayout from '../AppLayout';
export default {
title: 'Components/FormPhoneNumber',
component: FormPhoneNumber,
- decorators: [withLocalization],
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ withLocalization,
+ ],
} as Meta;
+const mockSubmit = async (phoneNumber: string) => {
+ action('submitPhoneNumber')(phoneNumber);
+ return { hasErrors: false };
+};
+
export const Default = () => (
-
-
-
+
+);
+
+export const WithError = () => (
+
{
+ action('submitPhoneNumber')();
+ return { hasErrors: true };
+ }}
+ />
);
-export const WithInfo = () => (
-
-
-
+export const WithInfoBanner = () => (
+
);
diff --git a/packages/fxa-settings/src/components/FormPhoneNumber/index.test.tsx b/packages/fxa-settings/src/components/FormPhoneNumber/index.test.tsx
index 44da8b63865..243da99cf86 100644
--- a/packages/fxa-settings/src/components/FormPhoneNumber/index.test.tsx
+++ b/packages/fxa-settings/src/components/FormPhoneNumber/index.test.tsx
@@ -4,14 +4,36 @@
import React from 'react';
import { screen, waitFor } from '@testing-library/react';
-import userEvent, { UserEvent } from '@testing-library/user-event';
+import userEvent from '@testing-library/user-event';
import FormPhoneNumber from '.';
import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider';
+const mockSubmit = jest.fn();
+
describe('FormPhoneNumber', () => {
function render() {
renderWithLocalizationProvider(
-
+
+ );
+ }
+
+ function renderWithInfoBannerProps() {
+ renderWithLocalizationProvider(
+
);
}
@@ -26,6 +48,32 @@ describe('FormPhoneNumber', () => {
});
}
+ it('renders the component as expected', async () => {
+ render();
+ await waitFor(() => {
+ expect(screen.getByRole('combobox')).toBeInTheDocument();
+ expect(getPhoneInput()).toBeInTheDocument();
+ expect(getSubmitButton()).toBeInTheDocument();
+ });
+ });
+
+ it('renders the component with info banner', async () => {
+ renderWithInfoBannerProps();
+ await waitFor(() => {
+ expect(screen.getByRole('combobox')).toBeInTheDocument();
+ });
+ expect(getPhoneInput()).toBeInTheDocument();
+ expect(getSubmitButton()).toBeInTheDocument();
+
+ expect(screen.getByText('This is a banner heading')).toBeInTheDocument();
+ expect(
+ screen.getByText('This is a banner description')
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('link', { name: /This is a banner link/ })
+ ).toBeInTheDocument();
+ });
+
it('submit button is disabled by default', async () => {
render();
await waitFor(() => {
@@ -34,49 +82,38 @@ describe('FormPhoneNumber', () => {
});
describe('form validity and submission', () => {
- // alertMock is temporary until real functionality is implemented
- let alertMock: jest.SpyInstance;
- beforeEach(() => {
- alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
- });
- afterEach(() => {
- alertMock.mockRestore();
- });
it('submit button is enabled for valid North American number, 1231231234, and is formatted as expected', async () => {
const user = userEvent.setup();
render();
- await waitFor(async () => await user.type(getPhoneInput(), '1231231234'));
+ await waitFor(() => user.type(getPhoneInput(), '1231231234'));
expect(getSubmitButton()).toBeEnabled();
- await waitFor(async () => await user.click(getSubmitButton()));
- expect(alertMock).toHaveBeenCalledWith(
- 'formattedPhoneNumber: +11231231234'
+ user.click(getSubmitButton());
+ await waitFor(() =>
+ expect(mockSubmit).toHaveBeenCalledWith('+11231231234')
);
});
it('submit button is enabled for valid North American number, (123) 123-1234, and is formatted as expected', async () => {
const user = userEvent.setup();
- const alertMock = jest
- .spyOn(window, 'alert')
- .mockImplementation(() => {});
render();
await waitFor(
async () => await user.type(getPhoneInput(), '(123) 123-1234')
);
expect(getSubmitButton()).toBeEnabled();
await waitFor(async () => await user.click(getSubmitButton()));
- expect(alertMock).toHaveBeenCalledWith(
- 'formattedPhoneNumber: +11231231234'
- );
+ expect(mockSubmit).toHaveBeenCalledWith('+11231231234');
});
- it('submit button is disabled for invalid 11-digit phoneValidationNorthAmerica, 12312312345', async () => {
+ it('input value is restricted to 10 digits', async () => {
const user = userEvent.setup();
render();
await waitFor(
async () => await user.type(getPhoneInput(), '12312312345')
);
- expect(getSubmitButton()).toBeDisabled();
+ expect(getPhoneInput()).toHaveValue('123-123-1234');
+ expect(getSubmitButton()).toBeEnabled();
});
+
it('submit button is disabled for invalid number with letters phoneValidationNorthAmerica, abc12312345', async () => {
const user = userEvent.setup();
render();
diff --git a/packages/fxa-settings/src/components/FormPhoneNumber/index.tsx b/packages/fxa-settings/src/components/FormPhoneNumber/index.tsx
index e280c0b84b7..87a4c75b608 100644
--- a/packages/fxa-settings/src/components/FormPhoneNumber/index.tsx
+++ b/packages/fxa-settings/src/components/FormPhoneNumber/index.tsx
@@ -3,56 +3,115 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import React from 'react';
-import { useForm } from 'react-hook-form';
+import { useForm, useWatch } from 'react-hook-form';
import InputPhoneNumber from '../InputPhoneNumber';
import Banner from '../Banner';
+import { BannerContentProps, BannerLinkProps } from '../Banner/interfaces';
export interface InputPhoneNumberData {
phoneNumber: string;
countryCode: string;
}
+export type FormPhoneNumberProps = {
+ infoBannerContent?: BannerContentProps;
+ infoBannerLink?: BannerLinkProps;
+ localizedCTAText: string;
+ submitPhoneNumber: (phoneNumber: string) => Promise<{ hasErrors: boolean }>;
+ errorBannerId?: string;
+};
+
const FormPhoneNumber = ({
- showInfo = false,
+ infoBannerContent,
+ infoBannerLink,
localizedCTAText,
-}: {
- showInfo?: boolean;
- localizedCTAText: string;
-}) => {
- const { handleSubmit, register, formState } = useForm({
- mode: 'onChange',
- criteriaMode: 'all',
- defaultValues: {
- phoneNumber: '',
- countryCode: '',
- },
+ submitPhoneNumber,
+ errorBannerId,
+}: FormPhoneNumberProps) => {
+ const [hasErrors, setHasErrors] = React.useState(false);
+ const { control, formState, handleSubmit, register } =
+ useForm({
+ mode: 'onChange',
+ criteriaMode: 'all',
+ defaultValues: {
+ phoneNumber: '',
+ countryCode: '',
+ },
+ });
+
+ // Use `useWatch` to observe the `phoneNumber` field without causing re-renders
+ const phoneNumberInput: string | undefined = useWatch({
+ control,
+ name: 'phoneNumber',
});
- const onSubmit = async ({
+ const formatPhoneNumber = ({
phoneNumber,
countryCode,
}: InputPhoneNumberData) => {
// Strip everything that isn't a number
const strippedNumber = phoneNumber.replace(/\D/g, '');
- const formattedPhoneNumber = countryCode + strippedNumber;
+ return countryCode + strippedNumber;
+ };
- // TODO, actually send this value where needed
- alert(`formattedPhoneNumber: ${formattedPhoneNumber}`);
+ const onSubmit = async ({
+ phoneNumber,
+ countryCode,
+ }: InputPhoneNumberData) => {
+ setHasErrors(false);
+ const formattedPhoneNumber = formatPhoneNumber({
+ phoneNumber,
+ countryCode,
+ });
+ const result = await submitPhoneNumber(formattedPhoneNumber);
+ if (result !== undefined && result.hasErrors) {
+ setHasErrors(true);
+ const phoneInput = document.querySelector(
+ 'input[name="phoneNumber"]'
+ ) as HTMLInputElement;
+ phoneInput && phoneInput.focus();
+ }
};
return (