Skip to content

Commit 0fdc4d8

Browse files
authored
TelephoneNumber Input (#151)
1 parent 50b83fd commit 0fdc4d8

11 files changed

Lines changed: 521 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- `i18n-iso-countries` required peer dependencies.
13+
- `TelephoneNumberInput` which helps in order to select nationality and national number.
14+
15+
1. It's needed to include icons style.
16+
17+
```tsx
18+
import "node_modules/flag-icons/css/flag-icons.min.css";
19+
```
20+
21+
2. Labels are by default the country code. In order to use localized names, you need to register locale on your own
22+
Locale is automatically recognized if only one is registered on your project. Otherwise you need to provide the `locale` property with the desidered locale.
23+
24+
```tsx
25+
import { registerLocale } from "i18n-iso-countries";
26+
import countriesEn from "i18n-iso-countries/langs/en.json";
27+
28+
registerLocale(countriesEn);
29+
```
30+
31+
3. for `yup` validation, it's possible to use utilities from `google-libphonenumber`.
32+
1033
## [3.11.2] - 2025-10-15
1134

1235
### Fixed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Form, TelephoneNumberInput } from "react-hook-form-components";
2+
import { faker } from "@faker-js/faker";
3+
import { mount } from "cypress/react18";
4+
5+
it("correctly set telephone number", () => {
6+
const name = faker.random.alpha(10);
7+
8+
mount(
9+
<div className="p-4">
10+
<Form onSubmit={cy.spy().as("onSubmitSpy")}>
11+
<TelephoneNumberInput name={name} useBootstrapStyle label="Phone Number" defaultCountry="CH" />
12+
<input type="submit" className="mt-4" />
13+
</Form>
14+
</div>,
15+
);
16+
17+
cy.get("input[type=submit]").click();
18+
cy.get("@onSubmitSpy").should("be.calledWith", { [name]: undefined });
19+
20+
cy.get(`#${name}`).type("123456789");
21+
cy.get("input[type=submit]").click();
22+
cy.get("@onSubmitSpy").should("be.calledWith", { [name]: "+41123456789" });
23+
});
24+
25+
it("correctly change country telephone number", () => {
26+
const name = faker.random.alpha(10);
27+
28+
mount(
29+
<div className="p-4">
30+
<Form onSubmit={cy.spy().as("onSubmitSpy")}>
31+
<TelephoneNumberInput name={name} useBootstrapStyle label="Phone Number" defaultCountry="CH" />
32+
<input type="submit" className="mt-4" />
33+
</Form>
34+
</div>,
35+
);
36+
37+
cy.get(`.MuiInputAdornment-root`).click();
38+
cy.get(`.MuiInputBase-input:not(#${name})`).clear();
39+
cy.get(`.MuiInputBase-input:not(#${name})`).type("IT");
40+
cy.get(`.MuiAutocomplete-option`).click();
41+
cy.get(`#${name}`).type("1234567890");
42+
43+
cy.get("input[type=submit]").click();
44+
cy.get("@onSubmitSpy").should("be.calledWith", { [name]: "+391234567890" });
45+
});
46+
47+
it("recognize telephone number coming from default values (even if specified in different format)", () => {
48+
const name = faker.random.alpha(10);
49+
50+
mount(
51+
<div className="p-4">
52+
<Form defaultValues={{ [name]: "+4 4 123456789" }} onSubmit={cy.spy().as("onSubmitSpy")}>
53+
<TelephoneNumberInput name={name} useBootstrapStyle label="Phone Number" defaultCountry="CH" />
54+
<input type="submit" className="mt-4" />
55+
</Form>
56+
</div>,
57+
);
58+
59+
cy.get(`.MuiInputAdornment-root`).contains("+44");
60+
cy.get(`#${name}`).should("have.value", "123456789");
61+
});

cypress/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
"@fortawesome/free-solid-svg-icons": "^6.4.0",
1717
"@fortawesome/react-fontawesome": "^0.2.0",
1818
"@hookform/resolvers": "^2.9.10",
19+
"google-libphonenumber": "^3.2.22",
20+
"i18n-iso-countries": "^7.14.0",
1921
"next": "13.0.2",
2022
"react": "link:../node_modules/react",
2123
"react-dom": "link:../node_modules/react-dom",
@@ -26,6 +28,7 @@
2628
"devDependencies": {
2729
"@faker-js/faker": "^7.6.0",
2830
"@neolution-ch/eslint-config-neolution": "2.1.0",
31+
"@types/google-libphonenumber": "^7.4.30",
2932
"@types/node": "18.11.9",
3033
"@types/react": "18.0.25",
3134
"@types/react-dom": "18.0.8",

cypress/yarn.lock

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,11 @@
346346
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
347347
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
348348

349+
"@types/google-libphonenumber@^7.4.30":
350+
version "7.4.30"
351+
resolved "https://registry.yarnpkg.com/@types/google-libphonenumber/-/google-libphonenumber-7.4.30.tgz#a47ed8f1f237bd43edbd1c8aff24400b0fd9b2fe"
352+
integrity sha512-Td1X1ayRxePEm6/jPHUBs2tT6TzW1lrVB6ZX7ViPGellyzO/0xMNi+wx5nH6jEitjznq276VGIqjK5qAju0XVw==
353+
349354
"@types/json-schema@^7.0.15":
350355
version "7.0.15"
351356
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
@@ -1195,6 +1200,11 @@ delayed-stream@~1.0.0:
11951200
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
11961201
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
11971202

1203+
diacritics@1.3.0:
1204+
version "1.3.0"
1205+
resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1"
1206+
integrity sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==
1207+
11981208
doctrine@^2.1.0:
11991209
version "2.1.0"
12001210
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@@ -2058,6 +2068,11 @@ globalthis@^1.0.4:
20582068
define-properties "^1.2.1"
20592069
gopd "^1.0.1"
20602070

2071+
google-libphonenumber@^3.2.22:
2072+
version "3.2.43"
2073+
resolved "https://registry.yarnpkg.com/google-libphonenumber/-/google-libphonenumber-3.2.43.tgz#c1e5107ab9c6e3848dc2108e380bde08da80931c"
2074+
integrity sha512-TbIX/UC3BFRJwCxbBeCPwuRC4Qws9Jz/CECmfTM1t9RFoI3X6eRThurv6AYr9wSrt640IA9KFIHuAD/vlyjqRw==
2075+
20612076
gopd@^1.0.1, gopd@^1.2.0:
20622077
version "1.2.0"
20632078
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
@@ -2166,6 +2181,13 @@ human-signals@^1.1.1:
21662181
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
21672182
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
21682183

2184+
i18n-iso-countries@^7.14.0:
2185+
version "7.14.0"
2186+
resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-7.14.0.tgz#cd5ae098198bce1cc40cadbf0a37ce6c8e9d0edf"
2187+
integrity sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==
2188+
dependencies:
2189+
diacritics "1.3.0"
2190+
21692191
ieee754@^1.1.13:
21702192
version "1.2.1"
21712193
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,14 @@
5353
"@emotion/styled": "^11.13.5",
5454
"@mui/icons-material": "^6.1.10",
5555
"@mui/material": "^6.1.10",
56+
"@neolution-ch/javascript-utils": "^2.2.0",
5657
"@uiw/react-color": "^2.4.1",
5758
"autosuggest-highlight": "^3.3.4",
5859
"classnames": "^2.3.2",
5960
"date-fns": "^4.1.0",
6061
"date-fns-tz": "^3.2.0",
62+
"flag-icons": "^7.5.0",
63+
"google-libphonenumber": "^3.2.43",
6164
"material-ui-popup-state": "^5.3.3",
6265
"react-datepicker": "^4.11.0",
6366
"react-number-format": "^5.1.4",
@@ -78,6 +81,7 @@
7881
"@storybook/addon-links": "^6.5.13",
7982
"@storybook/react": "^6.5.13",
8083
"@types/autosuggest-highlight": "^3.2.3",
84+
"@types/google-libphonenumber": "^7.4.30",
8185
"@types/node": "^18.16.3",
8286
"@types/react": "^18.2.5",
8387
"@types/react-datepicker": "^4.11.2",
@@ -90,6 +94,7 @@
9094
"eslint": "^9.18.0",
9195
"eslint-plugin-storybook": "^0.10.0",
9296
"gh-pages": "^5.0.0",
97+
"i18n-iso-countries": "^7.14.0",
9398
"nodemon": "^2.0.22",
9499
"prettier": "^3.4.2",
95100
"react": "^18.2.0",
@@ -108,6 +113,7 @@
108113
"yalc": "^1.0.0-pre.53"
109114
},
110115
"peerDependencies": {
116+
"i18n-iso-countries": "^7.14.0",
111117
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
112118
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0",
113119
"react-hook-form": "^7.0.0",

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from "./lib/Form";
22
export * from "./lib/Input";
33
export * from "./lib/FormattedInput";
44
export * from "./lib/StaticTypeaheadInput";
5+
export * from "./lib/TelephoneNumberInput";
56
export * from "./lib/AsyncTypeaheadInput";
67
export * from "./lib/types/Typeahead";
78
export * from "./lib/types/LabelValueOption";

src/lib/TelephoneNumberInput.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { FieldPathByValue, FieldValues } from "react-hook-form";
2+
import { useSafeNameId } from "src/lib/hooks/useSafeNameId";
3+
import { FormGroupLayout } from "./FormGroupLayout";
4+
import { CommonInputProps } from "./types/CommonInputProps";
5+
import { FocusEventHandler, ReactNode } from "react";
6+
import { TelephoneNumberInputInternal } from "./components/TelephoneNumberInput/TelephoneNumberInputInternal";
7+
import { RegionCode } from "google-libphonenumber";
8+
9+
interface TelephoneNumberInputProps<T extends FieldValues>
10+
extends Omit<CommonInputProps<T>, "minLength" | "maxLength" | "addonLeft" | "addonRight" | "name" | "onChange" | "onBlur"> {
11+
useBootstrapStyle?: boolean;
12+
name: FieldPathByValue<T, string | undefined>;
13+
onChange?: (telephoneNumber: string) => void;
14+
onBlur?: FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
15+
defaultCountry: RegionCode;
16+
placeholder?: string;
17+
renderAutocompleteField?: (children: ReactNode) => ReactNode;
18+
locale?: string;
19+
}
20+
21+
const TelephoneNumberInput = <T extends FieldValues>(props: TelephoneNumberInputProps<T>) => {
22+
const { label, helpText, inputGroupStyle, labelToolTip, hideValidationMessage = false, useBootstrapStyle } = props;
23+
const { name, id } = useSafeNameId(props.name, props.id);
24+
25+
return (
26+
<FormGroupLayout
27+
helpText={helpText}
28+
name={name}
29+
id={id}
30+
labelToolTip={labelToolTip}
31+
inputGroupStyle={inputGroupStyle}
32+
hideValidationMessage={hideValidationMessage}
33+
label={useBootstrapStyle ? label : undefined}
34+
labelStyle={useBootstrapStyle ? { color: "#8493A5", fontSize: 14 } : undefined}
35+
layout="muiInput"
36+
>
37+
<TelephoneNumberInputInternal {...props} name={name} id={id} />
38+
</FormGroupLayout>
39+
);
40+
};
41+
42+
export { TelephoneNumberInput, TelephoneNumberInputProps };
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { bindTrigger } from "material-ui-popup-state";
2+
import { PopupState } from "material-ui-popup-state/hooks";
3+
import { useMemo } from "react";
4+
import { Country } from "../../helpers/telephoneNumber";
5+
import InputAdornment from "@mui/material/InputAdornment";
6+
7+
interface TelephoneNumberInputAdornmentProps {
8+
popupState: PopupState;
9+
country: Country;
10+
disabled?: boolean;
11+
}
12+
13+
const TelephoneNumberInputAdornment = (props: TelephoneNumberInputAdornmentProps) => {
14+
const { popupState, disabled, country } = props;
15+
16+
const popoverMethods = useMemo(() => bindTrigger(popupState), [popupState]);
17+
return (
18+
<InputAdornment
19+
position="start"
20+
{...popoverMethods}
21+
style={{
22+
transition: "none",
23+
width: "40px",
24+
height: "50px",
25+
paddingRight: "65px",
26+
minWidth: 0,
27+
cursor: disabled ? "not-allowed" : "pointer",
28+
}}
29+
onClick={(e) => {
30+
if (disabled) {
31+
e.preventDefault();
32+
} else {
33+
popoverMethods.onClick(e);
34+
}
35+
}}
36+
>
37+
<div className="d-flex align-items-center">
38+
<span style={{ fontSize: "22px", marginRight: "5px" }} className={`fi fi-${country.region.toLowerCase()}`}></span>+{country.code}
39+
</div>
40+
</InputAdornment>
41+
);
42+
};
43+
44+
export { TelephoneNumberInputAdornment };

0 commit comments

Comments
 (0)