0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

feat(ui): add new passwordInputField component

add new passwordInputFieldComponent
This commit is contained in:
simeng-li 2023-02-06 17:04:44 +08:00
parent 4a84162722
commit 26b09b7f54
No known key found for this signature in database
GPG key ID: 14EA7BB1541E8075
9 changed files with 192 additions and 3 deletions

View file

@ -0,0 +1,43 @@
import { render, fireEvent } from '@testing-library/react';
import InputField from './InputField';
describe('InputField Component', () => {
const text = 'foo';
const onChange = jest.fn();
test('render plain text input with value', () => {
const { container } = render(
<InputField
name="foo"
value={text}
onChange={({ target }) => {
if (target instanceof HTMLInputElement) {
onChange(target.value);
}
}}
/>
);
const inputElement = container.querySelector('input');
expect(inputElement).not.toBeNull();
expect(inputElement?.value).toEqual(text);
if (inputElement) {
fireEvent.change(inputElement, { target: { value: 'update' } });
expect(onChange).toBeCalledWith('update');
}
});
test('render error message', () => {
const errorCode = 'invalid_email';
const { queryByText } = render(<InputField error={errorCode} />);
expect(queryByText(errorCode)).not.toBeNull();
});
test('render suffix', () => {
const text = 'clearBtn';
const { queryByText } = render(<InputField suffix={<button>{text}</button>} />);
expect(queryByText(text)).not.toBeNull();
});
});

View file

@ -5,9 +5,9 @@ import { forwardRef, cloneElement } from 'react';
import type { ErrorType } from '@/components/ErrorMessage';
import ErrorMessage from '@/components/ErrorMessage';
import * as styles from './index.module.scss';
import * as styles from './InputField.module.scss';
type Props = HTMLProps<HTMLInputElement> & {
export type Props = HTMLProps<HTMLInputElement> & {
className?: string;
error?: ErrorType;
isDanger?: boolean;

View file

@ -0,0 +1,59 @@
import { render, fireEvent } from '@testing-library/react';
import PasswordInputField from './PasswordInputField';
describe('Input Field UI Component', () => {
const text = 'foo';
const onChange = jest.fn();
test('render password input', () => {
const { container } = render(
<PasswordInputField
name="foo"
value={text}
onChange={({ target }) => {
if (target instanceof HTMLInputElement) {
onChange(target.value);
}
}}
/>
);
const inputElement = container.querySelector('input');
expect(inputElement?.value).toEqual(text);
expect(inputElement?.type).toEqual('password');
if (inputElement) {
fireEvent.change(inputElement, { target: { value: 'update' } });
expect(onChange).toBeCalledWith('update');
}
});
test('render error message', () => {
const errorCode = 'username_password_mismatch';
const { queryByText } = render(<PasswordInputField error={errorCode} />);
expect(queryByText(errorCode)).not.toBeNull();
});
test('click on toggle visibility button', () => {
const { container } = render(
<PasswordInputField name="foo" value={text} onChange={onChange} />
);
const inputElement = container.querySelector('input');
if (!inputElement) {
return;
}
expect(inputElement.type).toEqual('password');
const visibilityButton = container.querySelector('svg');
expect(visibilityButton).not.toBeNull();
if (visibilityButton) {
fireEvent.click(visibilityButton);
expect(inputElement.type).toEqual('text');
}
});
});

View file

@ -0,0 +1,46 @@
import type { Nullable } from '@silverhand/essentials';
import type { Ref } from 'react';
import { forwardRef, useRef, useImperativeHandle } from 'react';
import PasswordHideIcon from '@/assets/icons/password-hide-icon.svg';
import PasswordShowIcon from '@/assets/icons/password-show-icon.svg';
import IconButton from '@/components/Button/IconButton';
import useToggle from '@/hooks/use-toggle';
import useUpdateEffect from '@/hooks/use-update-effect';
import type { Props as InputFieldProps } from './InputField';
import InputField from './InputField';
type Props = Omit<InputFieldProps, 'type' | 'suffix' | 'isSuffixFocusVisible'>;
const PasswordInputField = (props: Props, forwardRef: Ref<Nullable<HTMLInputElement>>) => {
const [showPassword, toggleShowPassword] = useToggle(false);
const innerRef = useRef<HTMLInputElement>(null);
useImperativeHandle(forwardRef, () => innerRef.current);
// Refocus and move cursor to the end of the input field after password visibility is toggled
useUpdateEffect(() => {
if (innerRef.current) {
const { length } = innerRef.current.value;
innerRef.current.focus();
innerRef.current.setSelectionRange(length, length);
}
}, [showPassword]);
return (
<InputField
isSuffixFocusVisible
type={showPassword ? 'text' : 'password'}
suffix={
<IconButton onClick={toggleShowPassword}>
{showPassword ? <PasswordShowIcon /> : <PasswordHideIcon />}
</IconButton>
}
{...props}
ref={innerRef}
/>
);
};
export default forwardRef(PasswordInputField);

View file

@ -0,0 +1,2 @@
export { default as InputField } from './InputField';
export { default as PasswordInputField } from './PasswordInputField';

View file

@ -7,7 +7,7 @@ import ClearIcon from '@/assets/icons/clear-icon.svg';
import Button from '@/components/Button';
import IconButton from '@/components/Button/IconButton';
import ErrorMessage from '@/components/ErrorMessage';
import InputField from '@/components/InputField';
import { InputField } from '@/components/InputFields';
import { passwordErrorWatcher } from '@/utils/form';
import TogglePassword from './TogglePassword';

View file

@ -0,0 +1,13 @@
import { useState } from 'react';
const useToggle = (defaultValue = false) => {
const [value, setValue] = useState<boolean>(defaultValue);
const toggle = () => {
setValue((previous) => !previous);
};
return [value, toggle] as const;
};
export default useToggle;

View file

@ -0,0 +1,26 @@
import type { DependencyList, EffectCallback } from 'react';
import { useEffect, useRef } from 'react';
const useUpdateEffect = (effect: EffectCallback, dependencies: DependencyList | undefined = []) => {
const isMounted = useRef(false);
useEffect(() => {
return () => {
// eslint-disable-next-line @silverhand/fp/no-mutation
isMounted.current = false;
};
}, []);
useEffect(() => {
if (!isMounted.current) {
// eslint-disable-next-line @silverhand/fp/no-mutation
isMounted.current = true;
return;
}
return effect();
}, [effect, ...dependencies]);
};
export default useUpdateEffect;