0
Fork 0
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:
simeng-li 2023-02-16 10:16:52 +08:00 committed by GitHub
commit 43ecf01ce8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 0 additions and 642 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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();
}
});
});

View file

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

View file

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

View file

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