diff --git a/querybook/webapp/components/UnauthPage/LoginForm.tsx b/querybook/webapp/components/UnauthPage/LoginForm.tsx index 583981709..950e78141 100644 --- a/querybook/webapp/components/UnauthPage/LoginForm.tsx +++ b/querybook/webapp/components/UnauthPage/LoginForm.tsx @@ -1,9 +1,8 @@ import React from 'react'; -import { Formik, Form, Field } from 'formik'; +import { Formik, Form } from 'formik'; import ds from 'lib/datasource'; import { Button } from 'ui/Button/Button'; -import { FormField } from 'ui/Form/FormField'; import { Message } from 'ui/Message/Message'; import { FormWrapper } from 'ui/Form/FormWrapper'; import { SimpleField } from 'ui/FormikField/SimpleField'; @@ -39,9 +38,11 @@ export const LoginForm: React.FunctionComponent = ({ ); const passwordField = ( - - - + ); const errorMessageDOM = errorMessage && ( diff --git a/querybook/webapp/components/UnauthPage/SignupForm.tsx b/querybook/webapp/components/UnauthPage/SignupForm.tsx index db8b39c62..1d7ce2943 100644 --- a/querybook/webapp/components/UnauthPage/SignupForm.tsx +++ b/querybook/webapp/components/UnauthPage/SignupForm.tsx @@ -7,6 +7,7 @@ import { FormField } from 'ui/Form/FormField'; import { Message } from 'ui/Message/Message'; import { Button } from 'ui/Button/Button'; import { FormWrapper } from 'ui/Form/FormWrapper'; +import { SimpleField } from 'ui/FormikField/SimpleField'; export interface ISignupFormProps { onSuccessLogin: () => any; @@ -62,12 +63,7 @@ export const SignupForm: React.FunctionComponent = ({ > {({ handleSubmit, isSubmitting, isValid }) => { const usernameField = ( - } - > - - + ); const emailField = ( @@ -80,21 +76,19 @@ export const SignupForm: React.FunctionComponent = ({ ); const passwordField = ( - } - > - - + ); const password2Field = ( - } - > - - + ); const errorMessageDOM = errorMessage && ( diff --git a/querybook/webapp/ui/DebouncedInput/DebouncedInput.stories.tsx b/querybook/webapp/ui/DebouncedInput/DebouncedInput.stories.tsx index cab79f1a5..42ad150c4 100644 --- a/querybook/webapp/ui/DebouncedInput/DebouncedInput.stories.tsx +++ b/querybook/webapp/ui/DebouncedInput/DebouncedInput.stories.tsx @@ -3,6 +3,7 @@ import React, { useState } from 'react'; import centered from '@storybook/addon-centered/react'; import { DebouncedInput } from './DebouncedInput'; +import { DebouncedPasswordInput } from './DebouncedPasswordInput'; export default { title: 'Form/DebouncedInput', @@ -11,9 +12,13 @@ export default { export const _DebouncedInput = (args) => { const [text, setText] = useState(''); + const { password, ...props } = args; + + const InputComponent = password ? DebouncedPasswordInput : DebouncedInput; + return ( <> - { onChange={setText} value={text} className="mb8" - {...args} + {...props} /> ); }; _DebouncedInput.args = { + password: false, flex: true, transparent: false, autoAdjustWidth: false, diff --git a/querybook/webapp/ui/DebouncedInput/DebouncedInput.tsx b/querybook/webapp/ui/DebouncedInput/DebouncedInput.tsx index 9d1aebbde..cc282fe99 100644 --- a/querybook/webapp/ui/DebouncedInput/DebouncedInput.tsx +++ b/querybook/webapp/ui/DebouncedInput/DebouncedInput.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import classNames from 'classnames'; import { useDebounceState } from 'hooks/redux/useDebounceState'; @@ -22,15 +22,44 @@ export interface IDebouncedInputProps extends IDebouncedInputStylingProps { onChange: (value: string) => any; } +function useAdjustableWidth( + autoAdjustWidth: boolean, + inputRef: React.MutableRefObject, + value: string, + placeholder: string +) { + placeholder = placeholder ?? ''; + + React.useEffect(() => { + const adjustInputSize = () => { + if (inputRef.current) { + inputRef.current.size = Math.max( + 1, + value.length, + placeholder.length + ); + } + }; + + if (autoAdjustWidth) { + adjustInputSize(); + } + }, [autoAdjustWidth, value, inputRef.current, placeholder]); +} + export const DebouncedInput: React.FunctionComponent = ({ debounceTime = 500, debounceMethod = 'debounce', + + // Input value = '', - className = '', autoAdjustWidth = false, inputProps = {}, children, onChange, + + // Styling + className = '', transparent, flex, }) => { @@ -42,31 +71,13 @@ export const DebouncedInput: React.FunctionComponent = ({ method: debounceMethod, } ); - const inputRef = React.useRef(); - - React.useEffect(() => { - const adjustInputSize = () => { - if (inputRef.current) { - const { placeholder = '' } = inputProps; - - inputRef.current.size = Math.max( - 1, - debouncedValue.length, - placeholder.length - ); - } - }; - - if (autoAdjustWidth) { - adjustInputSize(); - } - }, [ + useAdjustableWidth( autoAdjustWidth, + inputRef, debouncedValue, - inputRef.current, - inputProps.placeholder, - ]); + inputProps?.placeholder + ); const onChangeFn = React.useCallback( (event: React.ChangeEvent) => { diff --git a/querybook/webapp/ui/DebouncedInput/DebouncedPasswordInput.scss b/querybook/webapp/ui/DebouncedInput/DebouncedPasswordInput.scss new file mode 100644 index 000000000..add4be571 --- /dev/null +++ b/querybook/webapp/ui/DebouncedInput/DebouncedPasswordInput.scss @@ -0,0 +1,18 @@ +.DebouncedPasswordInput { + position: relative; + .password-eye-icon { + position: absolute; + right: 12px; + top: 50%; + transform: translate(0%, -50%); + opacity: 0; + transition: opacity 0.1s ease-in-out; + background-color: var(--bg-color); + } + + &:hover { + .password-eye-icon { + opacity: 1; + } + } +} diff --git a/querybook/webapp/ui/DebouncedInput/DebouncedPasswordInput.tsx b/querybook/webapp/ui/DebouncedInput/DebouncedPasswordInput.tsx new file mode 100644 index 000000000..545c471d5 --- /dev/null +++ b/querybook/webapp/ui/DebouncedInput/DebouncedPasswordInput.tsx @@ -0,0 +1,45 @@ +import React, { useMemo } from 'react'; +import classNames from 'classnames'; + +import { useToggleState } from 'hooks/useToggleState'; +import { DebouncedInput, IDebouncedInputProps } from './DebouncedInput'; +import { IconButton } from 'ui/Button/IconButton'; +import './DebouncedPasswordInput.scss'; + +export const DebouncedPasswordInput: React.FC = ( + props +) => { + const [revealPassword, , toggleRevealPassword] = useToggleState(false); + + const className = useMemo( + () => + classNames({ + [props.className]: props.className, + DebouncedPasswordInput: true, + }), + [props.className] + ); + + const inputProps = useMemo( + () => ({ + ...props.inputProps, + type: revealPassword ? 'text' : 'password', + }), + [props.inputProps, revealPassword] + ); + + return ( + + + + ); +}; diff --git a/querybook/webapp/ui/FormikField/InputField.tsx b/querybook/webapp/ui/FormikField/InputField.tsx index 3f7bfe733..86f242ff6 100644 --- a/querybook/webapp/ui/FormikField/InputField.tsx +++ b/querybook/webapp/ui/FormikField/InputField.tsx @@ -4,13 +4,16 @@ import { DebouncedInput, IDebouncedInputProps, } from 'ui/DebouncedInput/DebouncedInput'; +import { DebouncedPasswordInput } from 'ui/DebouncedInput/DebouncedPasswordInput'; export interface IInputFieldProps extends Partial { name: string; + inputType?: 'text' | 'password'; } export const InputField: React.FC = ({ name, + inputType = 'text', ...inputProps }) => { const [_, meta, helpers] = useField(name); @@ -18,8 +21,11 @@ export const InputField: React.FC = ({ const { value } = meta; const { setValue } = helpers; + const InputComponent = + inputType === 'text' ? DebouncedInput : DebouncedPasswordInput; + return ( - ({ className: 'input', }; + let InputComponent = DebouncedInput; if (fieldType === 'number') { inputProps['type'] = 'number'; } else if (hidden) { - inputProps['type'] = 'password'; + InputComponent = DebouncedPasswordInput; } if (description) { @@ -74,7 +76,7 @@ function SimpleFormField({ } controlDOM = ( -