Skip to content

Commit 2336d5e

Browse files
Implement option for disabling parentheses (GH-34)
2 parents 37f2eb1 + bb7f3ec commit 2336d5e

24 files changed

+335
-140
lines changed

Diff for: development/src/ant-phone/index.tsx

+69-29
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"use client";
2+
13
import {
24
ChangeEvent,
35
forwardRef,
@@ -10,7 +12,9 @@ import {
1012
useState
1113
} from "react";
1214
import useFormInstance from "antd/es/form/hooks/useFormInstance";
15+
import {ConfigContext} from "antd/es/config-provider";
1316
import {FormContext} from "antd/es/form/context";
17+
import {useWatch} from "antd/es/form/Form";
1418
import Select from "antd/es/select";
1519
import Input from "antd/es/input";
1620

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

34-
injectMergedStyles();
35-
3638
const PhoneInput = forwardRef(({
3739
value: initialValue = "",
3840
country = getDefaultISO2Code(),
41+
disabled = false,
3942
enableSearch = false,
4043
disableDropdown = false,
44+
disableParentheses = false,
4145
onlyCountries = [],
4246
excludeCountries = [],
4347
preferredCountries = [],
4448
searchNotFound = "No country found",
4549
searchPlaceholder = "Search country",
50+
dropdownRender = (node) => node,
4651
onMount: handleMount = () => null,
4752
onInput: handleInput = () => null,
4853
onChange: handleChange = () => null,
@@ -51,13 +56,18 @@ const PhoneInput = forwardRef(({
5156
}: PhoneInputProps, forwardedRef: any) => {
5257
const formInstance = useFormInstance();
5358
const formContext = useContext(FormContext);
59+
const {getPrefixCls} = useContext(ConfigContext);
5460
const inputRef = useRef<any>(null);
61+
const searchRef = useRef<any>(null);
5562
const selectedRef = useRef<boolean>(false);
5663
const initiatedRef = useRef<boolean>(false);
5764
const [query, setQuery] = useState<string>("");
5865
const [minWidth, setMinWidth] = useState<number>(0);
5966
const [countryCode, setCountryCode] = useState<string>(country);
6067

68+
const prefixCls = getPrefixCls();
69+
injectMergedStyles(prefixCls);
70+
6171
const {
6272
value,
6373
pattern,
@@ -72,6 +82,7 @@ const PhoneInput = forwardRef(({
7282
onlyCountries,
7383
excludeCountries,
7484
preferredCountries,
85+
disableParentheses,
7586
});
7687

7788
const {
@@ -85,18 +96,22 @@ const PhoneInput = forwardRef(({
8596
return ({...metadata})?.[0] + ({...metadata})?.[2];
8697
}, [countriesList, countryCode, value])
8798

88-
const setFieldValue = useCallback((value: PhoneNumber) => {
89-
if (formInstance) {
90-
let namePath = [];
91-
let formName = (formContext as any)?.name || "";
92-
let fieldName = (antInputProps as any)?.id || "";
93-
if (formName) {
94-
namePath.push(formName);
95-
fieldName = fieldName.slice(formName.length + 1);
96-
}
97-
formInstance.setFieldValue(namePath.concat(fieldName.split("_")), value);
99+
const namePath = useMemo(() => {
100+
let path = [];
101+
let formName = (formContext as any)?.name || "";
102+
let fieldName = (antInputProps as any)?.id || "";
103+
if (formName) {
104+
path.push(formName);
105+
fieldName = fieldName.slice(formName.length + 1);
98106
}
99-
}, [antInputProps, formContext, formInstance])
107+
return path.concat(fieldName.split("_"));
108+
}, [antInputProps, formContext])
109+
110+
const phoneValue = useWatch(namePath, formInstance);
111+
112+
const setFieldValue = useCallback((value: PhoneNumber) => {
113+
if (formInstance) formInstance.setFieldValue(namePath, value);
114+
}, [formInstance, namePath])
100115

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

140+
const onDropdownVisibleChange = useCallback((open: boolean) => {
141+
if (open && enableSearch) setTimeout(() => searchRef.current.focus(), 100);
142+
}, [enableSearch])
143+
125144
const ref = useCallback((node: any) => {
126145
[forwardedRef, inputRef].forEach((ref) => {
127146
if (typeof ref === "function") ref(node);
128147
else if (ref != null) ref.current = node;
129148
})
130149
}, [forwardedRef])
131150

151+
useEffect(() => {
152+
const rawValue = getRawValue(phoneValue);
153+
const metadata = getMetadata(rawValue);
154+
// Skip if value has not been updated by `setFieldValue`.
155+
if (!metadata?.[3] || rawValue === getRawValue(value)) return;
156+
const formattedNumber = getFormattedNumber(rawValue, metadata?.[3] as string);
157+
const phoneMetadata = parsePhoneNumber(formattedNumber);
158+
setFieldValue({...phoneMetadata, valid: (strict: boolean) => checkValidity(phoneMetadata, strict)});
159+
setCountryCode(metadata?.[0] as string);
160+
setValue(formattedNumber);
161+
}, [phoneValue, value, setFieldValue, setValue])
162+
132163
useEffect(() => {
133164
if (initiatedRef.current) return;
134165
initiatedRef.current = true;
@@ -147,28 +178,33 @@ const PhoneInput = forwardRef(({
147178
<Select
148179
suffixIcon={null}
149180
value={selectValue}
181+
disabled={disabled}
150182
open={disableDropdown ? false : undefined}
151183
onSelect={(selectedOption, {key}) => {
152184
const [_, mask] = key.split("_");
153-
if (selectValue === selectedOption) return;
154185
const selectedCountryCode = selectedOption.slice(0, 2);
155186
const formattedNumber = displayFormat(cleanInput(mask, mask).join(""));
156187
const phoneMetadata = parsePhoneNumber(formattedNumber, countriesList, selectedCountryCode);
157188
setFieldValue({...phoneMetadata, valid: (strict: boolean) => checkValidity(phoneMetadata, strict)});
158189
setCountryCode(selectedCountryCode);
159190
setValue(formattedNumber);
191+
setQuery("");
160192
selectedRef.current = true;
161193
const nativeInputValueSetter = (Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value") as any).set;
162194
nativeInputValueSetter.call(inputRef.current.input, formattedNumber);
163195
inputRef.current.input.dispatchEvent(new Event("change", {bubbles: true}));
196+
inputRef.current.input.focus();
164197
}}
165198
optionLabelProp="label"
166199
dropdownStyle={{minWidth}}
167200
notFoundContent={searchNotFound}
201+
onDropdownVisibleChange={onDropdownVisibleChange}
168202
dropdownRender={(menu) => (
169-
<div className="ant-phone-input-search-wrapper">
203+
<div className={`${prefixCls}-phone-input-search-wrapper`}>
170204
{enableSearch && (
171205
<Input
206+
value={query}
207+
ref={searchRef}
172208
placeholder={searchPlaceholder}
173209
onInput={({target}: any) => setQuery(target.value)}
174210
/>
@@ -177,22 +213,25 @@ const PhoneInput = forwardRef(({
177213
</div>
178214
)}
179215
>
180-
{countriesList.map(([iso, name, dial, mask]) => (
181-
<Select.Option
182-
value={iso + dial}
183-
key={`${iso}_${mask}`}
184-
label={<div className={`flag ${iso}`}/>}
185-
children={<div className="ant-phone-input-select-item">
186-
<div className={`flag ${iso}`}/>
187-
{name}&nbsp;{displayFormat(mask)}
188-
</div>}
189-
/>
190-
))}
216+
{countriesList.map(([iso, name, dial, pattern]) => {
217+
const mask = disableParentheses ? pattern.replace(/[()]/g, "") : pattern;
218+
return (
219+
<Select.Option
220+
value={iso + dial}
221+
key={`${iso}_${mask}`}
222+
label={<div className={`flag ${iso}`}/>}
223+
children={<div className={`${prefixCls}-phone-input-select-item`}>
224+
<div className={`flag ${iso}`}/>
225+
{name}&nbsp;{displayFormat(mask)}
226+
</div>}
227+
/>
228+
)
229+
})}
191230
</Select>
192-
), [selectValue, disableDropdown, minWidth, searchNotFound, countriesList, setFieldValue, setValue, enableSearch, searchPlaceholder])
231+
), [selectValue, query, disabled, disableParentheses, disableDropdown, onDropdownVisibleChange, minWidth, searchNotFound, countriesList, setFieldValue, setValue, prefixCls, enableSearch, searchPlaceholder])
193232

194233
return (
195-
<div className="ant-phone-input-wrapper"
234+
<div className={`${prefixCls}-phone-input-wrapper`}
196235
ref={node => setMinWidth(node?.offsetWidth || 0)}>
197236
<Input
198237
ref={ref}
@@ -201,7 +240,8 @@ const PhoneInput = forwardRef(({
201240
onInput={onInput}
202241
onChange={onChange}
203242
onKeyDown={onKeyDown}
204-
addonBefore={countriesSelect}
243+
addonBefore={dropdownRender(countriesSelect)}
244+
disabled={disabled}
205245
{...antInputProps}
206246
/>
207247
</div>

Diff for: development/src/ant-phone/resources/stylesheet.json

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"margin": "0 6px 6px 6px"
1313
},
1414
".ant-phone-input-wrapper .ant-select-selector": {
15+
"padding": "0 11px !important",
16+
"height": "unset !important",
1517
"border": "none !important"
1618
},
1719
".ant-phone-input-wrapper .ant-select-selection-item": {

Diff for: development/src/ant-phone/styles.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
1+
"use client";
2+
13
import {injectStyles, jsonToCss} from "react-phone-hooks/styles";
24
import commonStyles from "react-phone-hooks/stylesheet.json";
5+
import {defaultPrefixCls} from "antd/es/config-provider";
36

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

6-
export const injectMergedStyles = () => injectStyles(jsonToCss(Object.assign(commonStyles, customStyles)));
9+
let prefix: any = null;
10+
11+
export const injectMergedStyles = (prefixCls: any = null) => {
12+
const stylesheet = customStyles as { [key: string]: any };
13+
if (prefixCls && prefixCls !== defaultPrefixCls) {
14+
if (prefix === prefixCls) return;
15+
Object.entries(stylesheet).forEach(([k, value]) => {
16+
const key = k.replace(/ant(?=-)/g, prefixCls);
17+
stylesheet[key] = value;
18+
delete stylesheet[k];
19+
})
20+
prefix = prefixCls;
21+
}
22+
return injectStyles(jsonToCss(Object.assign(commonStyles, stylesheet)));
23+
}

Diff for: development/src/ant-phone/types.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import {ChangeEvent, KeyboardEvent} from "react";
1+
"use client";
2+
3+
import {ChangeEvent, KeyboardEvent, ReactNode} from "react";
24
import types from "react-phone-hooks/types";
35
import {InputProps} from "antd/es/input";
46

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

1820
disableDropdown?: boolean;
1921

22+
disableParentheses?: boolean;
23+
2024
onlyCountries?: string[];
2125

2226
excludeCountries?: string[];
2327

2428
preferredCountries?: string[];
2529

30+
dropdownRender?: (menu: ReactNode) => ReactNode;
31+
2632
onMount?(value: PhoneNumber): void;
2733

2834
onInput?(event: ChangeEvent<HTMLInputElement>): void;

Diff for: development/src/mui-phone/base/index.tsx

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"use client";
2+
13
import {ChangeEvent, forwardRef, KeyboardEvent, useCallback, useEffect, useRef, useState} from "react";
24
import {Input as BaseInput, InputProps} from "@mui/base/Input";
35

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

1921
const Input = forwardRef<HTMLInputElement, InputProps>(({slotProps, ...props}, ref) => {
20-
const defaultInputProps = (slotProps?.input as any)?.className ? {} : {outline: "none", border: "none", paddingLeft: 5, width: "calc(100% - 30px)"};
21-
const defaultRootProps = (slotProps?.root as any)?.className ? {} : {background: "white", color: "black", paddingLeft: 5};
22+
const defaultInputProps = (slotProps?.input as any)?.className ? {} : {
23+
outline: "none",
24+
border: "none",
25+
paddingLeft: 5,
26+
width: "calc(100% - 30px)"
27+
};
28+
const defaultRootProps = (slotProps?.root as any)?.className ? {} : {
29+
background: "white",
30+
color: "black",
31+
paddingLeft: 5
32+
};
2233
return (
2334
<BaseInput
2435
ref={ref}
@@ -49,6 +60,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(({slotProps, ...props}, r
4960
const PhoneInput = forwardRef(({
5061
value: initialValue = "",
5162
country = getDefaultISO2Code(),
63+
disableParentheses = false,
5264
onlyCountries = [],
5365
excludeCountries = [],
5466
preferredCountries = [],
@@ -74,6 +86,7 @@ const PhoneInput = forwardRef(({
7486
onlyCountries,
7587
excludeCountries,
7688
preferredCountries,
89+
disableParentheses,
7790
});
7891

7992
const {

Diff for: development/src/mui-phone/base/styles.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"use client";
2+
13
import {injectStyles, jsonToCss} from "react-phone-hooks/styles";
24
import commonStyles from "react-phone-hooks/stylesheet.json";
35

Diff for: development/src/mui-phone/base/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"use client";
2+
13
import {ChangeEvent, KeyboardEvent} from "react";
24
import types from "react-phone-hooks/types";
35
import {InputProps} from "@mui/base/Input";
@@ -9,6 +11,8 @@ export interface PhoneInputProps extends Omit<InputProps, "onChange"> {
911

1012
country?: string;
1113

14+
disableParentheses?: boolean;
15+
1216
onlyCountries?: string[];
1317

1418
excludeCountries?: string[];

0 commit comments

Comments
 (0)