From d06aa24ab5b0a41d90b15346958c896854c61636 Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Thu, 21 Dec 2023 21:03:13 +0400 Subject: [PATCH 1/3] GH-65: Trigger `onChange` on country change Signed-off-by: Artyom Vancyan --- src/index.tsx | 497 ++++++++++++++++++++++++++------------------------ 1 file changed, 259 insertions(+), 238 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index b04a3f7..1771476 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,14 +1,14 @@ import { - ChangeEvent, - ForwardedRef, - forwardRef, - KeyboardEvent, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState + ChangeEvent, + ForwardedRef, + forwardRef, + KeyboardEvent, MutableRefObject, RefCallback, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState } from "react"; import useFormInstance from "antd/lib/form/hooks/useFormInstance"; import {FormContext} from "antd/lib/form/context"; @@ -26,260 +26,281 @@ styleInject("styles.css"); const slots = new Set("."); +type MutableRefList = Array | MutableRefObject | undefined | null>; + +function mergeRefs(...refs: MutableRefList): RefCallback { + return (val: T) => setRef(val, ...refs); +} + +function setRef(val: T, ...refs: MutableRefList): void { + return refs.forEach((ref) => { + if (typeof ref === "function") ref(val); + else if (ref != null) ref.current = val; + }) +} + const getMetadata = (rawValue: string, countriesList: typeof countries = countries, country: any = null) => { - country = country == null && rawValue.startsWith("44") ? "gb" : country; - if (country != null) { - countriesList = countriesList.filter((c) => c[0] === country); - countriesList = countriesList.sort((a, b) => b[2].length - a[2].length); - } - return countriesList.find((c) => rawValue.startsWith(c[2])); + country = country == null && rawValue.startsWith("44") ? "gb" : country; + if (country != null) { + countriesList = countriesList.filter((c) => c[0] === country); + countriesList = countriesList.sort((a, b) => b[2].length - a[2].length); + } + return countriesList.find((c) => rawValue.startsWith(c[2])); } const getRawValue = (value: PhoneNumber | string) => { - if (typeof value === "string") return value.replaceAll(/\D/g, ""); - return [value?.countryCode, value?.areaCode, value?.phoneNumber].filter(Boolean).join(""); + if (typeof value === "string") return value.replaceAll(/\D/g, ""); + return [value?.countryCode, value?.areaCode, value?.phoneNumber].filter(Boolean).join(""); } const displayFormat = (value: string) => { - return value.replace(/[.\s\D]+$/, "").replace(/(\(\d+)$/, "$1)"); + return value.replace(/[.\s\D]+$/, "").replace(/(\(\d+)$/, "$1)"); } const cleanInput = (input: any, pattern: string) => { - input = input.match(/\d/g) || []; - return Array.from(pattern, c => input[0] === c || slots.has(c) ? input.shift() || c : c); + input = input.match(/\d/g) || []; + return Array.from(pattern, c => input[0] === c || slots.has(c) ? input.shift() || c : c); } const checkValidity = (metadata: PhoneNumber, strict: boolean = false) => { - /** Checks if both the area code and phone number match the validation pattern */ - const pattern = (validations as any)[metadata.isoCode as keyof typeof validations][Number(strict)]; - return new RegExp(pattern).test([metadata.areaCode, metadata.phoneNumber].filter(Boolean).join("")); + /** Checks if both the area code and phone number match the validation pattern */ + const pattern = (validations as any)[metadata.isoCode as keyof typeof validations][Number(strict)]; + return new RegExp(pattern).test([metadata.areaCode, metadata.phoneNumber].filter(Boolean).join("")); } const getDefaultISO2Code = () => { - /** Returns the default ISO2 code, based on the user's timezone */ - return (timezones[Intl.DateTimeFormat().resolvedOptions().timeZone as keyof typeof timezones] || "") || "us"; + /** Returns the default ISO2 code, based on the user's timezone */ + return (timezones[Intl.DateTimeFormat().resolvedOptions().timeZone as keyof typeof timezones] || "") || "us"; } const parsePhoneNumber = (formattedNumber: string, countriesList: typeof countries = countries, country: any = null): PhoneNumber => { - const value = getRawValue(formattedNumber); - const isoCode = getMetadata(value, countriesList, country)?.[0] || getDefaultISO2Code(); - const countryCodePattern = /\+\d+/; - const areaCodePattern = /\((\d+)\)/; + const value = getRawValue(formattedNumber); + const isoCode = getMetadata(value, countriesList, country)?.[0] || getDefaultISO2Code(); + 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) || []) : []; + /** 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 ? areaCodeMatch[1] : null; + /** 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 ? 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; + /** 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; - return {countryCode, areaCode, phoneNumber, isoCode}; + return {countryCode, areaCode, phoneNumber, isoCode}; } const PhoneInput = forwardRef(({ - value: initialValue = "", - country = getDefaultISO2Code(), - enableSearch = false, - disableDropdown = false, - onlyCountries = [], - excludeCountries = [], - preferredCountries = [], - searchNotFound = "No country found", - searchPlaceholder = "Search country", - onMount: handleMount = () => null, - onInput: handleInput = () => null, - onChange: handleChange = () => null, - onKeyDown: handleKeyDown = () => null, - ...antInputProps - }: PhoneInputProps, ref: ForwardedRef) => { - const defaultValue = getRawValue(initialValue); - const defaultMetadata = getMetadata(defaultValue) || countries.find(([iso]) => iso === country); - const defaultValueState = defaultValue || countries.find(([iso]) => iso === defaultMetadata?.[0])?.[2] as string; - - const formInstance = useFormInstance(); - const formContext = useContext(FormContext); - const backRef = useRef(false); - const initiatedRef = useRef(false); - const [query, setQuery] = useState(""); - const [value, setValue] = useState(defaultValueState); - const [minWidth, setMinWidth] = useState(0); - const [countryCode, setCountryCode] = useState(country); - - const countriesOnly = useMemo(() => { - const allowList = onlyCountries.length > 0 ? onlyCountries : countries.map(([iso]) => iso); - return countries.map(([iso]) => iso).filter((iso) => { - return allowList.includes(iso) && !excludeCountries.includes(iso); - }); - }, [onlyCountries, excludeCountries]) - - const countriesList = useMemo(() => { - const filteredCountries = countries.filter(([iso, name, _1, dial]) => { - return countriesOnly.includes(iso) && ( - name.toLowerCase().startsWith(query.toLowerCase()) || dial.includes(query) - ); - }); - return [ - ...filteredCountries.filter(([iso]) => preferredCountries.includes(iso)), - ...filteredCountries.filter(([iso]) => !preferredCountries.includes(iso)), - ]; - }, [countriesOnly, preferredCountries, query]) - - const metadata = useMemo(() => { - const calculatedMetadata = getMetadata(getRawValue(value), countriesList, countryCode); - if (countriesList.find(([iso]) => iso === calculatedMetadata?.[0] || iso === defaultMetadata?.[0])) { - return calculatedMetadata || defaultMetadata; - } - return countriesList[0]; - }, [countriesList, countryCode, defaultMetadata, value]) - - const pattern = useMemo(() => { - return metadata?.[3] || defaultMetadata?.[3] || ""; - }, [defaultMetadata, metadata]) - - const clean = useCallback((input: any) => { - return cleanInput(input, pattern.replaceAll(/\d/g, ".")); - }, [pattern]) - - const first = useMemo(() => { - return [...pattern].findIndex(c => slots.has(c)); - }, [pattern]) - - const prev = useMemo((j = 0) => { - return Array.from(pattern.replaceAll(/\d/g, "."), (c, i) => { - return slots.has(c) ? j = i + 1 : j; - }); - }, [pattern]) - - const selectValue = useMemo(() => { - let metadata = getMetadata(getRawValue(value), countriesList); - metadata = metadata || countries.find(([iso]) => iso === countryCode); - return ({...metadata})?.[0] + ({...metadata})?.[2]; - }, [countriesList, countryCode, value]) - - const setFieldValue = useCallback((value: PhoneNumber) => { - if (formInstance) { - let namePath = []; - let formName = (formContext as any)?.name || ""; - let fieldName = (antInputProps as any)?.id || ""; - if (formName) { - namePath.push(formName); - fieldName = fieldName.slice(formName.length + 1); - } - formInstance.setFieldValue(namePath.concat(fieldName.split("_")), value); - } - }, [antInputProps, formContext, formInstance]) - - const format = useCallback(({target}: ChangeEvent) => { - const [i, j] = [target.selectionStart, target.selectionEnd].map((i: any) => { - i = clean(target.value.slice(0, i)).findIndex(c => slots.has(c)); - return i < 0 ? prev[prev.length - 1] : backRef.current ? prev[i - 1] || first : i; - }); - target.value = displayFormat(clean(target.value).join("")); - target.setSelectionRange(i, j); - backRef.current = false; - setValue(target.value); - }, [clean, first, prev]) - - const onKeyDown = useCallback((event: KeyboardEvent) => { - backRef.current = event.key === "Backspace"; - handleKeyDown(event); - }, [handleKeyDown]) - - const onChange = useCallback((event: ChangeEvent) => { - const formattedNumber = displayFormat(clean(event.target.value).join("")); - const phoneMetadata = parsePhoneNumber(formattedNumber, countriesList); - handleChange({...phoneMetadata, valid: (strict: boolean) => checkValidity(phoneMetadata, strict)}, event); - }, [clean, countriesList, handleChange]) - - const onInput = useCallback((event: ChangeEvent) => { - handleInput(event); - format(event); - }, [format, handleInput]) - - const onMount = useCallback((value: PhoneNumber) => { - setFieldValue(value); - handleMount(value); - }, [handleMount, setFieldValue]) - - useEffect(() => { - if (initiatedRef.current) return; - initiatedRef.current = true; - let initialValue = getRawValue(value); - if (!initialValue.startsWith(metadata?.[2] as string)) { - initialValue = metadata?.[2] as string; - } - const formattedNumber = displayFormat(clean(initialValue).join("")); - const phoneMetadata = parsePhoneNumber(formattedNumber, countriesList); - onMount({...phoneMetadata, valid: (strict: boolean) => checkValidity(phoneMetadata, strict)}); - setCountryCode(phoneMetadata.isoCode as keyof typeof validations); - setValue(formattedNumber); - }, [clean, countriesList, metadata, onMount, value]) - - const countriesSelect = useMemo(() => ( - setQuery(target.value)} - /> - )} - {menu} - - )} - > - {countriesList.map(([iso, name, dial, mask]) => ( - } - children={
-
- {name} {displayFormat(mask)} -
} - /> - ))} - - ), [selectValue, disableDropdown, minWidth, searchNotFound, countriesList, setFieldValue, enableSearch, searchPlaceholder]) - - return ( -
setMinWidth(node?.offsetWidth || 0)}> - -
- ) + value: initialValue = "", + country = getDefaultISO2Code(), + enableSearch = false, + disableDropdown = false, + onlyCountries = [], + excludeCountries = [], + preferredCountries = [], + searchNotFound = "No country found", + searchPlaceholder = "Search country", + onMount: handleMount = () => null, + onInput: handleInput = () => null, + onChange: handleChange = () => null, + onKeyDown: handleKeyDown = () => null, + ...antInputProps + }: PhoneInputProps, forwardedRef: ForwardedRef) => { + const defaultValue = getRawValue(initialValue); + const defaultMetadata = getMetadata(defaultValue) || countries.find(([iso]) => iso === country); + const defaultValueState = defaultValue || countries.find(([iso]) => iso === defaultMetadata?.[0])?.[2] as string; + + const formInstance = useFormInstance(); + const formContext = useContext(FormContext); + const inputRef = useRef(null); + const backRef = useRef(false); + const initiatedRef = useRef(false); + const [query, setQuery] = useState(""); + const [value, setValue] = useState(defaultValueState); + const [minWidth, setMinWidth] = useState(0); + const [countryCode, setCountryCode] = useState(country); + + const countriesOnly = useMemo(() => { + const allowList = onlyCountries.length > 0 ? onlyCountries : countries.map(([iso]) => iso); + return countries.map(([iso]) => iso).filter((iso) => { + return allowList.includes(iso) && !excludeCountries.includes(iso); + }); + }, [onlyCountries, excludeCountries]) + + const countriesList = useMemo(() => { + const filteredCountries = countries.filter(([iso, name, _1, dial]) => { + return countriesOnly.includes(iso) && ( + name.toLowerCase().startsWith(query.toLowerCase()) || dial.includes(query) + ); + }); + return [ + ...filteredCountries.filter(([iso]) => preferredCountries.includes(iso)), + ...filteredCountries.filter(([iso]) => !preferredCountries.includes(iso)), + ]; + }, [countriesOnly, preferredCountries, query]) + + const metadata = useMemo(() => { + const calculatedMetadata = getMetadata(getRawValue(value), countriesList, countryCode); + if (countriesList.find(([iso]) => iso === calculatedMetadata?.[0] || iso === defaultMetadata?.[0])) { + return calculatedMetadata || defaultMetadata; + } + return countriesList[0]; + }, [countriesList, countryCode, defaultMetadata, value]) + + const pattern = useMemo(() => { + return metadata?.[3] || defaultMetadata?.[3] || ""; + }, [defaultMetadata, metadata]) + + const clean = useCallback((input: any) => { + return cleanInput(input, pattern.replaceAll(/\d/g, ".")); + }, [pattern]) + + const first = useMemo(() => { + return [...pattern].findIndex(c => slots.has(c)); + }, [pattern]) + + const prev = useMemo((j = 0) => { + return Array.from(pattern.replaceAll(/\d/g, "."), (c, i) => { + return slots.has(c) ? j = i + 1 : j; + }); + }, [pattern]) + + const selectValue = useMemo(() => { + let metadata = getMetadata(getRawValue(value), countriesList); + metadata = metadata || countries.find(([iso]) => iso === countryCode); + return ({...metadata})?.[0] + ({...metadata})?.[2]; + }, [countriesList, countryCode, value]) + + const setFieldValue = useCallback((value: PhoneNumber) => { + if (formInstance) { + let namePath = []; + let formName = (formContext as any)?.name || ""; + let fieldName = (antInputProps as any)?.id || ""; + if (formName) { + namePath.push(formName); + fieldName = fieldName.slice(formName.length + 1); + } + formInstance.setFieldValue(namePath.concat(fieldName.split("_")), value); + } + }, [antInputProps, formContext, formInstance]) + + const format = useCallback(({target}: ChangeEvent) => { + const [i, j] = [target.selectionStart, target.selectionEnd].map((i: any) => { + i = clean(target.value.slice(0, i)).findIndex(c => slots.has(c)); + return i < 0 ? prev[prev.length - 1] : backRef.current ? prev[i - 1] || first : i; + }); + target.value = displayFormat(clean(target.value).join("")); + target.setSelectionRange(i, j); + backRef.current = false; + setValue(target.value); + }, [clean, first, prev]) + + // const ref = useCallback((node: any) => { + // if (forwardedRef) (forwardedRef as any)(node); + // if (inputRef.current) inputRef.current = node; + // }, [forwardedRef]) + + const onKeyDown = useCallback((event: KeyboardEvent) => { + backRef.current = event.key === "Backspace"; + handleKeyDown(event); + }, [handleKeyDown]) + + const onChange = useCallback((event: ChangeEvent) => { + const formattedNumber = displayFormat(clean(event.target.value).join("")); + const phoneMetadata = parsePhoneNumber(formattedNumber, countriesList); + handleChange({...phoneMetadata, valid: (strict: boolean) => checkValidity(phoneMetadata, strict)}, event); + }, [clean, countriesList, handleChange]) + + const onInput = useCallback((event: ChangeEvent) => { + handleInput(event); + format(event); + }, [format, handleInput]) + + const onMount = useCallback((value: PhoneNumber) => { + setFieldValue(value); + handleMount(value); + }, [handleMount, setFieldValue]) + + useEffect(() => { + if (initiatedRef.current) return; + initiatedRef.current = true; + let initialValue = getRawValue(value); + if (!initialValue.startsWith(metadata?.[2] as string)) { + initialValue = metadata?.[2] as string; + } + const formattedNumber = displayFormat(clean(initialValue).join("")); + const phoneMetadata = parsePhoneNumber(formattedNumber, countriesList); + onMount({...phoneMetadata, valid: (strict: boolean) => checkValidity(phoneMetadata, strict)}); + setCountryCode(phoneMetadata.isoCode as keyof typeof validations); + setValue(formattedNumber); + }, [clean, countriesList, metadata, onMount, value]) + + const countriesSelect = useMemo(() => ( + setQuery(target.value)} + /> + )} + {menu} +
+ )} + > + {countriesList.map(([iso, name, dial, mask]) => ( + } + children={
+
+ {name} {displayFormat(mask)} +
} + /> + ))} + + ), [selectValue, disableDropdown, minWidth, searchNotFound, countriesList, setFieldValue, handleChange, enableSearch, searchPlaceholder]) + + return ( +
setMinWidth(node?.offsetWidth || 0)}> + +
+ ) }) export default PhoneInput; From 9e2787908abec69efdb5e2680b51c0b1da9ba8ba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 22 Dec 2023 11:20:48 +0400 Subject: [PATCH 2/3] Pre-commit: reformat and fix code indentations Signed-off-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- examples/antd4.x/public/index.html | 6 +- examples/antd4.x/src/Demo.tsx | 72 +++--- examples/antd4.x/src/index.tsx | 2 +- examples/antd5.x/public/index.html | 6 +- examples/antd5.x/src/Demo.tsx | 84 +++---- scripts/prepare-styles.ts | 14 +- src/styles.ts | 18 +- src/types.ts | 38 +-- tests/antd.test.tsx | 364 ++++++++++++++--------------- 9 files changed, 302 insertions(+), 302 deletions(-) diff --git a/examples/antd4.x/public/index.html b/examples/antd4.x/public/index.html index 291a30c..e29550a 100644 --- a/examples/antd4.x/public/index.html +++ b/examples/antd4.x/public/index.html @@ -1,9 +1,9 @@ - - - Antd 4.x + + + Antd 4.x
diff --git a/examples/antd4.x/src/Demo.tsx b/examples/antd4.x/src/Demo.tsx index 5a37c01..ae56c8d 100644 --- a/examples/antd4.x/src/Demo.tsx +++ b/examples/antd4.x/src/Demo.tsx @@ -5,48 +5,48 @@ import FormItem from "antd/es/form/FormItem"; import PhoneInput from "antd-phone-input"; const Demo = () => { - const [value, setValue] = useState(null); + const [value, setValue] = useState(null); - const validator = (_: any, {valid}: any) => { - if (valid()) { - return Promise.resolve(); - } - return Promise.reject("Invalid phone number"); - } + const validator = (_: any, {valid}: any) => { + if (valid()) { + return Promise.resolve(); + } + return Promise.reject("Invalid phone number"); + } - const changeTheme = () => { - if (window.location.pathname === "/dark") { - window.location.replace("/"); - } else { - window.location.replace("/dark"); - } - } + const changeTheme = () => { + if (window.location.pathname === "/dark") { + window.location.replace("/"); + } else { + window.location.replace("/dark"); + } + } - const handleFinish = ({phone}: any) => setValue(phone); + const handleFinish = ({phone}: any) => setValue(phone); - return ( -
- {value && ( -
+    return (
+        
+ {value && ( +
                     {JSON.stringify(value, null, 2)}
                 
- )} -
- - - -
- - - -
-
-
- ) + )} +
+ + + +
+ + + +
+
+
+ ) } export default Demo; diff --git a/examples/antd4.x/src/index.tsx b/examples/antd4.x/src/index.tsx index 570abf1..1b5f0cc 100644 --- a/examples/antd4.x/src/index.tsx +++ b/examples/antd4.x/src/index.tsx @@ -5,7 +5,7 @@ const Light = lazy(() => import("./themes/Light")); const Dark = lazy(() => import("./themes/Dark")); const App = () => { - return window.location.pathname === "/dark" ? : ; + return window.location.pathname === "/dark" ? : ; } const elem = document.getElementById("root"); diff --git a/examples/antd5.x/public/index.html b/examples/antd5.x/public/index.html index 50f2796..8a4f5e5 100644 --- a/examples/antd5.x/public/index.html +++ b/examples/antd5.x/public/index.html @@ -1,9 +1,9 @@ - - - Antd 5.x + + + Antd 5.x
diff --git a/examples/antd5.x/src/Demo.tsx b/examples/antd5.x/src/Demo.tsx index 19a7e71..7795180 100644 --- a/examples/antd5.x/src/Demo.tsx +++ b/examples/antd5.x/src/Demo.tsx @@ -10,54 +10,54 @@ import PhoneInput from "antd-phone-input"; import "antd/dist/reset.css"; const Demo = () => { - const [value, setValue] = useState(null); - const [algorithm, setAlgorithm] = useState("defaultAlgorithm"); + const [value, setValue] = useState(null); + const [algorithm, setAlgorithm] = useState("defaultAlgorithm"); - const validator = (_: any, {valid}: any) => { - if (valid()) { - return Promise.resolve(); - } - return Promise.reject("Invalid phone number"); - } + const validator = (_: any, {valid}: any) => { + if (valid()) { + return Promise.resolve(); + } + return Promise.reject("Invalid phone number"); + } - const changeTheme = () => { - if (algorithm === "defaultAlgorithm") { - setAlgorithm("darkAlgorithm"); - } else { - setAlgorithm("defaultAlgorithm"); - } - } + const changeTheme = () => { + if (algorithm === "defaultAlgorithm") { + setAlgorithm("darkAlgorithm"); + } else { + setAlgorithm("defaultAlgorithm"); + } + } - const handleFinish = ({phone}: any) => setValue(phone); + const handleFinish = ({phone}: any) => setValue(phone); - return ( - - -
- {value && ( -
+    return (
+        
+            
+                
+ {value && ( +
                             {JSON.stringify(value, null, 2)}
                         
- )} -
- - - -
- - - -
-
-
-
-
- ) + )} +
+ + + +
+ + + +
+
+
+
+
+ ) } export default Demo; diff --git a/scripts/prepare-styles.ts b/scripts/prepare-styles.ts index c6301cb..8ec1cd4 100644 --- a/scripts/prepare-styles.ts +++ b/scripts/prepare-styles.ts @@ -6,12 +6,12 @@ import stylesheet from "../resources/stylesheet.json"; const exec = (command: string) => util.promisify(process.exec)(command, {shell: "/bin/bash"}); (async () => { - let styles = Object.entries(stylesheet).map(([selector, rules]) => { - return `${selector} {` + Object.entries(rules).map(([key, value]) => { - return `${key}: ${value}; `; - }).join("") + "} "; - }).join(""); + let styles = Object.entries(stylesheet).map(([selector, rules]) => { + return `${selector} {` + Object.entries(rules).map(([key, value]) => { + return `${key}: ${value}; `; + }).join("") + "} "; + }).join(""); - await exec(`ls *.js | xargs -I {} sed -i 's/styles.css/${styles}/g' {}`); - await exec("ls *.{j,t}s | xargs -I {} sed -i 's/antd\\/lib/antd\\/es/g' {}"); + await exec(`ls *.js | xargs -I {} sed -i 's/styles.css/${styles}/g' {}`); + await exec("ls *.{j,t}s | xargs -I {} sed -i 's/antd\\/lib/antd\\/es/g' {}"); })(); diff --git a/src/styles.ts b/src/styles.ts index 25f1ba6..740a95e 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -1,13 +1,13 @@ export default (cssText: string) => { - /** Inject the given `cssText` in the document head */ - const style = document.createElement("style"); - style.setAttribute("type", "text/css"); + /** Inject the given `cssText` in the document head */ + const style = document.createElement("style"); + style.setAttribute("type", "text/css"); - if ((style as any).styleSheet) { - (style as any).styleSheet.cssText = cssText; - } else { - style.appendChild(document.createTextNode(cssText)); - } + if ((style as any).styleSheet) { + (style as any).styleSheet.cssText = cssText; + } else { + style.appendChild(document.createTextNode(cssText)); + } - document.head.appendChild(style); + document.head.appendChild(style); } diff --git a/src/types.ts b/src/types.ts index d51ba4a..6db2c5d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,39 +2,39 @@ import {ChangeEvent, KeyboardEvent} from "react"; import {InputProps} from "antd/lib/input"; export interface PhoneNumber { - countryCode?: number | null; - areaCode?: string | null; - phoneNumber?: string | null; - isoCode?: string; + countryCode?: number | null; + areaCode?: string | null; + phoneNumber?: string | null; + isoCode?: string; - valid?(strict?: boolean): boolean; + valid?(strict?: boolean): boolean; } export interface PhoneInputProps extends Omit { - value?: PhoneNumber | string; + value?: PhoneNumber | string; - country?: string; + country?: string; - enableSearch?: boolean; + enableSearch?: boolean; - searchNotFound?: string; + searchNotFound?: string; - searchPlaceholder?: string; + searchPlaceholder?: string; - disableDropdown?: boolean; + disableDropdown?: boolean; - onlyCountries?: string[]; + onlyCountries?: string[]; - excludeCountries?: string[]; + excludeCountries?: string[]; - preferredCountries?: string[]; + preferredCountries?: string[]; - onMount?(value: PhoneNumber): void; + onMount?(value: PhoneNumber): void; - onInput?(event: ChangeEvent): void; + onInput?(event: ChangeEvent): void; - onKeyDown?(event: KeyboardEvent): void; + onKeyDown?(event: KeyboardEvent): void; - /** NOTE: This differs from the antd Input onChange interface */ - onChange?(value: PhoneNumber, event: ChangeEvent): void; + /** NOTE: This differs from the antd Input onChange interface */ + onChange?(value: PhoneNumber, event: ChangeEvent): void; } diff --git a/tests/antd.test.tsx b/tests/antd.test.tsx index ad0a605..cdc584e 100644 --- a/tests/antd.test.tsx +++ b/tests/antd.test.tsx @@ -8,195 +8,195 @@ import {act, render, screen} from "@testing-library/react"; import PhoneInput from "../src"; Object.defineProperty(console, "warn", { - value: jest.fn(), + value: jest.fn(), }) Object.defineProperty(window, "matchMedia", { - value: jest.fn().mockImplementation((): any => ({ - addListener: jest.fn(), - removeListener: jest.fn(), - })), + value: jest.fn().mockImplementation((): any => ({ + addListener: jest.fn(), + removeListener: jest.fn(), + })), }) function inputHasError(parent: any = document) { - const inputGroup = parent.querySelector(".ant-input-group-wrapper"); - return inputGroup.className.includes("ant-input-group-wrapper-status-error"); + const inputGroup = parent.querySelector(".ant-input-group-wrapper"); + return inputGroup.className.includes("ant-input-group-wrapper-status-error"); } describe("Checking the basic rendering and functionality", () => { - it("Rendering without crashing", () => { - render(); - }) - - it("Rendering with strict raw value", () => { - render(); - assert(screen.getByDisplayValue("+1 (702) 123 4567")); - }) - - it("Rendering with an initial value", () => { - render( { - assert(value.countryCode === 1); - assert(value.areaCode === "702"); - assert(value.phoneNumber === "1234567"); - assert(value.isoCode === "us"); - assert(value.valid()); - }} - value={{countryCode: 1, areaCode: "702", phoneNumber: "1234567"}} - />); - assert(screen.getByDisplayValue("+1 (702) 123 4567")); - }) - - it("Rendering with a raw initial value", () => { - render(
- - - -
); - assert(screen.getByDisplayValue("+1 (702) 123 4567")); - }) - - it("Checking the component on user input", async () => { - render( { - assert(value.isoCode === "us"); - }} - country="us" - />); - const input = screen.getByDisplayValue("+1"); - await userEvent.type(input, "907123456789"); - assert(input.getAttribute("value") === "+1 (907) 123 4567"); - }) - - it("Using the input with FormItem", async () => { - render(
{ - assert(phone.countryCode === 1); - assert(phone.areaCode === "907"); - assert(phone.phoneNumber === "1234567"); - assert(phone.isoCode === "us"); - }}> - - - - -
); - const input = screen.getByDisplayValue("+1"); - await userEvent.type(input, "907123456789"); - assert(input.getAttribute("value") === "+1 (907) 123 4567"); - screen.getByTestId("button").click(); - }) - - it("Checking input validation with FormItem", async () => { - render(
- { - assert(valid()); - return Promise.resolve(); - } - }]}> - - - -
); - await userEvent.click(screen.getByTestId("button")); - }) - - it("Checking form with initial value", async () => { - render(
- - - - -
); - const input = screen.getByDisplayValue("+1 (702)"); - await userEvent.type(input, "1234567"); - assert(input.getAttribute("value") === "+1 (702) 123 4567"); - }) - - it("Checking validation with casual form actions", async () => { - render(
- { - if (valid()) return Promise.resolve(); - return Promise.reject("Invalid phone number"); - } - }]}> - - - - -
); - - const form = screen.getByTestId("form"); - const reset = screen.getByTestId("reset"); - const submit = screen.getByTestId("submit"); - - assert(!inputHasError(form)); // valid - await userEvent.click(reset); - assert(!inputHasError(form)); // valid - await userEvent.click(submit); - await act(async () => { - await new Promise(r => setTimeout(r, 100)); - }) - assert(inputHasError(form)); // invalid - await userEvent.click(reset); - assert(!inputHasError(form)); // valid - await userEvent.click(reset); - assert(!inputHasError(form)); // valid - await userEvent.click(submit); - await act(async () => { - await new Promise(r => setTimeout(r, 100)); - }) - assert(inputHasError(form)); // invalid - await userEvent.click(submit); - await act(async () => { - await new Promise(r => setTimeout(r, 100)); - }) - assert(inputHasError(form)); // invalid - }) - - it("Checking validation with casual inputs and actions", async () => { - render(
- { - if (valid()) return Promise.resolve(); - return Promise.reject("Invalid phone number"); - } - }]}> - - - - -
); - - const form = screen.getByTestId("form"); - const reset = screen.getByTestId("reset"); - const submit = screen.getByTestId("submit"); - const input = screen.getByDisplayValue("+1"); - - await userEvent.type(input, "90712345"); - await act(async () => { - await new Promise(r => setTimeout(r, 100)); - }) - assert(inputHasError(form)); // invalid - await userEvent.type(input, "6"); - await act(async () => { - await new Promise(r => setTimeout(r, 100)); - }) - assert(inputHasError(form)); // invalid - await userEvent.type(input, "7"); - await act(async () => { - await new Promise(r => setTimeout(r, 100)); - }) - assert(!inputHasError(form)); // valid - await userEvent.click(reset); - assert(!inputHasError(form)); // valid - await userEvent.click(submit); - await act(async () => { - await new Promise(r => setTimeout(r, 100)); - }) - assert(inputHasError(form)); // invalid - await userEvent.click(reset); - assert(!inputHasError(form)); // valid - }) + it("Rendering without crashing", () => { + render(); + }) + + it("Rendering with strict raw value", () => { + render(); + assert(screen.getByDisplayValue("+1 (702) 123 4567")); + }) + + it("Rendering with an initial value", () => { + render( { + assert(value.countryCode === 1); + assert(value.areaCode === "702"); + assert(value.phoneNumber === "1234567"); + assert(value.isoCode === "us"); + assert(value.valid()); + }} + value={{countryCode: 1, areaCode: "702", phoneNumber: "1234567"}} + />); + assert(screen.getByDisplayValue("+1 (702) 123 4567")); + }) + + it("Rendering with a raw initial value", () => { + render(
+ + + +
); + assert(screen.getByDisplayValue("+1 (702) 123 4567")); + }) + + it("Checking the component on user input", async () => { + render( { + assert(value.isoCode === "us"); + }} + country="us" + />); + const input = screen.getByDisplayValue("+1"); + await userEvent.type(input, "907123456789"); + assert(input.getAttribute("value") === "+1 (907) 123 4567"); + }) + + it("Using the input with FormItem", async () => { + render(
{ + assert(phone.countryCode === 1); + assert(phone.areaCode === "907"); + assert(phone.phoneNumber === "1234567"); + assert(phone.isoCode === "us"); + }}> + + + + +
); + const input = screen.getByDisplayValue("+1"); + await userEvent.type(input, "907123456789"); + assert(input.getAttribute("value") === "+1 (907) 123 4567"); + screen.getByTestId("button").click(); + }) + + it("Checking input validation with FormItem", async () => { + render(
+ { + assert(valid()); + return Promise.resolve(); + } + }]}> + + + +
); + await userEvent.click(screen.getByTestId("button")); + }) + + it("Checking form with initial value", async () => { + render(
+ + + + +
); + const input = screen.getByDisplayValue("+1 (702)"); + await userEvent.type(input, "1234567"); + assert(input.getAttribute("value") === "+1 (702) 123 4567"); + }) + + it("Checking validation with casual form actions", async () => { + render(
+ { + if (valid()) return Promise.resolve(); + return Promise.reject("Invalid phone number"); + } + }]}> + + + + +
); + + const form = screen.getByTestId("form"); + const reset = screen.getByTestId("reset"); + const submit = screen.getByTestId("submit"); + + assert(!inputHasError(form)); // valid + await userEvent.click(reset); + assert(!inputHasError(form)); // valid + await userEvent.click(submit); + await act(async () => { + await new Promise(r => setTimeout(r, 100)); + }) + assert(inputHasError(form)); // invalid + await userEvent.click(reset); + assert(!inputHasError(form)); // valid + await userEvent.click(reset); + assert(!inputHasError(form)); // valid + await userEvent.click(submit); + await act(async () => { + await new Promise(r => setTimeout(r, 100)); + }) + assert(inputHasError(form)); // invalid + await userEvent.click(submit); + await act(async () => { + await new Promise(r => setTimeout(r, 100)); + }) + assert(inputHasError(form)); // invalid + }) + + it("Checking validation with casual inputs and actions", async () => { + render(
+ { + if (valid()) return Promise.resolve(); + return Promise.reject("Invalid phone number"); + } + }]}> + + + + +
); + + const form = screen.getByTestId("form"); + const reset = screen.getByTestId("reset"); + const submit = screen.getByTestId("submit"); + const input = screen.getByDisplayValue("+1"); + + await userEvent.type(input, "90712345"); + await act(async () => { + await new Promise(r => setTimeout(r, 100)); + }) + assert(inputHasError(form)); // invalid + await userEvent.type(input, "6"); + await act(async () => { + await new Promise(r => setTimeout(r, 100)); + }) + assert(inputHasError(form)); // invalid + await userEvent.type(input, "7"); + await act(async () => { + await new Promise(r => setTimeout(r, 100)); + }) + assert(!inputHasError(form)); // valid + await userEvent.click(reset); + assert(!inputHasError(form)); // valid + await userEvent.click(submit); + await act(async () => { + await new Promise(r => setTimeout(r, 100)); + }) + assert(inputHasError(form)); // invalid + await userEvent.click(reset); + assert(!inputHasError(form)); // valid + }) }) From bc6686ba76b094d7bdbb94b300e8c226e4c1493a Mon Sep 17 00:00:00 2001 From: Artyom Vancyan Date: Fri, 22 Dec 2023 19:15:13 +0400 Subject: [PATCH 3/3] GH-65: Optimize the double-ref and emit the right event Signed-off-by: Artyom Vancyan --- src/index.tsx | 39 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 1771476..0520a19 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,7 +2,7 @@ import { ChangeEvent, ForwardedRef, forwardRef, - KeyboardEvent, MutableRefObject, RefCallback, + KeyboardEvent, useCallback, useContext, useEffect, @@ -26,19 +26,6 @@ styleInject("styles.css"); const slots = new Set("."); -type MutableRefList = Array | MutableRefObject | undefined | null>; - -function mergeRefs(...refs: MutableRefList): RefCallback { - return (val: T) => setRef(val, ...refs); -} - -function setRef(val: T, ...refs: MutableRefList): void { - return refs.forEach((ref) => { - if (typeof ref === "function") ref(val); - else if (ref != null) ref.current = val; - }) -} - const getMetadata = (rawValue: string, countriesList: typeof countries = countries, country: any = null) => { country = country == null && rawValue.startsWith("44") ? "gb" : country; if (country != null) { @@ -119,6 +106,7 @@ const PhoneInput = forwardRef(({ const formContext = useContext(FormContext); const inputRef = useRef(null); const backRef = useRef(false); + const selectedRef = useRef(false); const initiatedRef = useRef(false); const [query, setQuery] = useState(""); const [value, setValue] = useState(defaultValueState); @@ -200,10 +188,12 @@ const PhoneInput = forwardRef(({ setValue(target.value); }, [clean, first, prev]) - // const ref = useCallback((node: any) => { - // if (forwardedRef) (forwardedRef as any)(node); - // if (inputRef.current) inputRef.current = node; - // }, [forwardedRef]) + const ref = useCallback((node: any) => { + [forwardedRef, inputRef].forEach((ref) => { + if (typeof ref === "function") ref(node); + else if (ref != null) ref.current = node; + }) + }, [forwardedRef]) const onKeyDown = useCallback((event: KeyboardEvent) => { backRef.current = event.key === "Backspace"; @@ -211,7 +201,8 @@ const PhoneInput = forwardRef(({ }, [handleKeyDown]) const onChange = useCallback((event: ChangeEvent) => { - const formattedNumber = displayFormat(clean(event.target.value).join("")); + const formattedNumber = selectedRef.current ? event.target.value : displayFormat(clean(event.target.value).join("")); + selectedRef.current = false; const phoneMetadata = parsePhoneNumber(formattedNumber, countriesList); handleChange({...phoneMetadata, valid: (strict: boolean) => checkValidity(phoneMetadata, strict)}, event); }, [clean, countriesList, handleChange]) @@ -252,10 +243,12 @@ const PhoneInput = forwardRef(({ const formattedNumber = displayFormat(cleanInput(mask, mask).join("")); const phoneMetadata = parsePhoneNumber(formattedNumber, countriesList, selectedCountryCode); setFieldValue({...phoneMetadata, valid: (strict: boolean) => checkValidity(phoneMetadata, strict)}); - const event = {target: inputRef.current.input} as ChangeEvent; - handleChange({...phoneMetadata, valid: (strict: boolean) => checkValidity(phoneMetadata, strict)}, event); setCountryCode(selectedCountryCode); setValue(formattedNumber); + selectedRef.current = true; + const nativeInputValueSetter = (Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value") as any).set; + nativeInputValueSetter.call(inputRef.current.input, formattedNumber); + inputRef.current.input.dispatchEvent(new Event("change", {bubbles: true})); }} optionLabelProp="label" dropdownStyle={{minWidth}} @@ -284,13 +277,13 @@ const PhoneInput = forwardRef(({ /> ))} - ), [selectValue, disableDropdown, minWidth, searchNotFound, countriesList, setFieldValue, handleChange, enableSearch, searchPlaceholder]) + ), [selectValue, disableDropdown, minWidth, searchNotFound, countriesList, setFieldValue, enableSearch, searchPlaceholder]) return (
setMinWidth(node?.offsetWidth || 0)}>