0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor(experience): use button loading if possible

This commit is contained in:
Xiao Yijun 2024-07-11 16:28:46 +08:00
parent 6060919a21
commit 942af078ba
No known key found for this signature in database
GPG key ID: 6F648FC1262DB420
29 changed files with 239 additions and 85 deletions

View file

@ -1,18 +1,16 @@
import { useContext } from 'react';
import { Outlet } from 'react-router-dom';
import { useDebouncedLoader } from 'use-debounced-loader';
import PageContext from '@/Providers/PageContextProvider/PageContext';
import LoadingLayer from '@/components/LoadingLayer';
import LoadingMask from '@/components/LoadingMask';
const LoadingLayerProvider = () => {
const { loading } = useContext(PageContext);
const debouncedLoading = useDebouncedLoader(loading, 500);
return (
<>
<Outlet />
{debouncedLoading && <LoadingLayer />}
{loading && <LoadingMask />}
</>
);
};

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.3357 16.8714C11.441 17.4135 11.0865 17.9451 10.5355 17.9821C9.4048 18.0579 8.2669 17.8929 7.19834 17.4934C5.81639 16.9767 4.60425 16.0879 3.69591 14.9253C2.78758 13.7627 2.21844 12.3715 2.05143 10.9056C1.88441 9.43973 2.12602 7.9562 2.74954 6.61905C3.37306 5.28191 4.35421 4.14323 5.5845 3.32891C6.8148 2.5146 8.24632 2.05636 9.7208 2.00487C11.1953 1.95338 12.6553 2.31064 13.9394 3.03715C14.9323 3.59891 15.7901 4.36452 16.4588 5.27942C16.7847 5.72531 16.6054 6.33858 16.1223 6.60633C15.6393 6.87408 15.0366 6.69278 14.6924 6.26086C14.2154 5.66218 13.6262 5.15785 12.9545 4.77787C11.9915 4.23298 10.8965 3.96504 9.7906 4.00366C8.68474 4.04227 7.6111 4.38595 6.68838 4.99669C5.76565 5.60742 5.02979 6.46143 4.56215 7.46429C4.09451 8.46715 3.91331 9.5798 4.03857 10.6792C4.16383 11.7786 4.59069 12.822 5.27194 13.694C5.95319 14.5659 6.8623 15.2325 7.89876 15.62C8.62154 15.8903 9.38663 16.0175 10.1519 15.9981C10.704 15.9841 11.2303 16.3293 11.3357 16.8714Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -21,6 +21,11 @@
line-height: normal;
margin-right: _.unit(2);
}
.loadingIcon {
color: var(--color-white);
animation: rotating 1s steps(60, end) infinite;
}
}
.large {
@ -40,11 +45,16 @@
&:disabled {
background: var(--color-bg-state-disabled);
color: var(--color-type-disable);
pointer-events: none;
}
&:active {
background: var(--color-brand-pressed);
}
&.loading {
background: var(--color-brand-70);
}
}
.secondary {
@ -74,7 +84,7 @@
outline: 3px solid var(--color-overlay-brand-focused);
}
&:not(:disabled):not(:active):hover {
&:not(:disabled):not(:active):not(.loading):hover {
background: var(--color-brand-hover);
}
}
@ -84,8 +94,18 @@
outline: 3px solid var(--color-overlay-neutral-focused);
}
&:not(:disabled):not(:active):hover {
&:not(:disabled):not(:active):not(.loading):hover {
background: var(--color-overlay-neutral-hover);
}
}
}
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View file

@ -2,6 +2,8 @@ import classNames from 'classnames';
import type { TFuncKey } from 'i18next';
import type { HTMLProps } from 'react';
import Ring from '@/assets/icons/ring.svg';
import DynamicT from '../DynamicT';
import * as styles from './index.module.scss';
@ -15,6 +17,7 @@ type BaseProps = Omit<HTMLProps<HTMLButtonElement>, 'type' | 'size' | 'title'> &
readonly isDisabled?: boolean;
readonly className?: string;
readonly onClick?: React.MouseEventHandler;
readonly isLoading?: boolean;
};
type Props = BaseProps & {
@ -31,6 +34,7 @@ const Button = ({
i18nProps,
className,
isDisabled = false,
isLoading = false,
icon,
onClick,
...rest
@ -42,14 +46,20 @@ const Button = ({
styles.button,
styles[type],
styles[size],
isDisabled && styles.isDisabled,
isDisabled && styles.disabled,
isLoading && styles.loading,
className
)}
type={htmlType}
onClick={onClick}
{...rest}
>
{icon && <span className={styles.icon}>{icon}</span>}
{icon && !isLoading && <span className={styles.icon}>{icon}</span>}
{isLoading && (
<span className={classNames(styles.icon, styles.loadingIcon)}>
<Ring />
</span>
)}
<DynamicT forKey={title} interpolation={i18nProps} />
</button>
);

View file

@ -1,12 +1,5 @@
@use '@/scss/underscore' as _;
.overlay {
position: fixed;
inset: 0;
@include _.flex-column;
z-index: 300;
}
.loadingIcon {
color: var(--color-type-primary);
animation: rotating 1s steps(12, end) infinite;

View file

@ -1,14 +1,16 @@
import LoadingMask from '../LoadingMask';
import LoadingIcon from './LoadingIcon';
import * as styles from './index.module.scss';
export { default as LoadingIcon } from './LoadingIcon';
const LoadingLayer = () => (
<div className={styles.overlay}>
<LoadingMask>
<div className={styles.container}>
<LoadingIcon />
</div>
</div>
</LoadingMask>
);
export default LoadingLayer;

View file

@ -0,0 +1,8 @@
@use '@/scss/underscore' as _;
.overlay {
position: fixed;
inset: 0;
@include _.flex-column;
z-index: 300;
}

View file

@ -0,0 +1,13 @@
import { type ReactNode } from 'react';
import * as styles from './index.module.scss';
type Props = {
readonly children?: ReactNode;
};
const LoadingMask = ({ children }: Props) => {
return <div className={styles.overlay}>{children}</div>;
};
export default LoadingMask;

View file

@ -14,7 +14,7 @@ type Props = {
readonly className?: string;
// eslint-disable-next-line react/boolean-prop-naming
readonly autoFocus?: boolean;
readonly onSubmit: (password: string) => void;
readonly onSubmit: (password: string) => Promise<void>;
readonly errorMessage?: string;
readonly clearErrorMessage?: () => void;
};
@ -29,7 +29,7 @@ const Lite = ({ className, autoFocus, onSubmit, errorMessage, clearErrorMessage
const {
register,
handleSubmit,
formState: { errors, isValid },
formState: { errors, isValid, isSubmitting },
} = useForm<FieldState>({
reValidateMode: 'onBlur',
defaultValues: { newPassword: '' },
@ -42,11 +42,11 @@ const Lite = ({ className, autoFocus, onSubmit, errorMessage, clearErrorMessage
}, [clearErrorMessage, isValid]);
const onSubmitHandler = useCallback(
(event?: React.FormEvent<HTMLFormElement>) => {
async (event?: React.FormEvent<HTMLFormElement>) => {
clearErrorMessage?.();
void handleSubmit((data, event) => {
onSubmit(data.newPassword);
await handleSubmit(async (data) => {
await onSubmit(data.newPassword);
})(event);
},
[clearErrorMessage, handleSubmit, onSubmit]
@ -70,7 +70,12 @@ const Lite = ({ className, autoFocus, onSubmit, errorMessage, clearErrorMessage
{errorMessage && <ErrorMessage className={styles.formErrors}>{errorMessage}</ErrorMessage>}
<Button name="submit" title="action.save_password" htmlType="submit" />
<Button
name="submit"
title="action.save_password"
htmlType="submit"
isLoading={isSubmitting}
/>
<input hidden type="submit" />
</form>

View file

@ -17,7 +17,7 @@ type Props = {
readonly className?: string;
// eslint-disable-next-line react/boolean-prop-naming
readonly autoFocus?: boolean;
readonly onSubmit: (password: string) => void;
readonly onSubmit: (password: string) => Promise<void>;
readonly errorMessage?: string;
readonly clearErrorMessage?: () => void;
};
@ -43,7 +43,7 @@ const SetPassword = ({
watch,
resetField,
handleSubmit,
formState: { errors, isValid },
formState: { errors, isValid, isSubmitting },
} = useForm<FieldState>({
reValidateMode: 'onBlur',
defaultValues: { newPassword: '', confirmPassword: '' },
@ -56,11 +56,11 @@ const SetPassword = ({
}, [clearErrorMessage, isValid]);
const onSubmitHandler = useCallback(
(event?: React.FormEvent<HTMLFormElement>) => {
async (event?: React.FormEvent<HTMLFormElement>) => {
clearErrorMessage?.();
void handleSubmit((data, event) => {
onSubmit(data.newPassword);
await handleSubmit(async (data) => {
await onSubmit(data.newPassword);
})(event);
},
[clearErrorMessage, handleSubmit, onSubmit]
@ -119,7 +119,12 @@ const SetPassword = ({
<TogglePassword isChecked={showPassword} onChange={setShowPassword} />
<Button name="submit" title="action.save_password" htmlType="submit" />
<Button
name="submit"
title="action.save_password"
htmlType="submit"
isLoading={isSubmitting}
/>
<input hidden type="submit" />
</form>

View file

@ -7,7 +7,7 @@ type Props = {
readonly className?: string;
// eslint-disable-next-line react/boolean-prop-naming
readonly autoFocus?: boolean;
readonly onSubmit: (password: string) => void;
readonly onSubmit: (password: string) => Promise<void>;
readonly errorMessage?: string;
readonly clearErrorMessage?: () => void;
readonly maxLength?: number;

View file

@ -3,3 +3,7 @@
.totpCodeInput {
margin-top: _.unit(4);
}
.continueButton {
margin-top: _.unit(6);
}

View file

@ -1,5 +1,6 @@
import { useCallback, useState } from 'react';
import Button from '@/components/Button';
import VerificationCodeInput from '@/components/VerificationCode';
import { type UserMfaFlow } from '@/types';
@ -8,32 +9,58 @@ import useTotpCodeVerification from './use-totp-code-verification';
const totpCodeLength = 6;
const isCodeReady = (code: string[]) => {
return code.length === totpCodeLength && code.every(Boolean);
};
type Props = {
readonly flow: UserMfaFlow;
};
const TotpCodeVerification = ({ flow }: Props) => {
const [code, setCode] = useState<string[]>([]);
const [codeInput, setCodeInput] = useState<string[]>([]);
const errorCallback = useCallback(() => {
setCode([]);
setCodeInput([]);
}, []);
const { errorMessage, onSubmit } = useTotpCodeVerification(flow, errorCallback);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = useCallback(
async (code: string[]) => {
setIsSubmitting(true);
await onSubmit(code.join(''));
setIsSubmitting(false);
},
[onSubmit]
);
return (
<>
<VerificationCodeInput
name="totpCode"
value={code}
value={codeInput}
className={styles.totpCodeInput}
error={errorMessage}
onChange={(code) => {
setCode(code);
if (code.length === totpCodeLength && code.every(Boolean)) {
onSubmit(code.join(''));
onChange={async (code) => {
setCodeInput(code);
if (isCodeReady(code)) {
await handleSubmit(code);
}
}}
/>
<Button
title="action.continue"
type="primary"
className={styles.continueButton}
isLoading={isSubmitting}
isDisabled={!isCodeReady(codeInput)}
onClick={async () => {
await handleSubmit(codeInput);
}}
/>
</>
);
};

View file

@ -19,8 +19,8 @@ const useTotpCodeVerification = (flow: UserMfaFlow, errorCallback?: () => void)
);
const onSubmit = useCallback(
(code: string) => {
void sendMfaPayload(
async (code: string) => {
await sendMfaPayload(
{ flow, payload: { type: MfaFactor.TOTP, code } },
invalidCodeErrorHandlers,
errorCallback

View file

@ -27,7 +27,8 @@
align-self: start;
}
.switch {
.switch,
.continueButton {
margin-top: _.unit(6);
}
}

View file

@ -3,6 +3,7 @@ import classNames from 'classnames';
import { useState, useEffect, useCallback } from 'react';
import { useTranslation, Trans } from 'react-i18next';
import Button from '@/components/Button';
import TextLink from '@/components/TextLink';
import VerificationCodeInput, { defaultLength } from '@/components/VerificationCode';
import { UserFlow } from '@/types';
@ -24,6 +25,8 @@ const VerificationCode = ({ flow, identifier, className, hasPasswordButton, targ
const [code, setCode] = useState<string[]>([]);
const { t } = useTranslation();
const isCodeReady = code.length === defaultLength && code.every(Boolean);
const useVerificationCode = getCodeVerificationHookByFlow(flow);
const errorCallback = useCallback(() => {
@ -42,15 +45,27 @@ const VerificationCode = ({ flow, identifier, className, hasPasswordButton, targ
target
);
useEffect(() => {
if (code.length === defaultLength && code.every(Boolean)) {
const payload =
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = useCallback(async () => {
if (!isCodeReady) {
return;
}
setIsSubmitting(true);
await onSubmit(
identifier === SignInIdentifier.Email
? { email: target, verificationCode: code.join('') }
: { phone: target, verificationCode: code.join('') };
void onSubmit(payload);
}
}, [code, identifier, onSubmit, target]);
: { phone: target, verificationCode: code.join('') }
);
setIsSubmitting(false);
}, [code, identifier, isCodeReady, onSubmit, target]);
useEffect(() => {
void handleSubmit();
}, [handleSubmit]);
return (
<form className={classNames(styles.form, className)}>
@ -88,6 +103,14 @@ const VerificationCode = ({ flow, identifier, className, hasPasswordButton, targ
{flow === UserFlow.SignIn && hasPasswordButton && (
<PasswordSignInLink className={styles.switch} />
)}
<Button
title="action.continue"
type="primary"
isDisabled={!isCodeReady}
isLoading={isSubmitting}
className={styles.continueButton}
onClick={handleSubmit}
/>
</form>
);
};

View file

@ -27,10 +27,14 @@ const Consent = () => {
const [consentData, setConsentData] = useState<ConsentInfoResponse>();
const [selectedOrganization, setSelectedOrganization] = useState<Organization>();
const [isConsentLoading, setIsConsentLoading] = useState(false);
const asyncGetConsentInfo = useApi(getConsentInfo);
const consentHandler = useCallback(async () => {
setIsConsentLoading(true);
const [error, result] = await asyncConsent(selectedOrganization?.id);
setIsConsentLoading(false);
if (error) {
await handleError(error);
@ -113,7 +117,7 @@ const Consent = () => {
window.location.replace(consentData.redirectUri);
}}
/>
<Button title="action.authorize" onClick={consentHandler} />
<Button title="action.authorize" isLoading={isConsentLoading} onClick={consentHandler} />
</div>
{!showTerms && (
<div className={styles.redirectUri}>

View file

@ -43,7 +43,7 @@ const IdentifierProfileForm = ({
const {
handleSubmit,
control,
formState: { errors, isValid },
formState: { errors, isValid, isSubmitting },
} = useForm<FormState>({
reValidateMode: 'onBlur',
defaultValues: {
@ -64,7 +64,7 @@ const IdentifierProfileForm = ({
async (event?: React.FormEvent<HTMLFormElement>) => {
clearErrorMessage?.();
void handleSubmit(async ({ identifier: { type, value } }) => {
await handleSubmit(async ({ identifier: { type, value } }) => {
if (!type) {
return;
}
@ -115,7 +115,7 @@ const IdentifierProfileForm = ({
{errorMessage && <ErrorMessage className={styles.formErrors}>{errorMessage}</ErrorMessage>}
<Button title="action.continue" htmlType="submit" />
<Button title="action.continue" htmlType="submit" isLoading={isSubmitting} />
<input hidden type="submit" />
</form>

View file

@ -42,12 +42,13 @@ const ForgotPasswordForm = ({
UserFlow.ForgotPassword
);
const { setForgotPasswordIdentifierInputValue } = useContext(UserInteractionContext);
const { setForgotPasswordIdentifierInputValue, setIdentifierInputValue } =
useContext(UserInteractionContext);
const {
handleSubmit,
control,
formState: { errors, isValid },
formState: { errors, isValid, isSubmitting },
} = useForm<FormState>({
reValidateMode: 'onBlur',
defaultValues: {
@ -76,10 +77,19 @@ const ForgotPasswordForm = ({
// Cache or update the forgot password identifier input value
setForgotPasswordIdentifierInputValue({ type, value });
// Set current identifier input value for code verification
setIdentifierInputValue({ type, value });
await onSubmit({ identifier: type, value });
})(event);
},
[clearErrorMessage, handleSubmit, onSubmit, setForgotPasswordIdentifierInputValue]
[
clearErrorMessage,
handleSubmit,
onSubmit,
setForgotPasswordIdentifierInputValue,
setIdentifierInputValue,
]
);
return (
@ -122,7 +132,7 @@ const ForgotPasswordForm = ({
{errorMessage && <ErrorMessage className={styles.formErrors}>{errorMessage}</ErrorMessage>}
<Button title="action.continue" htmlType="submit" />
<Button title="action.continue" htmlType="submit" isLoading={isSubmitting} />
<input hidden type="submit" />
</form>

View file

@ -1,5 +1,6 @@
import { MfaFactor } from '@logto/schemas';
import { t } from 'i18next';
import { useState } from 'react';
import { useLocation } from 'react-router-dom';
import { validate } from 'superstruct';
@ -18,6 +19,7 @@ import * as styles from './index.module.scss';
const BackupCodeBinding = () => {
const { copyText, downloadText } = useTextHandler();
const sendMfaPayload = useSendMfaPayload();
const [isSubmitting, setIsSubmitting] = useState(false);
const { state } = useLocation();
const [, backupCodeBindingState] = validate(state, backupCodeBindingStateGuard);
@ -46,7 +48,7 @@ const BackupCodeBinding = () => {
<Button
title="action.download"
type="secondary"
onClick={() => {
onClick={async () => {
downloadText(backupCodeTextContent, 'backup_code.txt');
}}
/>
@ -54,7 +56,7 @@ const BackupCodeBinding = () => {
<Button
title="action.copy"
type="secondary"
onClick={() => {
onClick={async () => {
void copyText(backupCodeTextContent, t('mfa.backup_code_copied'));
}}
/>
@ -64,11 +66,14 @@ const BackupCodeBinding = () => {
</div>
<Button
title="action.continue"
onClick={() => {
void sendMfaPayload({
isLoading={isSubmitting}
onClick={async () => {
setIsSubmitting(true);
await sendMfaPayload({
flow: UserMfaFlow.MfaBinding,
payload: { type: MfaFactor.BackupCode },
});
setIsSubmitting(false);
}}
/>
</div>

View file

@ -1,4 +1,5 @@
import { conditional } from '@silverhand/essentials';
import { useState } from 'react';
import { useLocation } from 'react-router-dom';
import { validate } from 'superstruct';
@ -19,6 +20,7 @@ const WebAuthnBinding = () => {
const [, webAuthnState] = validate(state, webAuthnStateGuard);
const handleWebAuthn = useWebAuthnOperation();
const skipMfa = useSkipMfa();
const [isCreatingPasskey, setIsCreatingPasskey] = useState(false);
if (!webAuthnState) {
return <ErrorPage title="error.invalid_session" />;
@ -38,8 +40,11 @@ const WebAuthnBinding = () => {
>
<Button
title="mfa.create_a_passkey"
onClick={() => {
void handleWebAuthn(options);
isLoading={isCreatingPasskey}
onClick={async () => {
setIsCreatingPasskey(true);
await handleWebAuthn(options);
setIsCreatingPasskey(false);
}}
/>
<SwitchMfaFactorsLink

View file

@ -22,12 +22,16 @@ type FormState = {
const BackupCodeVerification = () => {
const flowState = useMfaFlowState();
const sendMfaPayload = useSendMfaPayload();
const { register, handleSubmit } = useForm<FormState>({ defaultValues: { code: '' } });
const {
register,
handleSubmit,
formState: { isSubmitting },
} = useForm<FormState>({ defaultValues: { code: '' } });
const onSubmitHandler = useCallback(
(event?: FormEvent<HTMLFormElement>) => {
void handleSubmit(async ({ code }) => {
void sendMfaPayload({
async (event?: FormEvent<HTMLFormElement>) => {
await handleSubmit(async ({ code }) => {
await sendMfaPayload({
flow: UserMfaFlow.MfaVerification,
payload: { type: MfaFactor.BackupCode, code },
});
@ -53,7 +57,7 @@ const BackupCodeVerification = () => {
className={styles.backupCodeInput}
{...register('code')}
/>
<Button title="action.continue" htmlType="submit" />
<Button title="action.continue" htmlType="submit" isLoading={isSubmitting} />
</form>
</SectionLayout>
<SwitchMfaFactorsLink

View file

@ -1,3 +1,4 @@
import { useState } from 'react';
import { useLocation } from 'react-router-dom';
import { validate } from 'superstruct';
@ -17,6 +18,7 @@ const WebAuthnVerification = () => {
const { state } = useLocation();
const [, webAuthnState] = validate(state, webAuthnStateGuard);
const handleWebAuthn = useWebAuthnOperation();
const [isVerifying, setIsVerifying] = useState(false);
if (!webAuthnState) {
return <ErrorPage title="error.invalid_session" />;
@ -37,8 +39,11 @@ const WebAuthnVerification = () => {
<Button
title="action.verify_via_passkey"
className={styles.verifyButton}
onClick={() => {
void handleWebAuthn(options);
isLoading={isVerifying}
onClick={async () => {
setIsVerifying(true);
await handleWebAuthn(options);
setIsVerifying(false);
}}
/>
</SectionLayout>

View file

@ -40,7 +40,7 @@ const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props)
const {
watch,
handleSubmit,
formState: { errors, isValid },
formState: { errors, isValid, isSubmitting },
control,
} = useForm<FormState>({
reValidateMode: 'onBlur',
@ -154,6 +154,7 @@ const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props)
title={showSingleSignOnForm ? 'action.single_sign_on' : 'action.create_account'}
icon={showSingleSignOnForm ? <LockIcon /> : undefined}
htmlType="submit"
isLoading={isSubmitting}
/>
<input hidden type="submit" />

View file

@ -44,7 +44,7 @@ const IdentifierSignInForm = ({ className, autoFocus, signInMethods }: Props) =>
watch,
handleSubmit,
control,
formState: { errors, isValid },
formState: { errors, isValid, isSubmitting },
} = useForm<FormState>({
reValidateMode: 'onBlur',
});
@ -153,6 +153,7 @@ const IdentifierSignInForm = ({ className, autoFocus, signInMethods }: Props) =>
title={showSingleSignOnForm ? 'action.single_sign_on' : 'action.sign_in'}
icon={showSingleSignOnForm ? <LockIcon /> : undefined}
htmlType="submit"
isLoading={isSubmitting}
/>
<input hidden type="submit" />

View file

@ -45,7 +45,7 @@ const PasswordSignInForm = ({ className, autoFocus, signInMethods }: Props) => {
register,
handleSubmit,
control,
formState: { errors, isValid },
formState: { errors, isValid, isSubmitting },
} = useForm<FormState>({
reValidateMode: 'onBlur',
defaultValues: {
@ -174,6 +174,7 @@ const PasswordSignInForm = ({ className, autoFocus, signInMethods }: Props) => {
title={showSingleSignOnForm ? 'action.single_sign_on' : 'action.sign_in'}
icon={showSingleSignOnForm ? <LockIcon /> : undefined}
htmlType="submit"
isLoading={isSubmitting}
/>
<input hidden type="submit" />

View file

@ -47,7 +47,7 @@ const PasswordForm = ({
register,
handleSubmit,
control,
formState: { errors, isValid },
formState: { errors, isValid, isSubmitting },
} = useForm<FormState>({
reValidateMode: 'onBlur',
defaultValues: {
@ -69,7 +69,7 @@ const PasswordForm = ({
async (event?: React.FormEvent<HTMLFormElement>) => {
clearErrorMessage();
void handleSubmit(async ({ identifier: { type, value }, password }) => {
await handleSubmit(async ({ identifier: { type, value }, password }) => {
if (!type) {
return;
}
@ -114,7 +114,7 @@ const PasswordForm = ({
<ForgotPasswordLink className={styles.link} identifier={identifier} value={value} />
)}
<Button title="action.continue" name="submit" htmlType="submit" />
<Button title="action.continue" name="submit" htmlType="submit" isLoading={isSubmitting} />
{identifier !== SignInIdentifier.Username && isVerificationCodeEnabled && (
<VerificationCodeLink className={styles.switch} identifier={identifier} value={value} />

View file

@ -26,7 +26,7 @@ const SingleSignOnEmail = () => {
const {
handleSubmit,
control,
formState: { errors, isValid },
formState: { errors, isValid, isSubmitting },
} = useForm<FormState>({
reValidateMode: 'onBlur',
});
@ -82,7 +82,12 @@ const SingleSignOnEmail = () => {
{errorMessage && <ErrorMessage className={styles.formErrors}>{errorMessage}</ErrorMessage>}
<Button title="action.single_sign_on" htmlType="submit" icon={<LockIcon />} />
<Button
title="action.single_sign_on"
htmlType="submit"
icon={<LockIcon />}
isLoading={isSubmitting}
/>
<input hidden type="submit" />
</form>

View file

@ -38,6 +38,7 @@
--color-brand-30: #4300da;
--color-brand-40: #5d34f2;
--color-brand-50: #7958ff;
--color-brand-70: #af9eff;
--color-alert-60: #ca8000;
--color-alert-70: #eb9918;