0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-10 22:22:45 -05:00

refactor(ui): main flow mobile design review fix part 1 (#717)

* fix(ui): use description.or for all non-social methods divider

use description.or for all non-social methods divider

* fix(ui): should not validate format in sign-in form
should not validate format in sign-in form

* refactor(ui): add clear-icon and refine component import path

add clear-icon and refin component import path

* fix(ui): remove passcode input error border

remove passcode input error border

* refactor(ui): hide error border of confirm passcode

hide error border of confirm passcode

* fix(ui): fix i18n key

fix i18n key

* refactor(ui): show clear icon for password in create-account form

show clear icon for password in create-account form

* fix(ui): update passwordless confirm modal confirm button text

update passwordless confirm modal confirm button text

* refactor(ui): divider style update

divider style update

* fix(ui):  always show social links expand button

toggle social link

* fix(ui): extract mobile width style

extract mobile width style

* fix(ui): fix create account link

fix create account link

* fix(ui): cr fix
This commit is contained in:
simeng-li 2022-05-05 13:44:22 +08:00 committed by GitHub
parent 39bf3ccd8a
commit 92e8ed199d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 144 additions and 84 deletions

View file

@ -0,0 +1,3 @@
<svg width="18" height="18" id="clear" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 18C13.9706 18 18 13.9706 18 9C18 4.02944 13.9706 0 9 0C4.02944 0 0 4.02944 0 9C0 13.9706 4.02944 18 9 18ZM5.46447 6.87868C5.07394 6.48816 5.07394 5.85499 5.46447 5.46447C5.85499 5.07394 6.48816 5.07394 6.87868 5.46447L9 7.58579L11.1213 5.46447C11.5118 5.07394 12.145 5.07394 12.5355 5.46447C12.9261 5.85499 12.9261 6.48815 12.5355 6.87868L10.4142 9L12.5355 11.1213C12.9261 11.5118 12.9261 12.145 12.5355 12.5355C12.145 12.9261 11.5118 12.9261 11.1213 12.5355L9 10.4142L6.87868 12.5355C6.48816 12.9261 5.85499 12.9261 5.46447 12.5355C5.07394 12.145 5.07394 11.5118 5.46447 11.1213L7.58579 9L5.46447 6.87868Z" fill="#747778"/>
</svg>

After

Width:  |  Height:  |  Size: 793 B

View file

@ -5,8 +5,7 @@ $font-family: 'PingFang SC', 'SF Pro Display', 'Siyuan Heiti', 'Roboto';
$font-family-small: 'PingFang SC', 'SF Pro Text', 'Siyuan Heiti', 'Roboto';
.content {
position: absolute;
inset: 0;
min-height: 100vh;
background: var(--color-base);
color: var(--color-text);
font: var(--font-body);

View file

@ -5,8 +5,7 @@
@include _.flex-row;
font: var(--font-body);
color: var(--color-caption);
margin: _.unit(4) 0;
width: 100%;
margin: _.unit(5) 0;
.line {
flex: 1;

View file

@ -2,7 +2,7 @@ import classNames from 'classnames';
import React, { ReactNode } from 'react';
import ReactModal from 'react-modal';
import { ClearIcon } from '@/components/Icons';
import { CloseIcon } from '@/components/Icons';
import * as modalStyles from '../../scss/modal.module.scss';
import * as styles from './index.module.scss';
@ -29,7 +29,7 @@ const Drawer = ({ className, isOpen = false, children, onClose }: Props) => {
>
<div className={styles.container}>
<div className={styles.header}>
<ClearIcon className={styles.closeIcon} onClick={onClose} />
<CloseIcon className={styles.closeIcon} onClick={onClose} />
</div>
{children}
</div>

View file

@ -1,10 +1,10 @@
import React, { SVGProps } from 'react';
import CloseIcon from '@/assets/icons/close-icon.svg';
import IconClear from '@/assets/icons/clear-icon.svg';
const ClearIcon = (props: SVGProps<SVGSVGElement>) => (
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
<use href={`${CloseIcon}#close-icon`} />
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" {...props}>
<use href={`${IconClear}#clear`} />
</svg>
);

View file

@ -0,0 +1,11 @@
import React, { SVGProps } from 'react';
import IconClose from '@/assets/icons/close-icon.svg';
const CloseIcon = (props: SVGProps<SVGSVGElement>) => (
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
<use href={`${IconClose}#close-icon`} />
</svg>
);
export default CloseIcon;

View file

@ -1,3 +1,4 @@
export { default as CloseIcon } from './CloseIcon';
export { default as ClearIcon } from './ClearIcon';
export { default as PrivacyIcon } from './PrivacyIcon';
export { default as DownArrowIcon } from './DownArrowIcon';

View file

@ -1,25 +1,17 @@
import classNames from 'classnames';
import React, { useState, useRef, HTMLProps } from 'react';
import ErrorMessage, { ErrorType } from '../ErrorMessage';
import { PrivacyIcon } from '../Icons';
import ErrorMessage, { ErrorType } from '@/components/ErrorMessage';
import { PrivacyIcon } from '@/components/Icons';
import * as styles from './index.module.scss';
export type Props = Omit<HTMLProps<HTMLInputElement>, 'type'> & {
className?: string;
error?: ErrorType;
forceHidden?: boolean;
};
const PasswordInput = ({
className,
value,
error,
forceHidden = false,
onFocus,
onBlur,
...rest
}: Props) => {
const PasswordInput = ({ className, value, error, onFocus, onBlur, ...rest }: Props) => {
// Toggle the password visibility
const [type, setType] = useState('password');
const [onInputFocus, setOnInputFocus] = useState(false);
@ -45,7 +37,7 @@ const PasswordInput = ({
}}
{...rest}
/>
{!forceHidden && value && onInputFocus && (
{value && onInputFocus && (
<PrivacyIcon
className={styles.actionButton}
type={iconType}

View file

@ -1,10 +1,10 @@
import classNames from 'classnames';
import React, { useState, useMemo, useRef } from 'react';
import ErrorMessage, { ErrorType } from '@/components/ErrorMessage';
import { ClearIcon, DownArrowIcon } from '@/components/Icons';
import { CountryCallingCode, CountryMetaData } from '@/hooks/use-phone-number';
import ErrorMessage, { ErrorType } from '../ErrorMessage';
import { ClearIcon, DownArrowIcon } from '../Icons';
import * as styles from './index.module.scss';
import * as phoneInputStyles from './phoneInput.module.scss';

View file

@ -1,8 +1,9 @@
import classNames from 'classnames';
import React, { useState, HTMLProps } from 'react';
import ErrorMessage, { ErrorType } from '../ErrorMessage';
import { ClearIcon } from '../Icons';
import ErrorMessage, { ErrorType } from '@/components/ErrorMessage';
import { ClearIcon } from '@/components/Icons';
import * as styles from './index.module.scss';
export type Props = HTMLProps<HTMLInputElement> & {

View file

@ -1,6 +1,7 @@
import React from 'react';
import { LoadingIcon } from '../Icons';
import { LoadingIcon } from '@/components/Icons';
import * as styles from './index.module.scss';
const LoadingLayer = () => (

View file

@ -1,7 +1,8 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { NavArrowIcon } from '../Icons';
import { NavArrowIcon } from '@/components/Icons';
import * as styles from './index.module.scss';
type Props = {

View file

@ -2,9 +2,8 @@
.passcode {
@include _.flex-row;
@include _.mobile-container-width;
justify-content: space-between;
width: 100%;
max-width: 360px;
margin: 0 auto;
input {
@ -28,10 +27,6 @@
border: _.border(var(--color-primary));
}
&.error {
border: _.border(var(--color-error));
}
&::placeholder {
color: var(--color-caption);
}

View file

@ -9,7 +9,8 @@ import React, {
ClipboardEventHandler,
} from 'react';
import ErrorMessage from '../ErrorMessage';
import ErrorMessage from '@/components/ErrorMessage';
import * as styles from './index.module.scss';
export const defaultLength = 6;
@ -199,7 +200,6 @@ const Passcode = ({ name, className, value, length = defaultLength, error, onCha
name={`${name}_${index}`}
data-id={index}
value={codes[index]}
className={error ? styles.error : undefined}
type="text"
inputMode="numeric"
maxLength={2} // Allow overwrite input

View file

@ -2,8 +2,8 @@ import React, { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import reactStringReplace from 'react-string-replace';
import ConfirmModal from '../ConfirmModal';
import TextLink from '../TextLink';
import ConfirmModal from '@/components/ConfirmModal';
import TextLink from '@/components/TextLink';
type Props = {
isOpen?: boolean;

View file

@ -1,10 +1,9 @@
@use '@/scss/underscore' as _;
.form {
width: 100%;
max-width: 360px;
margin: 0 auto;
@include _.mobile-container-width;
@include _.flex-column;
margin: 0 auto;
> * {
width: 100%;
@ -14,6 +13,10 @@
margin-bottom: _.unit(4);
}
.confirmPassword > * {
border: _.border();
}
.terms {
margin: _.unit(8) 0 _.unit(4);
}

View file

@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next';
import { register } from '@/apis/register';
import Button from '@/components/Button';
import Input from '@/components/Input';
import PasswordInput from '@/components/Input/PasswordInput';
import TermsOfUse from '@/containers/TermsOfUse';
import useApi, { ErrorHandlers } from '@/hooks/use-api';
import useForm from '@/hooks/use-form';
@ -89,23 +88,29 @@ const CreateAccount = ({ className }: Props) => {
setFieldValue((state) => ({ ...state, username: '' }));
}}
/>
<PasswordInput
forceHidden
<Input
className={styles.inputField}
name="password"
type="password"
autoComplete="current-password"
placeholder={t('input.password')}
{...fieldRegister('password', passwordValidation)}
onClear={() => {
setFieldValue((state) => ({ ...state, password: '' }));
}}
/>
<PasswordInput
forceHidden
className={styles.inputField}
<Input
className={classNames(styles.inputField, styles.confirmPassword)}
name="confirm_password"
type="password"
autoComplete="current-password"
placeholder={t('input.confirm_password')}
{...fieldRegister('confirmPassword', (confirmPassword) =>
confirmPasswordValidation(fieldValue.password, confirmPassword)
)}
onClear={() => {
setFieldValue((state) => ({ ...state, confirmPassword: '' }));
}}
/>
<TermsOfUse className={styles.terms} />

View file

@ -1,8 +1,7 @@
@use '@/scss/underscore' as _;
.form {
width: 100%;
max-width: 360px;
@include _.mobile-container-width;
margin: 0 auto;
@include _.flex-column;

View file

@ -1,10 +1,9 @@
@use '@/scss/underscore' as _;
.form {
width: 100%;
max-width: 360px;
margin: 0 auto;
@include _.flex-column;
@include _.mobile-container-width;
> * {
width: 100%;

View file

@ -44,6 +44,7 @@ const PasswordlessConfirmModal = ({ className, isOpen, type, method, value, onCl
<ConfirmModal
className={className}
isOpen={isOpen}
confirmText={type === 'sign-in' ? 'action.sign_in' : 'action.continue'}
onClose={onClose}
onConfirm={onConfirmHandler}
>

View file

@ -2,10 +2,9 @@
.container {
width: 100%;
max-width: 360px;
margin: 0 auto;
@include _.flex-column;
@include _.mobile-container-width;
> * {
width: 100%;

View file

@ -19,15 +19,15 @@ const PrimarySocialSignIn = ({ className, isPopup = false, onSocialSignInCallbac
const [showAll, setShowAll] = useState(false);
const { invokeSocialSignIn, socialConnectors } = useSocial({ onSocialSignInCallback });
const isOverSize = socialConnectors.length > defaultSize;
const displayAll = showAll || isPopup || !isOverSize;
const fullDisplay = isPopup || !isOverSize;
const displayConnectors = useMemo(() => {
if (displayAll) {
if (fullDisplay || showAll) {
return socialConnectors;
}
return socialConnectors.slice(0, defaultSize);
}, [socialConnectors, displayAll]);
}, [fullDisplay, showAll, socialConnectors]);
return (
<div className={classNames(styles.socialLinkList, className)}>
@ -41,10 +41,11 @@ const PrimarySocialSignIn = ({ className, isPopup = false, onSocialSignInCallbac
}}
/>
))}
{!displayAll && (
{!fullDisplay && (
<ExpandMoreIcon
className={classNames(styles.expandIcon, showAll && styles.expanded)}
onClick={() => {
setShowAll(true);
setShowAll(!showAll);
}}
/>
)}

View file

@ -1,11 +1,10 @@
@use '@/scss/underscore' as _;
.socialIconList {
width: 100%;
max-width: 360px;
margin: 0 auto;
@include _.flex-row;
@include _.mobile-container-width;
justify-content: center;
margin: 0 auto;
.socialButton {
margin-right: _.unit(8);
@ -21,12 +20,20 @@
}
.socialLinkList {
width: 100%;
max-width: 360px;
margin: 0 auto;
@include _.flex-column;
@include _.mobile-container-width;
.socialLinkButton {
margin-bottom: _.unit(4);
}
.expandIcon {
width: 20px;
height: 20px;
&.expanded {
transform: rotate(180deg);
}
}
}

View file

@ -1,10 +1,9 @@
@use '@/scss/underscore' as _;
.form {
width: 100%;
max-width: 360px;
margin: 0 auto;
@include _.flex-column;
@include _.mobile-container-width;
> * {
width: 100%;

View file

@ -13,7 +13,7 @@ import useForm from '@/hooks/use-form';
import useTerms from '@/hooks/use-terms';
import { SearchParameters } from '@/types';
import { getSearchParameters } from '@/utils';
import { usernameValidation, passwordValidation } from '@/utils/field-validations';
import { requiredValidation } from '@/utils/field-validations';
import * as styles from './index.module.scss';
@ -81,7 +81,7 @@ const UsernameSignin = ({ className }: Props) => {
name="username"
autoComplete="username"
placeholder={t('input.username')}
{...register('username', usernameValidation)}
{...register('username', (value) => requiredValidation('username', value))}
onClear={() => {
setFieldValue((state) => ({ ...state, username: '' }));
}}
@ -91,7 +91,7 @@ const UsernameSignin = ({ className }: Props) => {
name="password"
autoComplete="current-password"
placeholder={t('input.password')}
{...register('password', passwordValidation)}
{...register('password', (value) => requiredValidation('password', value))}
/>
{responseErrorMessage && <ErrorMessage>{responseErrorMessage}</ErrorMessage>}
<TermsOfUse className={styles.terms} />

View file

@ -16,5 +16,5 @@
}
.button {
max-width: 360px;
@include _.mobile-container-width;
}

View file

@ -3,9 +3,12 @@
.wrapper {
position: relative;
padding: _.unit(8) _.unit(5);
min-height: 100vh;
@include _.flex-column;
justify-content: flex-start;
.header {
margin: _.unit(8);
margin-bottom: _.unit(12);
}
@ -14,7 +17,7 @@
}
.divider {
margin-top: _.unit(4);
@include _.mobile-container-width;
}
.primarySignIn {
@ -26,11 +29,14 @@
}
.otherMethodsLink {
margin-top: _.unit(1);
margin-bottom: _.unit(1);
}
.createAccount {
position: fixed;
bottom: _.unit(12);
margin-top: _.unit(8);
}
.placeHolder {
flex: 1;
}
}

View file

@ -3,11 +3,10 @@ import classNames from 'classnames';
import React, { useContext } from 'react';
import BrandingHeader from '@/components/BrandingHeader';
import TextLink from '@/components/TextLink';
import { PageContext } from '@/hooks/use-page-context';
import * as styles from './index.module.scss';
import { PrimarySection, SecondarySection } from './registry';
import { PrimarySection, SecondarySection, CreateAccoutnLink } from './registry';
const SignIn = () => {
const { experienceSettings } = useContext(PageContext);
@ -25,12 +24,7 @@ const SignIn = () => {
primarySignInMethod={experienceSettings?.primarySignInMethod}
secondarySignInMethods={experienceSettings?.secondarySignInMethods}
/>
<TextLink
className={styles.createAccount}
type="secondary"
href="/register"
text="action.create_account"
/>
<CreateAccoutnLink primarySignInMethod={experienceSettings?.primarySignInMethod} />
</div>
);
};

View file

@ -1,6 +1,7 @@
import React from 'react';
import Divider from '@/components/Divider';
import TextLink from '@/components/TextLink';
import { EmailPasswordless, PhonePasswordless } from '@/containers/Passwordless';
import SignInMethodsLink from '@/containers/SignInMethodsLink';
import { PrimarySocialSignIn, SecondarySocialSignIn } from '@/containers/SocialSignIn';
@ -49,20 +50,49 @@ export const SecondarySection = ({
return (
<>
<Divider label="description.continue_with" className={styles.divider} />
<SignInMethodsLink signInMethods={localMethods} className={styles.otherMethodsLink} />
<SignInMethodsLink signInMethods={localMethods} />
</>
);
}
return (
<>
<SignInMethodsLink signInMethods={localMethods} template="sign_in_with" />
<SignInMethodsLink
signInMethods={localMethods}
template="sign_in_with"
className={styles.otherMethodsLink}
/>
{secondarySignInMethods.includes('social') && (
<>
<Divider label="description.continue_with" className={styles.divider} />
<Divider label="description.or" className={styles.divider} />
<SecondarySocialSignIn />
</>
)}
</>
);
};
export const CreateAccoutnLink = ({
primarySignInMethod,
}: {
primarySignInMethod?: SignInMethod;
}) => {
switch (primarySignInMethod) {
case 'username':
case 'email':
case 'sms':
return (
<>
<div className={styles.placeHolder} />
<TextLink
className={styles.createAccount}
type="secondary"
href={`/register/${primarySignInMethod}`}
text="action.create_account"
/>
</>
);
default:
return null;
}
};

View file

@ -29,6 +29,11 @@
color: var(--color-text);
}
@mixin mobile-container-width {
width: 100%;
max-width: 360px;
}
@function border($color: transparent, $width: 1) {
@return #{$width}px solid #{$color};
}

View file

@ -3,6 +3,15 @@ import { ErrorType } from '@/components/ErrorMessage';
const usernameRegex = /^[A-Z_a-z-][\w-]*$/;
const emailRegex = /^\S+@\S+\.\S+$/;
export const requiredValidation = (
type: 'username' | 'password',
value: string
): ErrorType | undefined => {
if (!value) {
return type === 'username' ? 'username_required' : 'password_required';
}
};
export const usernameValidation = (username: string): ErrorType | undefined => {
if (!username) {
return 'username_required';