Use phone number input for react on formatting phone numbers as you type.master
@@ -52,6 +52,7 @@ | |||||
- [ ] Calendar | - [ ] Calendar | ||||
- [X] DateDropdown | - [X] DateDropdown | ||||
- [ ] DateTimeRangeInput | - [ ] DateTimeRangeInput | ||||
- [ ] DurationInput | |||||
- [ ] MonthInput | - [ ] MonthInput | ||||
- [ ] MonthDayInput | - [ ] MonthDayInput | ||||
- [ ] TimeSpinner | - [ ] TimeSpinner | ||||
@@ -58,8 +58,10 @@ | |||||
"access": "public" | "access": "public" | ||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"@tesseract-design/web-base": "workspace:*", | |||||
"clsx": "^1.2.1", | "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", | "types": "./dist/types/index.d.ts", | ||||
"main": "./dist/cjs/production/index.js", | "main": "./dist/cjs/production/index.js", | ||||
@@ -1,5 +1,7 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { TextControl } from '@tesseract-design/web-base'; | 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'; | import clsx from 'clsx'; | ||||
export type PhoneNumberInputDerivedElement = HTMLInputElement; | export type PhoneNumberInputDerivedElement = HTMLInputElement; | ||||
@@ -37,6 +39,14 @@ export interface PhoneNumberInputProps extends Omit<React.HTMLProps<PhoneNumberI | |||||
* Is the label hidden? | * Is the label hidden? | ||||
*/ | */ | ||||
hiddenLabel?: boolean, | hiddenLabel?: boolean, | ||||
/** | |||||
* Should the component be enhanced? | |||||
*/ | |||||
enhanced?: boolean, | |||||
/** | |||||
* Default country. | |||||
*/ | |||||
defaultCountry?: Country, | |||||
} | } | ||||
/** | /** | ||||
@@ -58,13 +68,63 @@ export const PhoneNumberInput = React.forwardRef< | |||||
className, | className, | ||||
id: idProp, | id: idProp, | ||||
style, | style, | ||||
enhanced = true, | |||||
defaultCountry = 'PH' as const, | |||||
value, | |||||
onChange, | |||||
name, | |||||
...etcProps | ...etcProps | ||||
}: PhoneNumberInputProps, | }: PhoneNumberInputProps, | ||||
forwardedRef, | forwardedRef, | ||||
) => { | ) => { | ||||
const { clientSide } = useClientSide({ clientSide: enhanced }); | |||||
const labelId = React.useId(); | const labelId = React.useId(); | ||||
const defaultId = React.useId(); | const defaultId = React.useId(); | ||||
const id = idProp ?? defaultId; | 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 ( | return ( | ||||
<div | <div | ||||
@@ -81,42 +141,28 @@ export const PhoneNumberInput = React.forwardRef< | |||||
> | > | ||||
<input | <input | ||||
{...etcProps} | {...etcProps} | ||||
ref={forwardedRef} | |||||
value={value} | |||||
onChange={onChange} | |||||
ref={ref} | |||||
aria-labelledby={labelId} | aria-labelledby={labelId} | ||||
type="tel" | type="tel" | ||||
id={id} | id={id} | ||||
name={name} | |||||
data-testid="input" | 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 && ( | ||||
<label | <label | ||||
@@ -212,4 +258,6 @@ PhoneNumberInput.defaultProps = { | |||||
block: false, | block: false, | ||||
variant: 'default', | variant: 'default', | ||||
hiddenLabel: false, | hiddenLabel: false, | ||||
enhanced: false, | |||||
defaultCountry: 'PH', | |||||
}; | }; |
@@ -240,12 +240,18 @@ importers: | |||||
categories/formatted/react: | categories/formatted/react: | ||||
dependencies: | dependencies: | ||||
'@modal-sh/react-utils': | |||||
specifier: workspace:* | |||||
version: link:../../../packages/react-utils | |||||
'@tesseract-design/web-base': | '@tesseract-design/web-base': | ||||
specifier: workspace:* | specifier: workspace:* | ||||
version: link:../../../base | version: link:../../../base | ||||
clsx: | clsx: | ||||
specifier: ^1.2.1 | specifier: ^1.2.1 | ||||
version: 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: | devDependencies: | ||||
'@testing-library/jest-dom': | '@testing-library/jest-dom': | ||||
specifier: ^5.16.5 | specifier: ^5.16.5 | ||||
@@ -2718,6 +2724,10 @@ packages: | |||||
engines: {node: '>=8'} | engines: {node: '>=8'} | ||||
dev: true | dev: true | ||||
/classnames@2.3.2: | |||||
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} | |||||
dev: false | |||||
/cli-cursor@4.0.0: | /cli-cursor@4.0.0: | ||||
resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} | resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} | ||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} | ||||
@@ -2841,6 +2851,10 @@ packages: | |||||
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} | resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} | ||||
dev: false | dev: false | ||||
/country-flag-icons@1.5.7: | |||||
resolution: {integrity: sha512-AdvXhMcmSp7nBSkpGfW4qR/luAdRUutJqya9PuwRbsBzuoknThfultbv7Ib6fWsHXC43Es/4QJ8gzQQdBNm75A==} | |||||
dev: false | |||||
/cross-spawn@7.0.3: | /cross-spawn@7.0.3: | ||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} | ||||
engines: {node: '>= 8'} | engines: {node: '>= 8'} | ||||
@@ -4316,6 +4330,12 @@ packages: | |||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} | resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} | ||||
dev: true | 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: | /internal-slot@1.0.5: | ||||
resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} | resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} | ||||
engines: {node: '>= 0.4'} | engines: {node: '>= 0.4'} | ||||
@@ -4781,6 +4801,10 @@ packages: | |||||
prelude-ls: 1.2.1 | prelude-ls: 1.2.1 | ||||
type-check: 0.4.0 | type-check: 0.4.0 | ||||
/libphonenumber-js@1.10.37: | |||||
resolution: {integrity: sha512-Z10PCaOCiAxbUxLyR31DNeeNugSVP6iv/m7UrSKS5JHziEMApJtgku4e9Q69pzzSC9LnQiM09sqsGf2ticZnMw==} | |||||
dev: false | |||||
/license@1.0.3: | /license@1.0.3: | ||||
resolution: {integrity: sha512-M3F6dUcor+vy4znXK5ULfTikeMWxSf/K2w7EUk5vbuZL4UAEN4zOmjh7d2UJ0/v9GYTOz13LNsLqPXJGu+A26w==} | resolution: {integrity: sha512-M3F6dUcor+vy4znXK5ULfTikeMWxSf/K2w7EUk5vbuZL4UAEN4zOmjh7d2UJ0/v9GYTOz13LNsLqPXJGu+A26w==} | ||||
hasBin: true | hasBin: true | ||||
@@ -5612,6 +5636,21 @@ packages: | |||||
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} | resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} | ||||
dev: true | 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): | /react-refractor@2.1.7(react@18.2.0): | ||||
resolution: {integrity: sha512-avNxSSsnjYg+BKpO8LVCK14KRn5pLZ+8DInMiUEeZPL6hs0SN0zafl3mJIxavGQPKyihqbXqzq4CYNflJQjaaw==} | resolution: {integrity: sha512-avNxSSsnjYg+BKpO8LVCK14KRn5pLZ+8DInMiUEeZPL6hs0SN0zafl3mJIxavGQPKyihqbXqzq4CYNflJQjaaw==} | ||||
peerDependencies: | 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 { NextPage } from 'next'; | ||||
import { getFormValues } from '@theoryofnekomata/formxtra'; | |||||
import * as Freeform from '@tesseract-design/web-freeform-react'; | import * as Freeform from '@tesseract-design/web-freeform-react'; | ||||
import * as Formatted from '@tesseract-design/web-formatted-react'; | import * as Formatted from '@tesseract-design/web-formatted-react'; | ||||
import * as Action from '@tesseract-design/web-action-react'; | import * as Action from '@tesseract-design/web-action-react'; | ||||
import {Refractor} from '@modal-sh/react-refractor'; | |||||
const RegistrationFormPage: NextPage = () => { | 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 ( | return ( | ||||
<main className="my-16 md:my-32"> | <main className="my-16 md:my-32"> | ||||
<div className="container mx-auto px-4"> | <div className="container mx-auto px-4"> | ||||
@@ -13,7 +34,7 @@ const RegistrationFormPage: NextPage = () => { | |||||
<p> | <p> | ||||
Input your data in order to gain access to the functions of this service. | Input your data in order to gain access to the functions of this service. | ||||
</p> | </p> | ||||
<form> | |||||
<form onSubmit={handleSubmit}> | |||||
<fieldset className="contents"> | <fieldset className="contents"> | ||||
<legend className="sr-only"> | <legend className="sr-only"> | ||||
Register | Register | ||||
@@ -25,6 +46,7 @@ const RegistrationFormPage: NextPage = () => { | |||||
border | border | ||||
label="First Name" | label="First Name" | ||||
name="firstName" | name="firstName" | ||||
onChange={handleChange} | |||||
/> | /> | ||||
</div> | </div> | ||||
<div className="sm:col-span-2"> | <div className="sm:col-span-2"> | ||||
@@ -33,6 +55,7 @@ const RegistrationFormPage: NextPage = () => { | |||||
border | border | ||||
label="Middle Name" | label="Middle Name" | ||||
name="middleName" | name="middleName" | ||||
onChange={handleChange} | |||||
/> | /> | ||||
</div> | </div> | ||||
<div className="sm:col-span-2"> | <div className="sm:col-span-2"> | ||||
@@ -41,6 +64,7 @@ const RegistrationFormPage: NextPage = () => { | |||||
border | border | ||||
label="Last Name" | label="Last Name" | ||||
name="lastName" | name="lastName" | ||||
onChange={handleChange} | |||||
/> | /> | ||||
</div> | </div> | ||||
<div className="sm:col-span-3"> | <div className="sm:col-span-3"> | ||||
@@ -49,6 +73,7 @@ const RegistrationFormPage: NextPage = () => { | |||||
border | border | ||||
label="Email" | label="Email" | ||||
name="email" | name="email" | ||||
onChange={handleChange} | |||||
/> | /> | ||||
</div> | </div> | ||||
<div className="sm:col-span-3"> | <div className="sm:col-span-3"> | ||||
@@ -57,6 +82,35 @@ const RegistrationFormPage: NextPage = () => { | |||||
border | border | ||||
label="Website" | label="Website" | ||||
name="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> | ||||
<div className="sm:col-span-6 text-center"> | <div className="sm:col-span-6 text-center"> | ||||
@@ -83,6 +137,12 @@ const RegistrationFormPage: NextPage = () => { | |||||
</div> | </div> | ||||
</fieldset> | </fieldset> | ||||
</form> | </form> | ||||
<div className="mt-4"> | |||||
<Refractor | |||||
language="json" | |||||
code={JSON.stringify(values, null, 2)} | |||||
/> | |||||
</div> | |||||
</div> | </div> | ||||
</main> | </main> | ||||
) | ) | ||||