diff --git a/packages/experience/src/Providers/LoadingLayerProvider/index.tsx b/packages/experience/src/Providers/LoadingLayerProvider/index.tsx index eccc54c9b..2c3601ee0 100644 --- a/packages/experience/src/Providers/LoadingLayerProvider/index.tsx +++ b/packages/experience/src/Providers/LoadingLayerProvider/index.tsx @@ -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 ( <> - {debouncedLoading && } + {loading && } ); }; diff --git a/packages/experience/src/assets/icons/ring.svg b/packages/experience/src/assets/icons/ring.svg new file mode 100644 index 000000000..a2464b97c --- /dev/null +++ b/packages/experience/src/assets/icons/ring.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/experience/src/components/Button/index.module.scss b/packages/experience/src/components/Button/index.module.scss index 67993f96c..fdd1cfe79 100644 --- a/packages/experience/src/components/Button/index.module.scss +++ b/packages/experience/src/components/Button/index.module.scss @@ -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); + } +} diff --git a/packages/experience/src/components/Button/index.tsx b/packages/experience/src/components/Button/index.tsx index 544198975..578c7731d 100644 --- a/packages/experience/src/components/Button/index.tsx +++ b/packages/experience/src/components/Button/index.tsx @@ -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, '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 && {icon}} + {icon && !isLoading && {icon}} + {isLoading && ( + + + + )} ); diff --git a/packages/experience/src/components/LoadingLayer/index.module.scss b/packages/experience/src/components/LoadingLayer/index.module.scss index 4f24e5e8b..115c477e4 100644 --- a/packages/experience/src/components/LoadingLayer/index.module.scss +++ b/packages/experience/src/components/LoadingLayer/index.module.scss @@ -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; diff --git a/packages/experience/src/components/LoadingLayer/index.tsx b/packages/experience/src/components/LoadingLayer/index.tsx index 4db64f3a1..52b43e236 100644 --- a/packages/experience/src/components/LoadingLayer/index.tsx +++ b/packages/experience/src/components/LoadingLayer/index.tsx @@ -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 = () => ( -
+
-
+
); export default LoadingLayer; diff --git a/packages/experience/src/components/LoadingMask/index.module.scss b/packages/experience/src/components/LoadingMask/index.module.scss new file mode 100644 index 000000000..30e65e818 --- /dev/null +++ b/packages/experience/src/components/LoadingMask/index.module.scss @@ -0,0 +1,8 @@ +@use '@/scss/underscore' as _; + +.overlay { + position: fixed; + inset: 0; + @include _.flex-column; + z-index: 300; +} diff --git a/packages/experience/src/components/LoadingMask/index.tsx b/packages/experience/src/components/LoadingMask/index.tsx new file mode 100644 index 000000000..b77e44351 --- /dev/null +++ b/packages/experience/src/components/LoadingMask/index.tsx @@ -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
{children}
; +}; + +export default LoadingMask; diff --git a/packages/experience/src/containers/SetPassword/Lite.tsx b/packages/experience/src/containers/SetPassword/Lite.tsx index aa1abd3b8..762ce4446 100644 --- a/packages/experience/src/containers/SetPassword/Lite.tsx +++ b/packages/experience/src/containers/SetPassword/Lite.tsx @@ -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; 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({ reValidateMode: 'onBlur', defaultValues: { newPassword: '' }, @@ -42,11 +42,11 @@ const Lite = ({ className, autoFocus, onSubmit, errorMessage, clearErrorMessage }, [clearErrorMessage, isValid]); const onSubmitHandler = useCallback( - (event?: React.FormEvent) => { + async (event?: React.FormEvent) => { 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}} -