Updated phone input component

This commit is contained in:
Shaurya Sharma 2025-03-24 20:58:09 +05:30
parent e9dc0f5710
commit 05fb042f2a
4 changed files with 414 additions and 1167 deletions

View file

@ -1,9 +1,15 @@
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import Accordion from '@/_ui/Accordion';
import { baseComponentProperties } from '../DefaultComponent';
import Select from '@/_ui/Select';
import useStore from '@/AppBuilder/_stores/store';
import { countries } from './en';
import { getCountries } from 'react-phone-number-input/input';
import en from 'react-phone-number-input/locale/en';
import flags from 'react-phone-number-input/flags';
import FxButton from '@/AppBuilder/CodeBuilder/Elements/FxButton';
import CodeHinter from '@/AppBuilder/CodeEditor';
import cx from 'classnames';
export const PhoneInput = ({ componentMeta, darkMode, ...restProps }) => {
const {
@ -21,7 +27,17 @@ export const PhoneInput = ({ componentMeta, darkMode, ...restProps }) => {
const events = Object.keys(componentMeta.events);
const validations = Object.keys(componentMeta.validation || {});
const resolvedProperties = useStore((state) => state.getResolvedComponent(component.id)?.properties);
const defaultCountry = resolvedProperties?.defaultCountry || 'None';
const defaultCountry = resolvedProperties?.defaultCountry;
const isDefaultCountryFxOn = componentMeta?.definition?.properties?.dateFormat?.fxActive || false;
const options = useMemo(
() =>
getCountries().map((country) => ({
label: `${en[country]}`,
value: country,
})),
[]
);
const renderCustomOption = ({ label, value: optionValue }) => {
const optionStyle = {
@ -37,10 +53,11 @@ export const PhoneInput = ({ componentMeta, darkMode, ...restProps }) => {
fontWeight: '400',
color: darkMode ? '#fff' : '#1B1F24',
};
const FlagIcon = flags[optionValue];
return (
<div style={optionStyle} className={`selectedOption ${optionValue !== 'none' && 'custom-phone-input-options'}`}>
<div style={{ width: '25px', height: '16px' }} className={`flag ${optionValue}`}></div>
<div>{FlagIcon ? <FlagIcon style={{ width: '22px', height: '16px' }} /> : null}</div>
{label}
</div>
);
@ -49,17 +66,40 @@ export const PhoneInput = ({ componentMeta, darkMode, ...restProps }) => {
const getCountryDropdown = () => {
return (
<div className="mb-2">
<label class="tj-text-xsm color-slate12 mb-2 false">Default country</label>
<Select
width="100%"
options={countries}
value={defaultCountry}
customOption={renderCustomOption}
onChange={(value) => {
console.log('value', value);
paramUpdated({ name: 'defaultCountry' }, 'value', value, 'properties');
}}
/>
<div className="d-flex justify-content-between mb-1">
<label className="form-label"> Default Country</label>
<div
className={cx({
'hide-fx': !isDefaultCountryFxOn,
})}
>
<FxButton
active={isDefaultCountryFxOn}
onPress={() => {
paramUpdated({ name: 'dateFormat' }, 'fxActive', !isDefaultCountryFxOn, 'properties');
}}
/>
</div>
</div>
{isDefaultCountryFxOn ? (
<CodeHinter
initialValue={defaultCountry}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
onChange={(value) => paramUpdated({ name: 'defaultCountry' }, 'value', value, 'properties')}
/>
) : (
<Select
width="100%"
options={options}
value={defaultCountry}
customOption={renderCustomOption}
onChange={(value) => {
paramUpdated({ name: 'defaultCountry' }, 'value', value, 'properties');
}}
/>
)}
</div>
);
};

View file

@ -145,6 +145,11 @@ export const useInput = ({
fireEvent('onChange');
};
const handlePhoneInputChange = (value) => {
setInputValue(value);
fireEvent('onChange');
};
const handleBlur = (e) => {
setShowValidationError(true);
setIsFocused(false);
@ -184,6 +189,7 @@ export const useInput = ({
validationError,
isMandatory,
setInputValue,
handlePhoneInputChange,
handleChange,
handleBlur,
handleFocus,

View file

@ -1,21 +1,24 @@
import React from 'react';
import { default as ReactPhoneInput } from 'react-phone-input-2';
import 'react-phone-input-2/lib/style.css';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import Input, { getCountries, getCountryCallingCode } from 'react-phone-number-input/input';
import en from 'react-phone-number-input/locale/en';
import flags from 'react-phone-number-input/flags';
import 'react-phone-number-input/style.css';
import Select, { components } from 'react-select';
import cx from 'classnames';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { useInput } from './BaseComponents/hooks/useInput';
import Loader from '@/ToolJetUI/Loader/Loader';
import Label from '@/_ui/Label';
import TickV3 from '@/_ui/Icon/solidIcons/TickV3';
const tinycolor = require('tinycolor2');
export const PhoneInput = (props) => {
const { properties, styles, componentName, darkMode } = props;
const transformedProps = {
...props,
inputType: 'phone',
};
const inputLogic = useInput(transformedProps);
const { properties, styles, componentName, darkMode, height, setExposedVariable, setExposedVariables, fireEvent } =
props;
const inputLogic = useInput(props);
const {
inputRef,
labelRef,
value,
visibility,
loading,
disable,
@ -23,19 +26,18 @@ export const PhoneInput = (props) => {
showValidationError,
isFocused,
labelWidth,
iconVisibility,
setIconVisibility,
isValid,
validationError,
isMandatory,
setInputValue,
handleChange,
handleBlur,
handleFocus,
handleKeyUp,
value,
handlePhoneInputChange,
} = inputLogic;
const { label, placeholder, isCountryChangeEnabled, defaultCountry = 'us' } = properties;
const { label, placeholder, isCountryChangeEnabled, defaultCountry = 'US' } = properties;
const {
padding,
textColor,
backgroundColor,
alignment,
@ -51,47 +53,70 @@ export const PhoneInput = (props) => {
} = styles;
const _width = (width / 100) * 70;
const defaultAlignment = alignment === 'side' || alignment === 'top' ? alignment : 'side';
const isInitialRender = useRef(true);
const [country, setCountry] = useState(defaultCountry);
const inputBorderColor = isFocused
? accentColor != '4368E3'
? accentColor
: 'var(--primary-accent-strong)'
: borderColor != '#CCD1D5'
? borderColor
: disable || loading
? '1px solid var(--borders-disabled-on-white)'
: 'var(--borders-default)';
const inputStyle = {
color: darkMode && textColor === '#1B1F24' ? '#FFF' : textColor,
backgroundColor: disable ? '#e4e7eb' : darkMode && backgroundColor === '#fff' ? '#1c2025' : backgroundColor,
border: `${isFocused ? '1.5px' : '1px'} solid ${inputBorderColor}`,
boxShadow,
borderRadius: `${borderRadius}px`,
const getCountryCallingCodeSafe = (country) => {
try {
return getCountryCallingCode(country);
} catch (error) {
return '';
}
};
const dropdownStyle = {
backgroundColor: darkMode ? '#1B1F24' : '#fff',
color: darkMode ? '#fff' : '#1B1F24',
const options = useMemo(
() =>
getCountries().map((country) => ({
label: `${en[country]} +${getCountryCallingCodeSafe(country)}`,
value: country,
})),
[]
);
const onInputValueChange = (value) => {
setExposedVariables({
country: country,
countryCode: getCountryCallingCodeSafe(country),
formattedValue: `+${getCountryCallingCodeSafe(country)} ${inputRef.current?.value}`,
});
handlePhoneInputChange(value);
};
const searchStyle = {
backgroundColor: darkMode ? '#1B1F24' : '#fff',
color: darkMode ? '#fff' : '#1B1F24',
const handleKeyUp = (e) => {
if (e.key === 'Enter') {
onInputValueChange(value);
fireEvent('onEnterPressed');
}
};
const containerStyle = {
backgroundColor: darkMode ? '#1B1F24' : '#fff',
color: darkMode ? '#fff' : '#1B1F24',
borderRadius: `${borderRadius}px`,
};
useEffect(() => {
if (isInitialRender.current) {
setExposedVariables({
country: country,
countryCode: getCountryCallingCodeSafe(country),
formattedValue: `+${getCountryCallingCodeSafe(country)} ${inputRef.current?.value}`,
value: value,
setCountryCode: (code) => {
let value = getCountryCallingCodeSafe(code);
if (value) {
setCountry(code);
} else {
value = getCountries().find((country) => `+${getCountryCallingCode(country)}` === code);
setCountry(value ? value : '');
}
},
});
isInitialRender.current = false;
}
}, []);
const buttonStyle = {
backgroundColor: disable ? '#e4e7eb' : darkMode && backgroundColor === '#fff' ? '#1c2025' : backgroundColor,
border: `${isFocused ? '1.5px' : '1px'} solid ${inputBorderColor}`,
borderTopLeftRadius: `${borderRadius}px`,
borderBottomLeftRadius: `${borderRadius}px`,
};
useEffect(() => {
if (!isInitialRender.current) {
setCountry(defaultCountry);
}
}, [defaultCountry]);
const disabledState = disable || loading;
const loaderStyle = {
right:
@ -112,6 +137,255 @@ export const PhoneInput = (props) => {
zIndex: 3,
};
const computedStyles = {
height: height == 36 ? (padding == 'default' ? '36px' : '40px') : padding == 'default' ? height : height + 4,
borderRadius: `${borderRadius}px`,
color: !['#1B1F24', '#000', '#000000ff'].includes(textColor)
? textColor
: disabledState
? 'var(--text-disabled)'
: 'var(--text-primary)',
borderColor: isFocused
? accentColor != '4368E3'
? accentColor
: 'var(--primary-accent-strong)'
: borderColor != '#CCD1D5'
? borderColor
: disabledState
? '1px solid var(--borders-disabled-on-white)'
: 'var(--borders-default)',
'--tblr-input-border-color-darker': tinycolor(borderColor).darken(24).toString(),
backgroundColor:
backgroundColor != '#fff'
? backgroundColor
: disabledState
? darkMode
? 'var(--surfaces-app-bg-default)'
: 'var(--surfaces-surface-03)'
: 'var(--surfaces-surface-01)',
padding: '8px 10px',
overflow: 'hidden',
textOverflow: 'ellipsis',
borderBottomLeftRadius: '0px',
borderTopLeftRadius: '0px',
borderLeft: 'none',
};
const CustomValueContainer = ({ children, getValue, ...props }) => {
const selectedValue = getValue()[0];
const FlagIcon = selectedValue ? flags[selectedValue.value] : null;
const countryCode = getCountryCallingCodeSafe(selectedValue.value);
return (
<components.ValueContainer {...props}>
{FlagIcon ? (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%' }}>
<FlagIcon style={{ height: '16px' }} />
<span style={{ marginLeft: '2px' }}>{` +${countryCode}`}</span>
</div>
) : (
children
)}
</components.ValueContainer>
);
};
const CustomOption = (props) => {
const { label, value: optionValue, isSelected } = props;
const optionStyle = {
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
minHeight: '32px',
gap: '6px',
cursor: 'pointer',
fontFamily: 'IBM Plex Sans',
fontSize: '12px',
lineHeight: '18px',
fontWeight: '400',
color: darkMode ? '#fff' : '#1B1F24',
width: '100%',
};
console.log('darkMode', darkMode);
const FlagIcon = flags[optionValue];
return (
<components.Option {...props}>
<div style={optionStyle}>
<div>{FlagIcon ? <FlagIcon style={{ width: '22px', height: '16px' }} /> : null}</div>
{label}
<div style={{ marginLeft: 'auto', display: isSelected ? 'block' : 'none' }}>
<TickV3 width="13.33px" height="11.27px" />
</div>
</div>
</components.Option>
);
};
const CustomMenuList = (props) => {
const { children, selectProps } = props;
const { onInputChange, inputValue } = selectProps;
return (
<div
className={cx('dropdown-multiselect-widget-custom-menu-list', {
'theme-dark dark-theme': selectProps?.darkMode,
})}
style={{ height: '236px' }}
onClick={(e) => e.stopPropagation()}
>
<div className="dropdown-multiselect-widget-search-box-wrapper">
<span>
<SolidIcon name="search01" width="14" />
</span>
<input
autoCorrect="off"
autoComplete="off"
spellCheck="false"
type="text"
placeholder="Search"
className="dropdown-multiselect-widget-search-box"
value={inputValue}
onChange={(e) => {
onInputChange(e.currentTarget.value, {
action: 'input-change',
});
}}
onMouseDown={(e) => {
e.stopPropagation();
e.target.focus();
}}
onTouchEnd={(e) => {
e.stopPropagation();
e.target.focus();
}}
/>
</div>
<components.MenuList {...props}>
{children?.length > 0 ? children : <div style={{ padding: '8px', textAlign: 'center' }}>No options</div>}
</components.MenuList>
</div>
);
};
const CountrySelect = ({ value, onChange, options, ...rest }) => {
const [menuIsOpen, setMenuIsOpen] = useState(false);
const dropdownRef = useRef(null);
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setMenuIsOpen(false);
}
};
// Add event listener when dropdown is open
if (menuIsOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
// Clean up the event listener
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [menuIsOpen]);
const customStyles = {
container: (provided) => ({
...provided,
minWidth: !isCountryChangeEnabled || disabledState ? '77px' : '87px',
width: !isCountryChangeEnabled || disabledState ? '77px' : '87px',
height: '100%',
}),
control: (provided, state) => ({
...provided,
minHeight: '0px',
height: '100%',
borderTopLeftRadius: `${borderRadius}px`,
borderBottomLeftRadius: `${borderRadius}px`,
borderTopRightRadius: '0px',
borderBottomRightRadius: '0px',
borderColor: `${
!isValid && showValidationError ? 'var(--status-error-strong)' : computedStyles?.borderColor
} !important`,
backgroundColor: `${
isCountryChangeEnabled
? computedStyles?.backgroundColor
: darkMode
? 'var(--surfaces-app-bg-default)'
: 'var(--surfaces-surface-03)'
} !important`,
}),
menu: (provided) => ({
...provided,
width: '208px',
height: '236px',
borderRadius: '8px',
marginTop: '2px',
}),
menuList: (provided) => ({
...provided,
maxHeight: '196px',
overflowY: 'auto',
scrollbarWidth: 'none',
gap: '1px',
padding: '8px',
borderRadius: '0px 0px 8px 8px',
display: 'flex',
flexDirection: 'column',
backgroundColor: 'var(--surfaces-surface-01)',
}),
option: (provided, state) => ({
...provided,
backgroundColor: state.isSelected ? '#4368E31A' : 'var(--surfaces-surface-01)',
...(state.isSelected && { borderRadius: '8px' }),
'&:hover': {
backgroundColor: 'var(--interactive-overlays-fill-hover)',
borderRadius: '8px',
},
display: 'flex',
cursor: 'pointer',
padding: '1px 14px',
}),
};
return (
<div style={{ height: '100%' }} onClick={() => setMenuIsOpen((prev) => !prev)} ref={dropdownRef}>
<Select
options={options}
value={value}
styles={customStyles}
onChange={onChange}
hasSearch={false}
useCustomStyles={true}
menuPortalTarget={document.body}
components={{
MenuList: CustomMenuList,
Option: CustomOption,
ValueContainer: CustomValueContainer, // Add this line
IndicatorSeparator: () => null,
DropdownIndicator:
!isCountryChangeEnabled || disabledState
? () => null
: () => (
<div style={{ position: 'relative', display: 'flex', left: '-1px' }}>
{menuIsOpen ? (
<SolidIcon name="TriangleDownCenter" width="16" height="16" />
) : (
<SolidIcon name="TriangleUpCenter" width="16" height="16" />
)}
</div>
),
}}
darkMode={darkMode}
isDisabled={disabledState}
menuIsOpen={menuIsOpen}
/>
</div>
);
};
return (
<>
<div
@ -144,29 +418,34 @@ export const PhoneInput = (props) => {
_width={_width}
labelWidth={labelWidth}
/>
<ReactPhoneInput
placeholder={placeholder}
value={value}
onChange={setInputValue}
enableSearch={true}
ref={inputRef}
inputStyle={inputStyle}
buttonStyle={buttonStyle}
searchPlaceholder="Search"
disabled={disable || loading}
onBlur={handleBlur}
onFocus={handleFocus}
inputProps={{
autoFocus: true,
}}
onKeyDown={handleKeyUp}
disableDropdown={!isCountryChangeEnabled}
{...(defaultCountry !== 'none' && { country: defaultCountry })}
countryCodeEditable={isCountryChangeEnabled}
dropdownStyle={dropdownStyle}
searchStyle={searchStyle}
containerStyle={containerStyle}
/>
<div className="d-flex h-100 w-100" style={{ boxShadow, borderRadius: `${borderRadius}px` }}>
<CountrySelect
value={{ label: `${en[country]} +${getCountryCallingCodeSafe(country)}`, value: country }}
options={options}
onChange={(selectedOption) => {
if (selectedOption) {
setCountry(selectedOption.value);
}
}}
/>
<Input
ref={inputRef}
country={country}
international={false}
value={value}
onChange={onInputValueChange}
placeholder={placeholder}
style={computedStyles}
className={`tj-text-input-widget ${
!isValid && showValidationError ? 'is-invalid' : ''
} validation-without-icon`}
disabled={disabledState}
data-ignore-hover={true}
onBlur={handleBlur}
onFocus={handleFocus}
onKeyUp={handleKeyUp}
/>
</div>
{loading && <Loader style={loaderStyle} width="16" />}
</div>
{showValidationError && visibility && (

File diff suppressed because one or more lines are too long