Skip to content

Commit

Permalink
Add antd 5.x support (GH-28)
Browse files Browse the repository at this point in the history
  • Loading branch information
ArtyomVancyan authored Mar 26, 2023
2 parents 47a6b96 + 77c6764 commit 89eaaec
Show file tree
Hide file tree
Showing 18 changed files with 1,450 additions and 238 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
name: Tests

on: push
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
tests:
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ package-lock.json
.idea/

# Build files
dist/
legacy/
index*

# Tarballs
*.tgz
38 changes: 25 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
# antd-phone-input

Advanced Phone Number Input for [Ant Design](https://github.com/ant-design/ant-design).

[![npm](https://img.shields.io/npm/v/antd-phone-input)](https://www.npmjs.com/package/antd-phone-input)
[![antd](https://img.shields.io/badge/antd-3.x%20%7C%204.x%20%7C%205.x-blue)](https://github.com/ant-design/ant-design)
[![types](https://img.shields.io/npm/types/antd-phone-input)](https://www.npmjs.com/package/antd-phone-input)
[![License](https://img.shields.io/npm/l/antd-phone-input)](https://github.com/ArtyomVancyan/antd-phone-input/blob/master/LICENSE)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://makeapullrequest.com)
[![Tests](https://github.com/ArtyomVancyan/antd-phone-input/actions/workflows/tests.yml/badge.svg)](https://github.com/ArtyomVancyan/antd-phone-input/actions/workflows/tests.yml)

Advanced phone input component for [Ant Design](https://github.com/ant-design/ant-design) that provides support for all
countries and is compatible with [`antd`](https://github.com/ant-design/ant-design) 3 and higher versions. It has
built-in support for area codes and provides validation to ensure that the entered numbers are valid. This open-source
project is designed to simplify the process of collecting phone numbers from users.

## Install

```shell
Expand All @@ -20,12 +24,17 @@ yarn add antd-phone-input

## Usage

### Antd 4.x
As mentioned above, this component is compatible with `3.x`, `4.x`, and after
the [v0.1.4](https://github.com/ArtyomVancyan/antd-phone-input/releases/tag/v0.1.4) release, `5.x` versions of Ant
Design. The latest one does not require any additional actions for loading the styles as it
uses [`cssinjs`](https://github.com/ant-design/cssinjs) ecosystem.

### Antd 5.x

```javascript
import React from "react";
import PhoneInput from "antd-phone-input";
import FormItem from "antd/es/form/FormItem";
import PhoneInput from "antd-phone-input";

const Demo = () => {
return (
Expand All @@ -36,22 +45,25 @@ const Demo = () => {
}
```

![latest](https://user-images.githubusercontent.com/44609997/227775101-72b03e76-52bc-421d-8e75-a03c9d0d6d08.png)

### Antd 4.x and older

For `4.x` and older versions, you should use the `legacy` endpoint.

```javascript
import PhoneInput from "antd-phone-input/legacy";
```

For including the styles, you should import them in the main `less` file after importing either
the `antd/dist/antd.less` or `antd/dist/antd.dark.less` styles.

```diff
@import "~antd/dist/antd";
+ @import "~antd-phone-input/dist/style";
+ @import "~antd-phone-input/legacy/style";
```

![light-dark-screenshots](https://user-images.githubusercontent.com/44609997/222975662-a2726b5f-954f-4a93-ac28-0339b432fa72.png)

### Antd 5.x

```ascii
v5.x does not have support yet
this issue is covered in GH-20
```
![legacy](https://user-images.githubusercontent.com/44609997/227775155-9e22bc63-2148-4714-ba8a-9bb4e44c0128.png)

## Value

Expand Down
2 changes: 1 addition & 1 deletion jestconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"transform": {
"^.+\\.(t|j)sx?$": "ts-jest"
},
"testRegex": "(/tests/.*|(\\.|/)(test|spec))\\.([jt]sx?)$",
"testRegex": "/tests/.*\\.test\\.([jt]sx?)$",
"moduleNameMapper": {
"^.+\\.((?:c|le|s[ca])ss)$": "identity-obj-proxy"
},
Expand Down
40 changes: 31 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.1.3",
"version": "0.1.4",
"name": "antd-phone-input",
"description": "Advanced Phone Number Input for Ant Design",
"keywords": [
Expand All @@ -23,16 +23,36 @@
"type": "git",
"url": "https://github.com/ArtyomVancyan/antd-phone-input"
},
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./index.esm.js",
"require": "./index.cjs.js",
"types": {
"default": "./index.d.ts"
}
},
"./legacy": {
"import": "./legacy/index.esm.js",
"require": "./legacy/index.cjs.js",
"types": {
"default": "./legacy/index.d.ts"
}
},
"./legacy/style": {
"default": "./legacy/style.less"
},
"./package.json": "./package.json"
},
"files": [
"dist",
"LICENSE"
"index*",
"legacy",
"LICENSE",
"README.md"
],
"scripts": {
"build": "rm -rf dist && npm run build:cjs && cp src/*.less dist/",
"build:cjs": "rollup -c --configPlugin @rollup/plugin-typescript",
"build": "npm run build:clean && npm run build:rollup && cp src/legacy/*.less legacy",
"build:rollup": "rollup -c --configPlugin @rollup/plugin-typescript",
"build:clean": "rm -r legacy index* || true",
"test": "jest --config jestconfig.json"
},
"license": "MIT",
Expand All @@ -41,14 +61,16 @@
"react": ">=16"
},
"devDependencies": {
"@rollup/plugin-alias": "^4.0.3",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-typescript": "^11.0.0",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.4.0",
"@types/node": "^18.14.1",
"@types/react": "^18.0.28",
"antd": "^4.24.8",
"antd": "npm:antd@^5.3.2",
"antd4": "npm:antd@^4.24.8",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.4.3",
"jest-environment-jsdom": "^29.4.3",
Expand Down
26 changes: 17 additions & 9 deletions rollup.config.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
import dts from "rollup-plugin-dts";
import json from "@rollup/plugin-json";
import alias from "@rollup/plugin-alias";
import postcss from "rollup-plugin-postcss";
import typescript from "@rollup/plugin-typescript";
import {readFileSync} from "fs";

const pkg = JSON.parse(readFileSync("./package.json") as unknown as string);

const input = "src/index.tsx";
const cjsOutput = {file: pkg.main, format: "cjs", exports: "auto"};
const esmOutput = {file: pkg.module, format: "es"};
const dtsOutput = {file: pkg.types, format: "es"};
const input4 = "src/legacy/index.tsx";
const input5 = "src/index.tsx";
const cjsInput4 = {file: "legacy/index.cjs.js", format: "cjs", exports: "auto"};
const esmInput4 = {file: "legacy/index.esm.js", format: "es"};
const dtsInput4 = {file: "legacy/index.d.ts", format: "es"};
const cjsInput5 = {file: "index.cjs.js", format: "cjs", exports: "auto"};
const esmInput5 = {file: "index.esm.js", format: "es"};
const dtsInput5 = {file: "index.d.ts", format: "es"};

const jsonPlugin = json();
const cssPlugin = postcss();
const tsPlugin = typescript();
const aliasPlugin = alias({entries: {"antd/lib": "antd/es"}});

const external = [
...Object.keys({...pkg.dependencies, ...pkg.peerDependencies}),
/^react($|\/)/,
/^antd($|\/)/,
/\.css$/,
/^antd($|\/es\/)/,
];

export default [
{input, output: cjsOutput, plugins: [tsPlugin, jsonPlugin, cssPlugin], external},
{input, output: esmOutput, plugins: [tsPlugin, jsonPlugin, cssPlugin], external},
{input, output: dtsOutput, plugins: [dts()], external: [/\.css$/]},
{input: input4, output: cjsInput4, plugins: [tsPlugin, jsonPlugin, cssPlugin], external},
{input: input4, output: esmInput4, plugins: [tsPlugin, jsonPlugin, cssPlugin], external},
{input: input4, output: dtsInput4, plugins: [dts()], external: [/\.css$/]},
{input: input5, output: cjsInput5, plugins: [tsPlugin, jsonPlugin, cssPlugin, aliasPlugin], external},
{input: input5, output: esmInput5, plugins: [tsPlugin, jsonPlugin, cssPlugin, aliasPlugin], external},
{input: input5, output: dtsInput5, plugins: [dts()], external: [/\.css$/]},
];
165 changes: 60 additions & 105 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,116 +1,71 @@
import {useMemo, useState} from "react";
import ReactPhoneInput from "react-phone-input-2";
import {useContext, useEffect, useMemo} from "react";
import theme from "antd/lib/theme";
import genComponentStyleHook from "antd/lib/input/style";
import {FormItemInputContext} from "antd/lib/form/context";
import {getStatusClassNames} from "antd/lib/_util/statusUtils";

import {ParsePhoneNumber, PhoneInputProps, ReactPhoneOnChange, ReactPhoneOnMount} from "./types";
import InputLegacy from "./legacy";
import {PhoneInputProps} from "./types";

import masks from "./phoneMasks.json";
import timezones from "./timezones.json";
import validations from "./validations.json";

import "react-phone-input-2/lib/style.css";

type ISO2Code = keyof typeof masks;
type Timezone = keyof typeof timezones;

const getDefaultISO2Code = () => {
/** Returns the default ISO2 code based on the user's timezone */
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone as Timezone;
return (timezones[timezone] || "").toLowerCase() || "us";
}

const parsePhoneNumber: ParsePhoneNumber = (value, data, formattedNumber) => {
const isoCode = data?.countryCode;
const countryCodePattern = /\+\d+/;
const areaCodePattern = /\((\d+)\)/;

/** Parses the matching partials of the phone number by predefined regex patterns */
const countryCodeMatch = formattedNumber ? (formattedNumber.match(countryCodePattern) || []) : [];
const areaCodeMatch = formattedNumber ? (formattedNumber.match(areaCodePattern) || []) : [];

/** Converts the parsed values of the country and area codes to integers if values present */
const countryCode = countryCodeMatch.length > 0 ? parseInt(countryCodeMatch[0]) : null;
const areaCode = areaCodeMatch.length > 1 ? parseInt(areaCodeMatch[1]) : null;

/** Parses the phone number by removing the country and area codes from the formatted value */
const phoneNumberPattern = new RegExp(`^${countryCode}${(areaCode || "")}(\\d+)`);
const phoneNumberMatch = value ? (value.match(phoneNumberPattern) || []) : [];
const phoneNumber = phoneNumberMatch.length > 1 ? phoneNumberMatch[1] : null;

/** Checks if both the area code and phone number length satisfy the validation rules */
const rules = validations[isoCode as ISO2Code] || {areaCode: [], phoneNumber: []};
const valid = [
rules.areaCode.includes((areaCode || "").toString().length),
rules.phoneNumber.includes((phoneNumber || "").toString().length),
].every(Boolean);

return {countryCode, areaCode, phoneNumber, isoCode, valid};
}

const PhoneInput = ({
value,
style,
country,
className,
size = "middle",
onPressEnter = () => null,
onMount: handleMount = () => null,
onChange: handleChange = () => null,
...reactPhoneInputProps
}: PhoneInputProps) => {
const [currentCode, setCurrentCode] = useState("");

const countryCode = useMemo(() => country || getDefaultISO2Code(), [country]);

const rawPhone = useMemo(() => {
const {countryCode, areaCode, phoneNumber} = {...value};
return [countryCode, areaCode, phoneNumber].map(v => v || "").join("");
}, [value]);
const PhoneInput = (inputLegacyProps: PhoneInputProps) => {
const {token} = theme.useToken();
const {status}: any = useContext(FormItemInputContext);
const [_1, inputCls] = genComponentStyleHook("ant-input");
const [_2, dropdownCls] = genComponentStyleHook("ant-dropdown");

const inputClass = useMemo(() => {
const suffix = {small: "sm", middle: "", large: "lg"}[size];
return "ant-input" + (suffix ? " ant-input-" + suffix : "");
}, [size]);

const onChange: ReactPhoneOnChange = (value, data, event, formattedNumber) => {
const metadata = parsePhoneNumber(value, data, formattedNumber);
const code = metadata.isoCode as ISO2Code;

if (code !== currentCode) {
/** Clears phone number when the country is selected manually */
handleChange({...metadata, areaCode: null, phoneNumber: null}, event);
setCurrentCode(code);
return;
return `${inputCls} ` + getStatusClassNames("ant-input", status);
}, [inputCls, status]);

const dropdownClass = useMemo(() => "ant-dropdown " + dropdownCls, [dropdownCls]);

useEffect(() => {
/** Load antd 5.x styles dynamically observing the theme change */
for (let styleSheet of document.styleSheets) {
let rule: any;
for (rule of styleSheet.cssRules || styleSheet.rules) {
if (rule.selectorText === ".react-tel-input .country-list") {
rule.style.boxShadow = token.boxShadow;
rule.style.backgroundColor = token.colorBgElevated;
}
if (rule.selectorText === ".react-tel-input .selected-flag") {
rule.style.borderColor = token.colorBorder;
}
if (rule.selectorText === ".react-tel-input .country-list .search") {
rule.style.backgroundColor = token.colorBgElevated;
}
if (rule.selectorText === ".react-tel-input .country-list .country") {
rule.style.borderRadius = token.borderRadiusOuter + "px";
}
if (rule.selectorText === ".react-tel-input .country-list .country-name") {
rule.style.color = token.colorText;
}
if (rule.selectorText === ".react-tel-input .country-list .country .dial-code") {
rule.style.color = token.colorTextDescription;
}
if (rule.selectorText === ".react-tel-input .country-list .country:hover") {
rule.style.backgroundColor = token.colorBgTextHover;
}
if (rule.selectorText === ".react-tel-input .country-list .country.highlight") {
rule.style.backgroundColor = token.colorPrimaryBg;
}
if (rule.selectorText === `:where(.${inputCls}).ant-input`) {
rule.selectorText += "\n,.react-tel-input .country-list .search-box";
rule.style.backgroundColor = token.colorBgElevated;
}
if (rule.selectorText === `:where(.${inputCls}).ant-input:hover`) {
rule.selectorText += "\n,.react-tel-input .country-list .search-box:focus";
rule.selectorText += "\n,.react-tel-input .country-list .search-box:hover";
}
}
}

handleChange(metadata, event);
}

const onMount: ReactPhoneOnMount = (rawValue, {countryCode, ...event}, formattedNumber) => {
const metadata = parsePhoneNumber(rawValue, {countryCode}, formattedNumber);
/** Initiates the current country code with the code of initial value */
setCurrentCode(metadata.isoCode as ISO2Code);
/** Initializes the existing value */
handleChange(metadata, event);
handleMount(metadata);
}
}, [inputCls, token])

return (
<ReactPhoneInput
/** Static properties for stable functionality */
masks={masks}
value={rawPhone}
enableAreaCodes
disableSearchIcon
/** Static properties providing dynamic behavior */
onMount={onMount}
onChange={onChange}
country={countryCode}
<InputLegacy
{...inputLegacyProps}
inputClass={inputClass}
/** Dynamic properties for customization */
{...reactPhoneInputProps}
containerStyle={style}
containerClass={className}
onEnterKeyPress={onPressEnter}
dropdownClass={dropdownClass}
/>
)
}
Expand Down
Loading

0 comments on commit 89eaaec

Please sign in to comment.