mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
Merge pull request #3113 from logto-io/simeng-ui-clean-up
refactor(ui): clean up legacy code
This commit is contained in:
commit
43ecf01ce8
9 changed files with 0 additions and 642 deletions
|
@ -1,57 +0,0 @@
|
||||||
import { render, fireEvent } from '@testing-library/react';
|
|
||||||
|
|
||||||
import PasswordInput from './PasswordInput';
|
|
||||||
|
|
||||||
describe('Input Field UI Component', () => {
|
|
||||||
const text = 'foo';
|
|
||||||
const onChange = jest.fn();
|
|
||||||
|
|
||||||
test('render password input', () => {
|
|
||||||
const { container } = render(
|
|
||||||
<PasswordInput
|
|
||||||
name="foo"
|
|
||||||
value={text}
|
|
||||||
onChange={({ target }) => {
|
|
||||||
if (target instanceof HTMLInputElement) {
|
|
||||||
onChange(target.value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const inputEle = container.querySelector('input');
|
|
||||||
expect(inputEle?.value).toEqual(text);
|
|
||||||
expect(inputEle?.type).toEqual('password');
|
|
||||||
|
|
||||||
if (inputEle) {
|
|
||||||
fireEvent.change(inputEle, { target: { value: 'update' } });
|
|
||||||
expect(onChange).toBeCalledWith('update');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('render error message', () => {
|
|
||||||
const errorCode = 'password_required';
|
|
||||||
const { queryByText } = render(<PasswordInput error={errorCode} />);
|
|
||||||
expect(queryByText(errorCode)).not.toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('click on toggle visibility button', () => {
|
|
||||||
const { container } = render(<PasswordInput name="foo" value={text} onChange={onChange} />);
|
|
||||||
|
|
||||||
const inputEle = container.querySelector('input');
|
|
||||||
|
|
||||||
if (!inputEle) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fireEvent.focus(inputEle);
|
|
||||||
|
|
||||||
const visibilityButton = container.querySelector('svg');
|
|
||||||
expect(visibilityButton).not.toBeNull();
|
|
||||||
|
|
||||||
if (visibilityButton) {
|
|
||||||
fireEvent.mouseDown(visibilityButton);
|
|
||||||
expect(inputEle.type).toEqual('text');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,64 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import type { HTMLProps } from 'react';
|
|
||||||
import { useState, useRef } from 'react';
|
|
||||||
|
|
||||||
import PasswordHideIcon from '@/assets/icons/password-hide-icon.svg';
|
|
||||||
import PasswordShowIcon from '@/assets/icons/password-show-icon.svg';
|
|
||||||
import type { ErrorType } from '@/components/ErrorMessage';
|
|
||||||
import ErrorMessage from '@/components/ErrorMessage';
|
|
||||||
|
|
||||||
import * as styles from './index.module.scss';
|
|
||||||
|
|
||||||
export type Props = Omit<HTMLProps<HTMLInputElement>, 'type'> & {
|
|
||||||
className?: string;
|
|
||||||
error?: ErrorType;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PasswordInput = ({ className, value, error, onFocus, onBlur, ...rest }: Props) => {
|
|
||||||
// Toggle the password visibility
|
|
||||||
const [type, setType] = useState('password');
|
|
||||||
const [onInputFocus, setOnInputFocus] = useState(false);
|
|
||||||
const inputElement = useRef<HTMLInputElement>(null);
|
|
||||||
const Icon = type === 'password' ? PasswordHideIcon : PasswordShowIcon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<div className={classNames(styles.wrapper, error && styles.error)}>
|
|
||||||
<input
|
|
||||||
ref={inputElement}
|
|
||||||
type={type}
|
|
||||||
value={value}
|
|
||||||
onFocus={(event) => {
|
|
||||||
setOnInputFocus(true);
|
|
||||||
onFocus?.(event);
|
|
||||||
}}
|
|
||||||
onBlur={(event) => {
|
|
||||||
setOnInputFocus(false);
|
|
||||||
onBlur?.(event);
|
|
||||||
}}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
{value && onInputFocus && (
|
|
||||||
<Icon
|
|
||||||
className={styles.actionButton}
|
|
||||||
onMouseDown={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setType(type === 'password' ? 'text' : 'password');
|
|
||||||
|
|
||||||
if (inputElement.current) {
|
|
||||||
const { length } = inputElement.current.value;
|
|
||||||
// Force async render, move cursor to the end of the input
|
|
||||||
setTimeout(() => {
|
|
||||||
inputElement.current?.setSelectionRange(length, length);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{error && <ErrorMessage className={styles.errorMessage} error={error} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PasswordInput;
|
|
|
@ -1,97 +0,0 @@
|
||||||
import { render, fireEvent } from '@testing-library/react';
|
|
||||||
|
|
||||||
import { getCountryList, getDefaultCountryCallingCode } from '@/utils/country-code';
|
|
||||||
|
|
||||||
import PhoneInput from './PhoneInput';
|
|
||||||
|
|
||||||
jest.mock('i18next', () => ({
|
|
||||||
language: 'en',
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('Phone Input Field UI Component', () => {
|
|
||||||
const onChange = jest.fn();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
onChange.mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultCountryCallingCode = getDefaultCountryCallingCode();
|
|
||||||
|
|
||||||
it('render empty PhoneInput', () => {
|
|
||||||
const { queryByText, container } = render(
|
|
||||||
<PhoneInput name="PhoneInput" nationalNumber="" onChange={onChange} />
|
|
||||||
);
|
|
||||||
expect(queryByText(`+${defaultCountryCallingCode}`)).toBeNull();
|
|
||||||
expect(container.querySelector('input')?.value).toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('render with country list', () => {
|
|
||||||
const { queryAllByText, container } = render(
|
|
||||||
<PhoneInput
|
|
||||||
name="PhoneInput"
|
|
||||||
nationalNumber=""
|
|
||||||
countryList={getCountryList()}
|
|
||||||
countryCallingCode={defaultCountryCallingCode}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const countryCode = queryAllByText(`+${defaultCountryCallingCode}`);
|
|
||||||
expect(countryCode).toHaveLength(2);
|
|
||||||
|
|
||||||
const selector = container.querySelector('select');
|
|
||||||
expect(selector).not.toBeNull();
|
|
||||||
|
|
||||||
if (selector) {
|
|
||||||
fireEvent.change(selector, { target: { value: '1' } });
|
|
||||||
expect(onChange).toBeCalledWith({ countryCallingCode: '1' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('render input update', () => {
|
|
||||||
const { container } = render(
|
|
||||||
<PhoneInput name="PhoneInput" nationalNumber="911" onChange={onChange} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const inputField = container.querySelector('input');
|
|
||||||
expect(inputField?.value).toBe('911');
|
|
||||||
|
|
||||||
if (inputField) {
|
|
||||||
fireEvent.change(inputField, { target: { value: '110' } });
|
|
||||||
expect(onChange).toBeCalledWith({ nationalNumber: '110' });
|
|
||||||
fireEvent.focus(inputField);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('render input error', () => {
|
|
||||||
const { queryByText } = render(
|
|
||||||
<PhoneInput
|
|
||||||
name="PhoneInput"
|
|
||||||
nationalNumber="110"
|
|
||||||
error="invalid_phone"
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(queryByText('invalid_phone')).not.toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('render input clear', () => {
|
|
||||||
const { container } = render(
|
|
||||||
<PhoneInput name="PhoneInput" nationalNumber="911" onChange={onChange} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const inputField = container.querySelector('input');
|
|
||||||
|
|
||||||
if (inputField) {
|
|
||||||
fireEvent.focus(inputField);
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearButton = container.querySelector('svg');
|
|
||||||
expect(clearButton).not.toBeNull();
|
|
||||||
|
|
||||||
if (clearButton) {
|
|
||||||
fireEvent.mouseDown(clearButton);
|
|
||||||
expect(onChange).toBeCalledWith({ nationalNumber: '' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,112 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import { useState, useMemo, useRef } from 'react';
|
|
||||||
|
|
||||||
import DownArrowIcon from '@/assets/icons/arrow-down.svg';
|
|
||||||
import ClearIcon from '@/assets/icons/clear-icon.svg';
|
|
||||||
import type { ErrorType } from '@/components/ErrorMessage';
|
|
||||||
import ErrorMessage from '@/components/ErrorMessage';
|
|
||||||
import type { CountryCallingCode, CountryMetaData } from '@/hooks/use-phone-number';
|
|
||||||
|
|
||||||
import * as styles from './index.module.scss';
|
|
||||||
import * as phoneInputStyles from './phoneInput.module.scss';
|
|
||||||
|
|
||||||
type Value = { countryCallingCode?: CountryCallingCode; nationalNumber?: string };
|
|
||||||
|
|
||||||
export type Props = {
|
|
||||||
name: string;
|
|
||||||
// eslint-disable-next-line react/boolean-prop-naming
|
|
||||||
autoFocus?: boolean;
|
|
||||||
className?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
countryCallingCode?: CountryCallingCode;
|
|
||||||
nationalNumber: string;
|
|
||||||
countryList?: CountryMetaData[];
|
|
||||||
error?: ErrorType;
|
|
||||||
onChange: (value: Value) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PhoneInput = ({
|
|
||||||
name,
|
|
||||||
autoFocus,
|
|
||||||
className,
|
|
||||||
placeholder,
|
|
||||||
countryCallingCode,
|
|
||||||
nationalNumber,
|
|
||||||
countryList,
|
|
||||||
error,
|
|
||||||
onChange,
|
|
||||||
}: Props) => {
|
|
||||||
const [onFocus, setOnFocus] = useState(false);
|
|
||||||
const inputReference = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const countrySelector = useMemo(() => {
|
|
||||||
if (countryCallingCode === undefined || !countryList?.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={phoneInputStyles.countryCodeSelector}>
|
|
||||||
<span>{`+${countryCallingCode}`}</span>
|
|
||||||
<DownArrowIcon />
|
|
||||||
<select
|
|
||||||
autoComplete="tel-country-code"
|
|
||||||
onChange={({ target: { value } }) => {
|
|
||||||
onChange({ countryCallingCode: value });
|
|
||||||
|
|
||||||
// Auto Focus to the input
|
|
||||||
if (inputReference.current) {
|
|
||||||
inputReference.current.focus();
|
|
||||||
const { length } = inputReference.current.value;
|
|
||||||
inputReference.current.setSelectionRange(length, length);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{countryList.map(({ countryCallingCode, countryCode }) => (
|
|
||||||
<option key={countryCode} value={countryCallingCode}>
|
|
||||||
{`+${countryCallingCode}`}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}, [countryCallingCode, countryList, onChange]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<div className={classNames(styles.wrapper, error && styles.error)}>
|
|
||||||
{countrySelector}
|
|
||||||
<input
|
|
||||||
ref={inputReference}
|
|
||||||
type="tel"
|
|
||||||
inputMode="tel"
|
|
||||||
autoComplete="tel-national"
|
|
||||||
autoFocus={autoFocus}
|
|
||||||
name={name}
|
|
||||||
placeholder={placeholder}
|
|
||||||
value={nationalNumber}
|
|
||||||
onFocus={() => {
|
|
||||||
setOnFocus(true);
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
setOnFocus(false);
|
|
||||||
}}
|
|
||||||
onChange={({ target: { value } }) => {
|
|
||||||
onChange({ nationalNumber: value.replace(/\D/g, '') });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{nationalNumber && onFocus && (
|
|
||||||
<ClearIcon
|
|
||||||
className={styles.actionButton}
|
|
||||||
onMouseDown={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
onChange({ nationalNumber: '' });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{error && <ErrorMessage error={error} className={styles.errorMessage} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PhoneInput;
|
|
|
@ -1,91 +0,0 @@
|
||||||
@use '@/scss/underscore' as _;
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
position: relative;
|
|
||||||
@include _.flex-row;
|
|
||||||
border: _.border(var(--color-line-border));
|
|
||||||
border-radius: var(--radius);
|
|
||||||
// fix in safari input field line-height issue
|
|
||||||
height: 44px;
|
|
||||||
overflow: hidden;
|
|
||||||
transition-property: outline, border;
|
|
||||||
transition-timing-function: ease-in-out;
|
|
||||||
transition-duration: 0.2s;
|
|
||||||
|
|
||||||
.actionButton {
|
|
||||||
position: absolute;
|
|
||||||
right: _.unit(4);
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
display: none;
|
|
||||||
color: var(--color-type-secondary);
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
padding: 0 _.unit(4);
|
|
||||||
flex: 1;
|
|
||||||
background: none;
|
|
||||||
caret-color: var(--color-brand-default);
|
|
||||||
font: var(--font-body-1);
|
|
||||||
color: var(--color-type-primary);
|
|
||||||
align-self: stretch;
|
|
||||||
|
|
||||||
&:not(:first-child) {
|
|
||||||
padding-left: _.unit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--color-type-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-within {
|
|
||||||
border: _.border(var(--color-brand-default));
|
|
||||||
|
|
||||||
.actionButton {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
padding-right: calc(24px + _.unit(4));
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.error {
|
|
||||||
border: _.border(var(--color-danger-default));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorMessage {
|
|
||||||
margin-left: _.unit(0.5);
|
|
||||||
margin-top: _.unit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(body.desktop) {
|
|
||||||
.wrapper {
|
|
||||||
border-radius: 6px;
|
|
||||||
outline: 3px solid transparent;
|
|
||||||
|
|
||||||
input {
|
|
||||||
font: var(--font-body-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionButton {
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.error {
|
|
||||||
border: _.border(var(--color-danger-default));
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-within {
|
|
||||||
border: _.border(var(--color-brand-default));
|
|
||||||
outline-color: var(--color-overlay-brand-focused);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
import { render, fireEvent } from '@testing-library/react';
|
|
||||||
|
|
||||||
import Input from '.';
|
|
||||||
|
|
||||||
describe('Input Field UI Component', () => {
|
|
||||||
const text = 'foo';
|
|
||||||
const onChange = jest.fn();
|
|
||||||
const onClear = jest.fn();
|
|
||||||
|
|
||||||
test('render plain text input with value', () => {
|
|
||||||
const { container } = render(
|
|
||||||
<Input
|
|
||||||
name="foo"
|
|
||||||
value={text}
|
|
||||||
onChange={({ target }) => {
|
|
||||||
if (target instanceof HTMLInputElement) {
|
|
||||||
onChange(target.value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const inputEle = container.querySelector('input');
|
|
||||||
expect(inputEle).not.toBeNull();
|
|
||||||
expect(inputEle?.value).toEqual(text);
|
|
||||||
|
|
||||||
if (inputEle) {
|
|
||||||
fireEvent.change(inputEle, { target: { value: 'update' } });
|
|
||||||
expect(onChange).toBeCalledWith('update');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('render error message', () => {
|
|
||||||
const errorCode = 'invalid_email';
|
|
||||||
const { queryByText } = render(<Input error={errorCode} />);
|
|
||||||
expect(queryByText(errorCode)).not.toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('click on clear button', () => {
|
|
||||||
const { container } = render(
|
|
||||||
<Input name="foo" value={text} onChange={onChange} onClear={onClear} />
|
|
||||||
);
|
|
||||||
const inputField = container.querySelector('input');
|
|
||||||
|
|
||||||
if (inputField) {
|
|
||||||
fireEvent.focus(inputField);
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearIcon = container.querySelector('svg');
|
|
||||||
expect(clearIcon).not.toBeNull();
|
|
||||||
|
|
||||||
if (clearIcon) {
|
|
||||||
fireEvent.mouseDown(clearIcon);
|
|
||||||
expect(onClear).toBeCalledWith();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,48 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import type { HTMLProps } from 'react';
|
|
||||||
|
|
||||||
import ClearIcon from '@/assets/icons/clear-icon.svg';
|
|
||||||
import type { ErrorType } from '@/components/ErrorMessage';
|
|
||||||
import ErrorMessage from '@/components/ErrorMessage';
|
|
||||||
|
|
||||||
import * as styles from './index.module.scss';
|
|
||||||
|
|
||||||
export { default as PasswordInput } from './PasswordInput';
|
|
||||||
export { default as PhoneInput } from './PhoneInput';
|
|
||||||
|
|
||||||
export type Props = HTMLProps<HTMLInputElement> & {
|
|
||||||
className?: string;
|
|
||||||
error?: ErrorType;
|
|
||||||
onClear?: () => void;
|
|
||||||
isErrorStyling?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Input = ({
|
|
||||||
className,
|
|
||||||
type = 'text',
|
|
||||||
value,
|
|
||||||
error,
|
|
||||||
isErrorStyling = true,
|
|
||||||
onClear,
|
|
||||||
...rest
|
|
||||||
}: Props) => {
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<div className={classNames(styles.wrapper, error && isErrorStyling && styles.error)}>
|
|
||||||
<input type={type} value={value} {...rest} />
|
|
||||||
{value && onClear && (
|
|
||||||
<ClearIcon
|
|
||||||
className={styles.actionButton}
|
|
||||||
onMouseDown={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
onClear();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{error && <ErrorMessage error={error} className={styles.errorMessage} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Input;
|
|
|
@ -1,62 +0,0 @@
|
||||||
@use '@/scss/underscore' as _;
|
|
||||||
|
|
||||||
.countryCodeSelector {
|
|
||||||
color: var(--color-type-primary);
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
width: auto;
|
|
||||||
@include _.flex-row;
|
|
||||||
position: relative;
|
|
||||||
margin-right: _.unit(1);
|
|
||||||
margin-left: _.unit(4);
|
|
||||||
|
|
||||||
> select {
|
|
||||||
appearance: none;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
background: none;
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(body.mobile) {
|
|
||||||
.countryCodeSelector {
|
|
||||||
font: var(--font-label-1);
|
|
||||||
|
|
||||||
> select option {
|
|
||||||
font: var(--font-label-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
> svg {
|
|
||||||
color: var(--color-type-secondary);
|
|
||||||
margin-left: _.unit(1);
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
+ input {
|
|
||||||
// hot fix unknown android bug of input width
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(body.desktop) {
|
|
||||||
.countryCodeSelector {
|
|
||||||
font: var(--font-body-2);
|
|
||||||
|
|
||||||
> select option {
|
|
||||||
font: var(--font-body-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
> svg {
|
|
||||||
color: var(--color-type-secondary);
|
|
||||||
margin-left: _.unit(2);
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
/**
|
|
||||||
* Provide PhoneNumber Format support
|
|
||||||
* Reference [libphonenumber-js](https://gitlab.com/catamphetamine/libphonenumber-js)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { CountryCallingCode, CountryCode } from 'libphonenumber-js/mobile';
|
|
||||||
import { parsePhoneNumberWithError, ParseError } from 'libphonenumber-js/mobile';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getDefaultCountryCallingCode,
|
|
||||||
getCountryList,
|
|
||||||
parseE164Number,
|
|
||||||
} from '@/utils/country-code';
|
|
||||||
|
|
||||||
export type { CountryCallingCode } from 'libphonenumber-js/mobile';
|
|
||||||
|
|
||||||
export type CountryMetaData = {
|
|
||||||
countryCode: CountryCode;
|
|
||||||
countryCallingCode: CountryCallingCode;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PhoneNumberData = {
|
|
||||||
countryCallingCode: string;
|
|
||||||
nationalNumber: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isValidPhoneNumber = (value: string): boolean => {
|
|
||||||
try {
|
|
||||||
const phoneNumber = parsePhoneNumberWithError(parseE164Number(value));
|
|
||||||
|
|
||||||
return phoneNumber.isValid();
|
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof ParseError) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const usePhoneNumber = () => {
|
|
||||||
const [phoneNumber, setPhoneNumber] = useState<PhoneNumberData>({
|
|
||||||
countryCallingCode: getDefaultCountryCallingCode(),
|
|
||||||
nationalNumber: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
countryList: getCountryList(),
|
|
||||||
phoneNumber,
|
|
||||||
setPhoneNumber,
|
|
||||||
isValidPhoneNumber,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default usePhoneNumber;
|
|
Loading…
Reference in a new issue