Use phone number input for react on formatting phone numbers as you type.master
@@ -52,6 +52,7 @@ | |||
- [ ] Calendar | |||
- [X] DateDropdown | |||
- [ ] DateTimeRangeInput | |||
- [ ] DurationInput | |||
- [ ] MonthInput | |||
- [ ] MonthDayInput | |||
- [ ] TimeSpinner | |||
@@ -58,8 +58,10 @@ | |||
"access": "public" | |||
}, | |||
"dependencies": { | |||
"@tesseract-design/web-base": "workspace:*", | |||
"clsx": "^1.2.1", | |||
"@tesseract-design/web-base": "workspace:*" | |||
"react-phone-number-input": "^3.3.0", | |||
"@modal-sh/react-utils": "workspace:*" | |||
}, | |||
"types": "./dist/types/index.d.ts", | |||
"main": "./dist/cjs/production/index.js", | |||
@@ -1,5 +1,7 @@ | |||
import * as React from 'react'; | |||
import { TextControl } from '@tesseract-design/web-base'; | |||
import { delegateTriggerEvent, useClientSide } from '@modal-sh/react-utils'; | |||
import PhoneInput, {Country, Value} from 'react-phone-number-input/input'; | |||
import clsx from 'clsx'; | |||
export type PhoneNumberInputDerivedElement = HTMLInputElement; | |||
@@ -37,6 +39,14 @@ export interface PhoneNumberInputProps extends Omit<React.HTMLProps<PhoneNumberI | |||
* Is the label hidden? | |||
*/ | |||
hiddenLabel?: boolean, | |||
/** | |||
* Should the component be enhanced? | |||
*/ | |||
enhanced?: boolean, | |||
/** | |||
* Default country. | |||
*/ | |||
defaultCountry?: Country, | |||
} | |||
/** | |||
@@ -58,13 +68,63 @@ export const PhoneNumberInput = React.forwardRef< | |||
className, | |||
id: idProp, | |||
style, | |||
enhanced = true, | |||
defaultCountry = 'PH' as const, | |||
value, | |||
onChange, | |||
name, | |||
...etcProps | |||
}: PhoneNumberInputProps, | |||
forwardedRef, | |||
) => { | |||
const { clientSide } = useClientSide({ clientSide: enhanced }); | |||
const labelId = React.useId(); | |||
const defaultId = React.useId(); | |||
const id = idProp ?? defaultId; | |||
const defaultRef = React.useRef<PhoneNumberInputDerivedElement>(null); | |||
const ref = forwardedRef ?? defaultRef; | |||
const handlePhoneInputChange = (phoneNumberValue: Value) => { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const { current: input } = ref; | |||
if (!input) { | |||
return; | |||
} | |||
delegateTriggerEvent('change', input, phoneNumberValue); | |||
}; | |||
const commonInputStyles = clsx( | |||
'bg-negative rounded-inherit w-full peer block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'h-10': size === 'small', | |||
'h-12': size === 'medium', | |||
'h-16': size === 'large', | |||
}, | |||
); | |||
return ( | |||
<div | |||
@@ -81,42 +141,28 @@ export const PhoneNumberInput = React.forwardRef< | |||
> | |||
<input | |||
{...etcProps} | |||
ref={forwardedRef} | |||
value={value} | |||
onChange={onChange} | |||
ref={ref} | |||
aria-labelledby={labelId} | |||
type="tel" | |||
id={id} | |||
name={name} | |||
data-testid="input" | |||
className={clsx( | |||
'bg-negative rounded-inherit w-full peer block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
{ | |||
'h-10': size === 'small', | |||
'h-12': size === 'medium', | |||
'h-16': size === 'large', | |||
}, | |||
)} | |||
tabIndex={clientSide ? -1 : undefined} | |||
className={clsx(commonInputStyles, clientSide && 'sr-only')} | |||
/> | |||
{ | |||
clientSide && ( | |||
<PhoneInput | |||
{...etcProps} | |||
ref={undefined} | |||
onChange={handlePhoneInputChange} | |||
defaultCountry={defaultCountry} | |||
className={commonInputStyles} | |||
/> | |||
) | |||
} | |||
{ | |||
label && ( | |||
<label | |||
@@ -212,4 +258,6 @@ PhoneNumberInput.defaultProps = { | |||
block: false, | |||
variant: 'default', | |||
hiddenLabel: false, | |||
enhanced: false, | |||
defaultCountry: 'PH', | |||
}; |
@@ -240,12 +240,18 @@ importers: | |||
categories/formatted/react: | |||
dependencies: | |||
'@modal-sh/react-utils': | |||
specifier: workspace:* | |||
version: link:../../../packages/react-utils | |||
'@tesseract-design/web-base': | |||
specifier: workspace:* | |||
version: link:../../../base | |||
clsx: | |||
specifier: ^1.2.1 | |||
version: 1.2.1 | |||
react-phone-number-input: | |||
specifier: ^3.3.0 | |||
version: 3.3.0(react-dom@18.2.0)(react@18.2.0) | |||
devDependencies: | |||
'@testing-library/jest-dom': | |||
specifier: ^5.16.5 | |||
@@ -2718,6 +2724,10 @@ packages: | |||
engines: {node: '>=8'} | |||
dev: true | |||
/classnames@2.3.2: | |||
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} | |||
dev: false | |||
/cli-cursor@4.0.0: | |||
resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} | |||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} | |||
@@ -2841,6 +2851,10 @@ packages: | |||
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} | |||
dev: false | |||
/country-flag-icons@1.5.7: | |||
resolution: {integrity: sha512-AdvXhMcmSp7nBSkpGfW4qR/luAdRUutJqya9PuwRbsBzuoknThfultbv7Ib6fWsHXC43Es/4QJ8gzQQdBNm75A==} | |||
dev: false | |||
/cross-spawn@7.0.3: | |||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} | |||
engines: {node: '>= 8'} | |||
@@ -4316,6 +4330,12 @@ packages: | |||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} | |||
dev: true | |||
/input-format@0.3.8: | |||
resolution: {integrity: sha512-tLR0XRig1xIcG1PtIpMd/uoltvkAI62CN9OIbtj4/tEJAkqTCQLNHUZ9N4M46w0dopny7Rlt/lRH5Xzp7e6F+g==} | |||
dependencies: | |||
prop-types: 15.8.1 | |||
dev: false | |||
/internal-slot@1.0.5: | |||
resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} | |||
engines: {node: '>= 0.4'} | |||
@@ -4781,6 +4801,10 @@ packages: | |||
prelude-ls: 1.2.1 | |||
type-check: 0.4.0 | |||
/libphonenumber-js@1.10.37: | |||
resolution: {integrity: sha512-Z10PCaOCiAxbUxLyR31DNeeNugSVP6iv/m7UrSKS5JHziEMApJtgku4e9Q69pzzSC9LnQiM09sqsGf2ticZnMw==} | |||
dev: false | |||
/license@1.0.3: | |||
resolution: {integrity: sha512-M3F6dUcor+vy4znXK5ULfTikeMWxSf/K2w7EUk5vbuZL4UAEN4zOmjh7d2UJ0/v9GYTOz13LNsLqPXJGu+A26w==} | |||
hasBin: true | |||
@@ -5612,6 +5636,21 @@ packages: | |||
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} | |||
dev: true | |||
/react-phone-number-input@3.3.0(react-dom@18.2.0)(react@18.2.0): | |||
resolution: {integrity: sha512-6d1lq9parRGnVz6laEN7ijU7MeUCkFEJsTnzB/97nVrm/WE48EDEV5/2bu08mzfZjvX6shpryqG0DUCNiP07Cg==} | |||
peerDependencies: | |||
react: '>=16.8' | |||
react-dom: '>=16.8' | |||
dependencies: | |||
classnames: 2.3.2 | |||
country-flag-icons: 1.5.7 | |||
input-format: 0.3.8 | |||
libphonenumber-js: 1.10.37 | |||
prop-types: 15.8.1 | |||
react: 18.2.0 | |||
react-dom: 18.2.0(react@18.2.0) | |||
dev: false | |||
/react-refractor@2.1.7(react@18.2.0): | |||
resolution: {integrity: sha512-avNxSSsnjYg+BKpO8LVCK14KRn5pLZ+8DInMiUEeZPL6hs0SN0zafl3mJIxavGQPKyihqbXqzq4CYNflJQjaaw==} | |||
peerDependencies: | |||
@@ -0,0 +1,25 @@ | |||
import {NextPage} from 'next'; | |||
import {DefaultLayout} from '@/components/DefaultLayout'; | |||
import {Section, Subsection} from '@/components/Section'; | |||
import * as Formatted from '@tesseract-design/web-formatted-react'; | |||
const TemporalPage: NextPage = () => { | |||
return ( | |||
<DefaultLayout title="Formatted"> | |||
<Section title="PhoneNumberInput"> | |||
<Subsection title="Default"> | |||
<Formatted.PhoneNumberInput | |||
label="Phone" | |||
name="phone" | |||
enhanced | |||
onFocus={(e) => { console.log('focus', e.currentTarget)}} | |||
onBlur={(e) => { console.log('blur', e.currentTarget)}} | |||
onChange={(e) => { console.log('change', e.currentTarget.name, e.currentTarget, e.currentTarget.value)}} | |||
/> | |||
</Subsection> | |||
</Section> | |||
</DefaultLayout> | |||
) | |||
} | |||
export default TemporalPage; |
@@ -1,9 +1,30 @@ | |||
import * as React from 'react'; | |||
import { NextPage } from 'next'; | |||
import { getFormValues } from '@theoryofnekomata/formxtra'; | |||
import * as Freeform from '@tesseract-design/web-freeform-react'; | |||
import * as Formatted from '@tesseract-design/web-formatted-react'; | |||
import * as Action from '@tesseract-design/web-action-react'; | |||
import {Refractor} from '@modal-sh/react-refractor'; | |||
const RegistrationFormPage: NextPage = () => { | |||
const [values, setValues] = React.useState({} as Record<string, unknown>); | |||
const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => { | |||
e.preventDefault(); | |||
const newValues = getFormValues(e.currentTarget); | |||
setValues(newValues); | |||
}; | |||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { | |||
e.preventDefault(); | |||
const form = e.currentTarget.form; | |||
if (!form) { | |||
return; | |||
} | |||
const newValues = getFormValues(form); | |||
setValues(newValues); | |||
}; | |||
return ( | |||
<main className="my-16 md:my-32"> | |||
<div className="container mx-auto px-4"> | |||
@@ -13,7 +34,7 @@ const RegistrationFormPage: NextPage = () => { | |||
<p> | |||
Input your data in order to gain access to the functions of this service. | |||
</p> | |||
<form> | |||
<form onSubmit={handleSubmit}> | |||
<fieldset className="contents"> | |||
<legend className="sr-only"> | |||
Register | |||
@@ -25,6 +46,7 @@ const RegistrationFormPage: NextPage = () => { | |||
border | |||
label="First Name" | |||
name="firstName" | |||
onChange={handleChange} | |||
/> | |||
</div> | |||
<div className="sm:col-span-2"> | |||
@@ -33,6 +55,7 @@ const RegistrationFormPage: NextPage = () => { | |||
border | |||
label="Middle Name" | |||
name="middleName" | |||
onChange={handleChange} | |||
/> | |||
</div> | |||
<div className="sm:col-span-2"> | |||
@@ -41,6 +64,7 @@ const RegistrationFormPage: NextPage = () => { | |||
border | |||
label="Last Name" | |||
name="lastName" | |||
onChange={handleChange} | |||
/> | |||
</div> | |||
<div className="sm:col-span-3"> | |||
@@ -49,6 +73,7 @@ const RegistrationFormPage: NextPage = () => { | |||
border | |||
label="Email" | |||
name="email" | |||
onChange={handleChange} | |||
/> | |||
</div> | |||
<div className="sm:col-span-3"> | |||
@@ -57,6 +82,35 @@ const RegistrationFormPage: NextPage = () => { | |||
border | |||
label="Website" | |||
name="website" | |||
onChange={handleChange} | |||
/> | |||
</div> | |||
<div className="sm:col-span-2"> | |||
<Formatted.PhoneNumberInput | |||
block | |||
border | |||
label="Phone" | |||
name="phone" | |||
onChange={handleChange} | |||
enhanced | |||
/> | |||
</div> | |||
<div className="sm:col-span-2"> | |||
<Freeform.MaskedTextInput | |||
block | |||
border | |||
label="Password" | |||
name="password" | |||
onChange={handleChange} | |||
/> | |||
</div> | |||
<div className="sm:col-span-2"> | |||
<Freeform.MaskedTextInput | |||
block | |||
border | |||
label="Confirm Password" | |||
name="confirmPassword" | |||
onChange={handleChange} | |||
/> | |||
</div> | |||
<div className="sm:col-span-6 text-center"> | |||
@@ -83,6 +137,12 @@ const RegistrationFormPage: NextPage = () => { | |||
</div> | |||
</fieldset> | |||
</form> | |||
<div className="mt-4"> | |||
<Refractor | |||
language="json" | |||
code={JSON.stringify(values, null, 2)} | |||
/> | |||
</div> | |||
</div> | |||
</main> | |||
) | |||