Skip to content

Commit

Permalink
Implement option for disabling parentheses (GH-34)
Browse files Browse the repository at this point in the history
  • Loading branch information
ArtyomVancyan authored Jun 15, 2024
2 parents 37f2eb1 + bb7f3ec commit 2336d5e
Show file tree
Hide file tree
Showing 24 changed files with 335 additions and 140 deletions.
98 changes: 69 additions & 29 deletions development/src/ant-phone/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import {
ChangeEvent,
forwardRef,
Expand All @@ -10,7 +12,9 @@ import {
useState
} from "react";
import useFormInstance from "antd/es/form/hooks/useFormInstance";
import {ConfigContext} from "antd/es/config-provider";
import {FormContext} from "antd/es/form/context";
import {useWatch} from "antd/es/form/Form";
import Select from "antd/es/select";
import Input from "antd/es/input";

Expand All @@ -31,18 +35,19 @@ import {
import {injectMergedStyles} from "./styles";
import {PhoneInputProps, PhoneNumber} from "./types";

injectMergedStyles();

const PhoneInput = forwardRef(({
value: initialValue = "",
country = getDefaultISO2Code(),
disabled = false,
enableSearch = false,
disableDropdown = false,
disableParentheses = false,
onlyCountries = [],
excludeCountries = [],
preferredCountries = [],
searchNotFound = "No country found",
searchPlaceholder = "Search country",
dropdownRender = (node) => node,
onMount: handleMount = () => null,
onInput: handleInput = () => null,
onChange: handleChange = () => null,
Expand All @@ -51,13 +56,18 @@ const PhoneInput = forwardRef(({
}: PhoneInputProps, forwardedRef: any) => {
const formInstance = useFormInstance();
const formContext = useContext(FormContext);
const {getPrefixCls} = useContext(ConfigContext);
const inputRef = useRef<any>(null);
const searchRef = useRef<any>(null);
const selectedRef = useRef<boolean>(false);
const initiatedRef = useRef<boolean>(false);
const [query, setQuery] = useState<string>("");
const [minWidth, setMinWidth] = useState<number>(0);
const [countryCode, setCountryCode] = useState<string>(country);

const prefixCls = getPrefixCls();
injectMergedStyles(prefixCls);

const {
value,
pattern,
Expand All @@ -72,6 +82,7 @@ const PhoneInput = forwardRef(({
onlyCountries,
excludeCountries,
preferredCountries,
disableParentheses,
});

const {
Expand All @@ -85,18 +96,22 @@ const PhoneInput = forwardRef(({
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);
const namePath = useMemo(() => {
let path = [];
let formName = (formContext as any)?.name || "";
let fieldName = (antInputProps as any)?.id || "";
if (formName) {
path.push(formName);
fieldName = fieldName.slice(formName.length + 1);
}
}, [antInputProps, formContext, formInstance])
return path.concat(fieldName.split("_"));
}, [antInputProps, formContext])

const phoneValue = useWatch(namePath, formInstance);

const setFieldValue = useCallback((value: PhoneNumber) => {
if (formInstance) formInstance.setFieldValue(namePath, value);
}, [formInstance, namePath])

const onKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) => {
onKeyDownMaskHandler(event);
Expand All @@ -122,13 +137,29 @@ const PhoneInput = forwardRef(({
handleMount(value);
}, [handleMount, setFieldValue])

const onDropdownVisibleChange = useCallback((open: boolean) => {
if (open && enableSearch) setTimeout(() => searchRef.current.focus(), 100);
}, [enableSearch])

const ref = useCallback((node: any) => {
[forwardedRef, inputRef].forEach((ref) => {
if (typeof ref === "function") ref(node);
else if (ref != null) ref.current = node;
})
}, [forwardedRef])

useEffect(() => {
const rawValue = getRawValue(phoneValue);
const metadata = getMetadata(rawValue);
// Skip if value has not been updated by `setFieldValue`.
if (!metadata?.[3] || rawValue === getRawValue(value)) return;
const formattedNumber = getFormattedNumber(rawValue, metadata?.[3] as string);
const phoneMetadata = parsePhoneNumber(formattedNumber);
setFieldValue({...phoneMetadata, valid: (strict: boolean) => checkValidity(phoneMetadata, strict)});
setCountryCode(metadata?.[0] as string);
setValue(formattedNumber);
}, [phoneValue, value, setFieldValue, setValue])

useEffect(() => {
if (initiatedRef.current) return;
initiatedRef.current = true;
Expand All @@ -147,28 +178,33 @@ const PhoneInput = forwardRef(({
<Select
suffixIcon={null}
value={selectValue}
disabled={disabled}
open={disableDropdown ? false : undefined}
onSelect={(selectedOption, {key}) => {
const [_, mask] = key.split("_");
if (selectValue === selectedOption) return;
const selectedCountryCode = selectedOption.slice(0, 2);
const formattedNumber = displayFormat(cleanInput(mask, mask).join(""));
const phoneMetadata = parsePhoneNumber(formattedNumber, countriesList, selectedCountryCode);
setFieldValue({...phoneMetadata, valid: (strict: boolean) => checkValidity(phoneMetadata, strict)});
setCountryCode(selectedCountryCode);
setValue(formattedNumber);
setQuery("");
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}));
inputRef.current.input.focus();
}}
optionLabelProp="label"
dropdownStyle={{minWidth}}
notFoundContent={searchNotFound}
onDropdownVisibleChange={onDropdownVisibleChange}
dropdownRender={(menu) => (
<div className="ant-phone-input-search-wrapper">
<div className={`${prefixCls}-phone-input-search-wrapper`}>
{enableSearch && (
<Input
value={query}
ref={searchRef}
placeholder={searchPlaceholder}
onInput={({target}: any) => setQuery(target.value)}
/>
Expand All @@ -177,22 +213,25 @@ const PhoneInput = forwardRef(({
</div>
)}
>
{countriesList.map(([iso, name, dial, mask]) => (
<Select.Option
value={iso + dial}
key={`${iso}_${mask}`}
label={<div className={`flag ${iso}`}/>}
children={<div className="ant-phone-input-select-item">
<div className={`flag ${iso}`}/>
{name}&nbsp;{displayFormat(mask)}
</div>}
/>
))}
{countriesList.map(([iso, name, dial, pattern]) => {
const mask = disableParentheses ? pattern.replace(/[()]/g, "") : pattern;
return (
<Select.Option
value={iso + dial}
key={`${iso}_${mask}`}
label={<div className={`flag ${iso}`}/>}
children={<div className={`${prefixCls}-phone-input-select-item`}>
<div className={`flag ${iso}`}/>
{name}&nbsp;{displayFormat(mask)}
</div>}
/>
)
})}
</Select>
), [selectValue, disableDropdown, minWidth, searchNotFound, countriesList, setFieldValue, setValue, enableSearch, searchPlaceholder])
), [selectValue, query, disabled, disableParentheses, disableDropdown, onDropdownVisibleChange, minWidth, searchNotFound, countriesList, setFieldValue, setValue, prefixCls, enableSearch, searchPlaceholder])

return (
<div className="ant-phone-input-wrapper"
<div className={`${prefixCls}-phone-input-wrapper`}
ref={node => setMinWidth(node?.offsetWidth || 0)}>
<Input
ref={ref}
Expand All @@ -201,7 +240,8 @@ const PhoneInput = forwardRef(({
onInput={onInput}
onChange={onChange}
onKeyDown={onKeyDown}
addonBefore={countriesSelect}
addonBefore={dropdownRender(countriesSelect)}
disabled={disabled}
{...antInputProps}
/>
</div>
Expand Down
2 changes: 2 additions & 0 deletions development/src/ant-phone/resources/stylesheet.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"margin": "0 6px 6px 6px"
},
".ant-phone-input-wrapper .ant-select-selector": {
"padding": "0 11px !important",
"height": "unset !important",
"border": "none !important"
},
".ant-phone-input-wrapper .ant-select-selection-item": {
Expand Down
19 changes: 18 additions & 1 deletion development/src/ant-phone/styles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
"use client";

import {injectStyles, jsonToCss} from "react-phone-hooks/styles";
import commonStyles from "react-phone-hooks/stylesheet.json";
import {defaultPrefixCls} from "antd/es/config-provider";

import customStyles from "./resources/stylesheet.json";

export const injectMergedStyles = () => injectStyles(jsonToCss(Object.assign(commonStyles, customStyles)));
let prefix: any = null;

export const injectMergedStyles = (prefixCls: any = null) => {
const stylesheet = customStyles as { [key: string]: any };
if (prefixCls && prefixCls !== defaultPrefixCls) {
if (prefix === prefixCls) return;
Object.entries(stylesheet).forEach(([k, value]) => {
const key = k.replace(/ant(?=-)/g, prefixCls);
stylesheet[key] = value;
delete stylesheet[k];
})
prefix = prefixCls;
}
return injectStyles(jsonToCss(Object.assign(commonStyles, stylesheet)));
}
8 changes: 7 additions & 1 deletion development/src/ant-phone/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {ChangeEvent, KeyboardEvent} from "react";
"use client";

import {ChangeEvent, KeyboardEvent, ReactNode} from "react";
import types from "react-phone-hooks/types";
import {InputProps} from "antd/es/input";

Expand All @@ -17,12 +19,16 @@ export interface PhoneInputProps extends Omit<InputProps, "value" | "onChange">

disableDropdown?: boolean;

disableParentheses?: boolean;

onlyCountries?: string[];

excludeCountries?: string[];

preferredCountries?: string[];

dropdownRender?: (menu: ReactNode) => ReactNode;

onMount?(value: PhoneNumber): void;

onInput?(event: ChangeEvent<HTMLInputElement>): void;
Expand Down
17 changes: 15 additions & 2 deletions development/src/mui-phone/base/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import {ChangeEvent, forwardRef, KeyboardEvent, useCallback, useEffect, useRef, useState} from "react";
import {Input as BaseInput, InputProps} from "@mui/base/Input";

Expand All @@ -17,8 +19,17 @@ import {PhoneInputProps, PhoneNumber} from "./types";
injectMergedStyles();

const Input = forwardRef<HTMLInputElement, InputProps>(({slotProps, ...props}, ref) => {
const defaultInputProps = (slotProps?.input as any)?.className ? {} : {outline: "none", border: "none", paddingLeft: 5, width: "calc(100% - 30px)"};
const defaultRootProps = (slotProps?.root as any)?.className ? {} : {background: "white", color: "black", paddingLeft: 5};
const defaultInputProps = (slotProps?.input as any)?.className ? {} : {
outline: "none",
border: "none",
paddingLeft: 5,
width: "calc(100% - 30px)"
};
const defaultRootProps = (slotProps?.root as any)?.className ? {} : {
background: "white",
color: "black",
paddingLeft: 5
};
return (
<BaseInput
ref={ref}
Expand Down Expand Up @@ -49,6 +60,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(({slotProps, ...props}, r
const PhoneInput = forwardRef(({
value: initialValue = "",
country = getDefaultISO2Code(),
disableParentheses = false,
onlyCountries = [],
excludeCountries = [],
preferredCountries = [],
Expand All @@ -74,6 +86,7 @@ const PhoneInput = forwardRef(({
onlyCountries,
excludeCountries,
preferredCountries,
disableParentheses,
});

const {
Expand Down
2 changes: 2 additions & 0 deletions development/src/mui-phone/base/styles.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import {injectStyles, jsonToCss} from "react-phone-hooks/styles";
import commonStyles from "react-phone-hooks/stylesheet.json";

Expand Down
4 changes: 4 additions & 0 deletions development/src/mui-phone/base/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import {ChangeEvent, KeyboardEvent} from "react";
import types from "react-phone-hooks/types";
import {InputProps} from "@mui/base/Input";
Expand All @@ -9,6 +11,8 @@ export interface PhoneInputProps extends Omit<InputProps, "onChange"> {

country?: string;

disableParentheses?: boolean;

onlyCountries?: string[];

excludeCountries?: string[];
Expand Down
Loading

0 comments on commit 2336d5e

Please sign in to comment.