Browse Source

Update phone number input

Use phone number input for react on formatting phone numbers as you type.
master
TheoryOfNekomata 1 year ago
parent
commit
6fa669c48e
6 changed files with 208 additions and 33 deletions
  1. +1
    -0
      TODO.md
  2. +3
    -1
      categories/formatted/react/package.json
  3. +79
    -31
      categories/formatted/react/src/components/PhoneNumberInput/index.tsx
  4. +39
    -0
      pnpm-lock.yaml
  5. +25
    -0
      showcases/web-kitchensink-reactnext/src/pages/categories/formatted/index.tsx
  6. +61
    -1
      showcases/web-kitchensink-reactnext/src/pages/examples/registration-form/index.tsx

+ 1
- 0
TODO.md View File

@@ -52,6 +52,7 @@
- [ ] Calendar
- [X] DateDropdown
- [ ] DateTimeRangeInput
- [ ] DurationInput
- [ ] MonthInput
- [ ] MonthDayInput
- [ ] TimeSpinner


+ 3
- 1
categories/formatted/react/package.json View File

@@ -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",


+ 79
- 31
categories/formatted/react/src/components/PhoneNumberInput/index.tsx View File

@@ -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',
};

+ 39
- 0
pnpm-lock.yaml View File

@@ -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:


+ 25
- 0
showcases/web-kitchensink-reactnext/src/pages/categories/formatted/index.tsx View File

@@ -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;

+ 61
- 1
showcases/web-kitchensink-reactnext/src/pages/examples/registration-form/index.tsx View File

@@ -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>
)


Loading…
Cancel
Save