diff --git a/web/account/log-in-form.react.js b/web/account/log-in-form.react.js index a4fe90655..b9520d575 100644 --- a/web/account/log-in-form.react.js +++ b/web/account/log-in-form.react.js @@ -1,167 +1,166 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { logInActionTypes, logIn } from 'lib/actions/user-actions'; import { useModalContext } from 'lib/components/modal-provider.react'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors'; import { oldValidUsernameRegex, validEmailRegex, } from 'lib/shared/account-utils'; import { type LogInExtraInfo, type LogInStartingPayload, logInActionSources, } from 'lib/types/account-types'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils'; import Button from '../components/button.react'; import LoadingIndicator from '../loading-indicator.react'; import Input from '../modals/input.react'; import { useSelector } from '../redux/redux-utils'; import { webLogInExtraInfoSelector } from '../selectors/account-selectors'; import css from './log-in-form.css'; +import PasswordInput from './password-input.react'; const loadingStatusSelector = createLoadingStatusSelector(logInActionTypes); function LoginForm(): React.Node { const inputDisabled = useSelector(loadingStatusSelector) === 'loading'; const loginExtraInfo = useSelector(webLogInExtraInfoSelector); const callLogIn = useServerCall(logIn); const dispatchActionPromise = useDispatchActionPromise(); const modalContext = useModalContext(); const [username, setUsername] = React.useState(''); const [password, setPassword] = React.useState(''); const [errorMessage, setErrorMessage] = React.useState(''); const usernameInputRef = React.useRef(); React.useEffect(() => { usernameInputRef.current?.focus(); }, []); const onUsernameChange = React.useCallback(e => { invariant(e.target instanceof HTMLInputElement, 'target not input'); setUsername(e.target.value); }, []); const onUsernameBlur = React.useCallback(() => { setUsername(untrimmedUsername => untrimmedUsername.trim()); }, []); const onPasswordChange = React.useCallback(e => { invariant(e.target instanceof HTMLInputElement, 'target not input'); setPassword(e.target.value); }, []); const logInAction = React.useCallback( async (extraInfo: LogInExtraInfo) => { try { const result = await callLogIn({ ...extraInfo, username, password, logInActionSource: logInActionSources.logInFromWebForm, }); modalContext.popModal(); return result; } catch (e) { setUsername(''); setPassword(''); if (e.message === 'invalid_credentials') { setErrorMessage('incorrect username or password'); } else { setErrorMessage('unknown error'); } usernameInputRef.current?.focus(); throw e; } }, [callLogIn, modalContext, password, username], ); const onSubmit = React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); if (username.search(validEmailRegex) > -1) { setUsername(''); setErrorMessage('usernames only, not emails'); usernameInputRef.current?.focus(); return; } else if (username.search(oldValidUsernameRegex) === -1) { setUsername(''); setErrorMessage('alphanumeric usernames only'); usernameInputRef.current?.focus(); return; } const extraInfo = loginExtraInfo(); dispatchActionPromise( logInActionTypes, logInAction(extraInfo), undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); }, [dispatchActionPromise, logInAction, loginExtraInfo, username], ); const loginButtonContent = React.useMemo(() => { if (inputDisabled) { return ; } return 'Log in'; }, [inputDisabled]); return (
Username
Password
-
{errorMessage}
); } export default LoginForm; diff --git a/web/account/password-input.css b/web/account/password-input.css new file mode 100644 index 000000000..80033a178 --- /dev/null +++ b/web/account/password-input.css @@ -0,0 +1,22 @@ +.input { + padding-right: 50px; +} + +.button { + position: absolute; + right: 0; + top: 0; + margin: 7px; + padding: 6px; + border: 0; + border-radius: 50%; + color: var(--fg); +} + +.button:hover { + background-color: var(--show-password-bg-hover); +} + +.wrapper { + position: relative; +} diff --git a/web/account/password-input.react.js b/web/account/password-input.react.js new file mode 100644 index 000000000..8c73db43e --- /dev/null +++ b/web/account/password-input.react.js @@ -0,0 +1,44 @@ +// @flow + +import * as React from 'react'; + +import Button from '../components/button.react'; +import Input, { type BaseInputProps } from '../modals/input.react'; +import SWMansionIcon, { type Icon } from '../SWMansionIcon.react'; +import css from './password-input.css'; + +type PasswordInputProps = BaseInputProps; + +function PasswordInput(props: PasswordInputProps, ref): React.Node { + const [htmlInputType, setHtmlInputType] = React.useState<'password' | 'text'>( + 'password', + ); + + const onToggleShowPassword = React.useCallback(() => { + setHtmlInputType(oldType => (oldType === 'password' ? 'text' : 'password')); + }, []); + + const icon: Icon = htmlInputType === 'password' ? 'eye-open' : 'eye-closed'; + + return ( +
+ + +
+ ); +} + +const ForwardedPasswordInput: React.AbstractComponent< + PasswordInputProps, + HTMLInputElement, +> = React.forwardRef(PasswordInput); + +export default ForwardedPasswordInput; diff --git a/web/modals/input.react.js b/web/modals/input.react.js index de6214c20..c9d6fd314 100644 --- a/web/modals/input.react.js +++ b/web/modals/input.react.js @@ -1,49 +1,63 @@ // @flow +import classNames from 'classnames'; import * as React from 'react'; import css from './input.css'; -type Props = { - +type: string, - +placeholder: string, +export type BaseInputProps = { +value: string, +onChange: (value: SyntheticEvent) => mixed, +onBlur?: (value: SyntheticEvent) => mixed, +disabled?: boolean, +label?: string, +id?: string, + +className?: string, }; -function Input(props: Props, ref): React.Node { - const { label: labelProp, disabled = false, id, ...rest } = props; +export type InputProps = { + ...BaseInputProps, + +type: string, + +placeholder: string, +}; + +function Input(props: InputProps, ref): React.Node { + const { + label: labelProp, + disabled = false, + className = '', + id, + ...rest + } = props; let label; if (labelProp) { label = ( ); } + const inputClassName = classNames(css.input, className); + return ( <> {label} ); } const ForwardedInput: React.AbstractComponent< - Props, + InputProps, HTMLInputElement, -> = React.forwardRef(Input); +> = React.forwardRef(Input); export default ForwardedInput; diff --git a/web/theme.css b/web/theme.css index 73d95aef5..1b39623c8 100644 --- a/web/theme.css +++ b/web/theme.css @@ -1,183 +1,184 @@ :root { /* Never use color values defined here directly in CSS. Add color variables to "Color Theme" below The reason we never use color values defined here directly in CSS is 1. It makes changing themes from light / dark / user generated impossible. 2. Gives the programmer context into the color being used. 3. If our color system changes it's much easier to change color values in one place. Add a color value to the theme below, and then use it in your CSS. naming convention: - bg: background. - fg: foreground. - color: text-color */ --shades-white-100: #ffffff; --shades-white-90: #f5f5f5; --shades-white-80: #ebebeb; --shades-white-70: #e0e0e0; --shades-white-60: #cccccc; --shades-black-100: #0a0a0a; --shades-black-90: #1f1f1f; --shades-black-80: #404040; --shades-black-70: #666666; --shades-black-60: #808080; --violet-dark-100: #7e57c2; --violet-dark-80: #6d49ab; --violet-dark-60: #563894; --violet-dark-40: #44297a; --violet-dark-20: #331f5c; --violet-light-100: #ae94db; --violet-light-80: #b9a4df; --violet-light-60: #d3c6ec; --violet-light-40: #e8e0f5; --violet-light-20: #f3f0fa; --success-light-10: #d5f6e3; --success-light-50: #6cdf9c; --success-primary: #00c853; --success-dark-50: #029841; --success-dark-90: #034920; --error-light-10: #feebe6; --error-light-50: #f9947b; --error-primary: #f53100; --error-dark-50: #b62602; --error-dark-90: #4f1203; --bg: var(--shades-black-100); --fg: var(--shades-white-100); --color-disabled: var(--shades-black-60); --text-input-bg: var(--shades-black-80); --text-input-color: var(--shades-white-60); --text-input-placeholder: var(--shades-white-60); --border: var(--shades-black-80); --error: var(--error-primary); --success: var(--success-dark-50); /* Color Theme */ --btn-bg-filled: var(--violet-dark-100); --btn-bg-outline: var(--shades-black-90); --btn-bg-success: var(--success-dark-50); --btn-bg-danger: var(--error-primary); --btn-bg-disabled: var(--shades-black-80); --btn-disabled-color: var(--shades-black-60); --chat-bg: var(--violet-dark-80); --chat-confirmation-icon: var(--violet-dark-100); --keyserver-selection: var(--violet-dark-60); --thread-selection: var(--violet-light-80); --thread-hover-bg: var(--shades-black-80); --thread-active-bg: var(--shades-black-80); --chat-timestamp-color: var(--shades-black-60); --tool-tip-bg: var(--shades-black-80); --tool-tip-color: var(--shades-white-60); --border-color: var(--shades-black-80); --calendar-chevron: var(--shades-black-60); --calendar-day-bg: var(--shades-black-60); --calendar-day-selected-color: var(--violet-dark-80); --community-bg: var(--shades-black-90); --community-settings-selected: var(--violet-dark-60); --unread-bg: var(--error-primary); --settings-btn-bg: var(--violet-dark-100); --modal-bg: var(--shades-black-90); --modal-fg: var(--shades-white-60); --join-bg: var(--shades-black-90); --help-color: var(--shades-black-60); --breadcrumb-color: var(--shades-white-60); --breadcrumb-color-unread: var(--shades-white-60); --btn-outline-border: var(--shades-black-60); --thread-color-read: var(--shades-black-60); --thread-from-color-read: var(--shades-black-80); --thread-last-message-color-read: var(--shades-black-60); --relationship-button-green: var(--success-dark-50); --relationship-button-red: var(--error-primary); --relationship-button-text: var(--fg); --disconnected-bar-alert-bg: var(--error-dark-50); --disconnected-bar-alert-color: var(--shades-white-100); --disconnected-bar-connecting-bg: var(--shades-white-70); --disconnected-bar-connecting-color: var(--shades-black-100); --permission-color: var(--shades-white-60); --thread-top-bar-color: var(--shades-white-100); --thread-top-bar-menu-color: var(--shades-white-70); --thread-ancestor-keyserver-border: var(--shades-black-70); --thread-ancestor-color-light: var(--shades-white-70); --thread-ancestor-color-dark: var(--shades-black-100); --thread-ancestor-separator-color: var(--shades-white-60); --text-message-default-background: var(--shades-black-80); --message-action-tooltip-bg: var(--shades-black-90); --menu-bg: var(--shades-black-90); --menu-bg-light: var(--shades-black-80); --menu-separator-color: var(--shades-black-80); --menu-color: var(--shades-black-60); --menu-color-light: var(--shades-white-60); --menu-color-hover: var(--shades-white-100); --menu-color-dangerous: var(--error-primary); --menu-color-dangerous-hover: var(--error-light-50); --app-list-icon-read-only-color: var(--shades-black-60); --app-list-icon-enabled-color: var(--success-primary); --app-list-icon-disabled-color: var(--shades-white-80); --account-settings-label: var(--shades-black-60); --account-button-color: var(--violet-dark-100); --chat-thread-list-color-active: var(--shades-white-60); --chat-thread-list-menu-color: var(--shades-white-60); --chat-thread-list-menu-bg: var(--shades-black-80); --chat-thread-list-menu-active-color: var(--shades-white-60); --chat-thread-list-menu-active-bg: var(--shades-black-90); --search-clear-color: var(--shades-white-100); --search-clear-bg: var(--shades-black-70); --search-input-color: var(--shades-white-100); --search-input-placeholder: var(--shades-black-60); --search-icon-color: var(--shades-black-60); --tabs-header-active-color: var(--shades-white-100); --tabs-header-active-border: var(--violet-light-100); --tabs-header-background-color: var(--shades-black-60); --tabs-header-background-border: var(--shades-black-80); --tabs-header-background-color-hover: var(--shades-white-80); --tabs-header-background-border-hover: var(--shades-black-70); --members-modal-member-text: var(--shades-black-60); --members-modal-member-text-hover: var(--shades-white-100); --label-default-bg: var(--violet-dark-80); --label-default-color: var(--shades-white-80); --subchannels-modal-color: var(--shades-black-60); --subchannels-modal-color-hover: var(--shades-white-100); --color-selector-active-bg: var(--shades-black-80); --relationship-modal-color: var(--shades-black-60); --arrow-extension-color: var(--shades-black-60); --modal-close-color: var(--shades-black-60); --modal-close-color-hover: var(--shades-white-100); --add-members-group-header-color: var(--shades-black-60); --add-members-item-color: var(--shades-black-60); --add-members-item-color-hover: var(--shades-white-100); --add-members-item-disabled-color: var(--shades-black-80); --add-members-item-disabled-color-hover: var(--shades-black-60); --add-members-remove-pending-color: var(--error-primary); --add-members-remove-pending-color-hover: var(--error-light-50); --radio-border: var(--shades-black-70); --radio-color: var(--shades-white-60); --notification-settings-option-selected-bg: var(--shades-black-80); --notification-settings-option-title-color: var(--shades-white-90); --notification-settings-option-color: var(--shades-white-60); --notification-settings-option-invalid-color: var(--shades-black-80); --notification-settings-option-invalid-selected-color: var(--shades-black-60); --danger-zone-subheading-color: var(--shades-white-60); --danger-zone-explanation-color: var(--shades-black-60); --thread-creation-search-container-bg: var(--shades-black-90); --thread-creation-close-search-color: var(--shades-black-60); --thread-creation-search-item-bg-hover: var(--shades-black-80); --thread-creation-search-item-info-color: var(--shades-black-60); --chat-message-list-active-border: #5989d6; --sidebars-modal-color: var(--shades-black-60); --sidebars-modal-color-hover: var(--shades-white-100); --inline-sidebar-bg: var(--shades-black-70); --inline-sidebar-bg-hover: var(--shades-black-80); --inline-sidebar-color: var(--fg); --compose-subchannel-header-fg: var(--shades-black-60); --compose-subchannel-header-bg: var(--shades-black-80); --compose-subchannel-label-color: var(--shades-black-60); --compose-subchannel-mark-color: var(--violet-light-100); --enum-option-icon-color: var(--violet-dark-100); + --show-password-bg-hover: var(--shades-black-70); }