0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

feat(experience): support loading state for buttons (#6232)

This commit is contained in:
Xiao Yijun 2024-07-12 21:51:10 +08:00 committed by GitHub
parent d203c8d2ff
commit 6bf3bebb40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 101 additions and 13 deletions

View file

@ -0,0 +1,3 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.8357 16.8714C11.941 17.4135 11.5865 17.9451 11.0355 17.9821C9.9048 18.0579 8.7669 17.8929 7.69834 17.4934C6.31639 16.9767 5.10425 16.0879 4.19591 14.9253C3.28758 13.7627 2.71844 12.3715 2.55143 10.9056C2.38441 9.43973 2.62602 7.9562 3.24954 6.61905C3.87306 5.28191 4.85421 4.14323 6.0845 3.32891C7.3148 2.5146 8.74632 2.05636 10.2208 2.00487C11.6953 1.95338 13.1553 2.31064 14.4394 3.03715C15.4323 3.59891 16.2901 4.36452 16.9588 5.27942C17.2847 5.72531 17.1054 6.33858 16.6223 6.60633C16.1393 6.87408 15.5366 6.69278 15.1924 6.26086C14.7154 5.66218 14.1262 5.15785 13.4545 4.77787C12.4915 4.23298 11.3965 3.96504 10.2906 4.00366C9.18474 4.04227 8.1111 4.38595 7.18838 4.99669C6.26565 5.60742 5.52979 6.46143 5.06215 7.46429C4.59451 8.46715 4.41331 9.5798 4.53857 10.6792C4.66383 11.7786 5.09069 12.822 5.77194 13.694C6.45319 14.5659 7.3623 15.2325 8.39876 15.62C9.12154 15.8903 9.88663 16.0175 10.6519 15.9981C11.204 15.9841 11.7303 16.3293 11.8357 16.8714Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,14 @@
.icon {
animation: rotating 1s linear infinite;
}
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View file

@ -0,0 +1,7 @@
import Ring from '@/assets/icons/loading-ring.svg';
import * as RotatingRingIconStyles from './RotatingRingIcon.module.scss';
const RotatingRingIcon = () => <Ring className={RotatingRingIconStyles.icon} />;
export default RotatingRingIcon;

View file

@ -12,6 +12,15 @@
overflow: hidden; overflow: hidden;
} }
.loadingIcon {
display: block;
// To avoid the layout shift, add padding manually (keep the same size as the icon)
padding: 0 1.5px;
color: var(--color-brand-70);
font-size: 0;
line-height: normal;
}
.name { .name {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;

View file

@ -1,11 +1,14 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDebouncedLoader } from 'use-debounced-loader';
import RotatingRingIcon from './RotatingRingIcon';
import * as socialLinkButtonStyles from './SocialLinkButton.module.scss'; import * as socialLinkButtonStyles from './SocialLinkButton.module.scss';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
export type Props = { export type Props = {
readonly isDisabled?: boolean; readonly isDisabled?: boolean;
readonly isLoading?: boolean;
readonly className?: string; readonly className?: string;
readonly target: string; readonly target: string;
readonly logo: string; readonly logo: string;
@ -13,7 +16,15 @@ export type Props = {
readonly onClick?: () => void; readonly onClick?: () => void;
}; };
const SocialLinkButton = ({ isDisabled, className, target, name, logo, onClick }: Props) => { const SocialLinkButton = ({
isDisabled,
isLoading = false,
className,
target,
name,
logo,
onClick,
}: Props) => {
const { const {
t, t,
i18n: { language }, i18n: { language },
@ -21,6 +32,8 @@ const SocialLinkButton = ({ isDisabled, className, target, name, logo, onClick }
const localName = name[language] ?? name.en; const localName = name[language] ?? name.en;
const isLoadingActive = useDebouncedLoader(isLoading, 300);
return ( return (
<button <button
disabled={isDisabled} disabled={isDisabled}
@ -29,13 +42,13 @@ const SocialLinkButton = ({ isDisabled, className, target, name, logo, onClick }
styles.secondary, styles.secondary,
styles.large, styles.large,
socialLinkButtonStyles.socialButton, socialLinkButtonStyles.socialButton,
isDisabled && styles.disabled, (isDisabled ?? isLoadingActive) && styles.disabled,
className className
)} )}
type="button" type="button"
onClick={onClick} onClick={onClick}
> >
{logo && ( {logo && !isLoadingActive && (
<img <img
src={logo} src={logo}
alt={target} alt={target}
@ -43,6 +56,11 @@ const SocialLinkButton = ({ isDisabled, className, target, name, logo, onClick }
crossOrigin="anonymous" crossOrigin="anonymous"
/> />
)} )}
{isLoadingActive && (
<span className={socialLinkButtonStyles.loadingIcon}>
<RotatingRingIcon />
</span>
)}
<div className={socialLinkButtonStyles.name}> <div className={socialLinkButtonStyles.name}>
<div className={socialLinkButtonStyles.placeHolder} /> <div className={socialLinkButtonStyles.placeHolder} />
<span>{t('action.sign_in_with', { name: localName })}</span> <span>{t('action.sign_in_with', { name: localName })}</span>

View file

@ -16,10 +16,28 @@
user-select: none; user-select: none;
overflow: hidden; overflow: hidden;
.icon { .content {
font-size: 0; position: relative;
line-height: normal; transition: padding-left 0.2s ease;
margin-right: _.unit(2);
.icon {
font-size: 0;
line-height: normal;
position: absolute;
pointer-events: none;
left: 0;
opacity: 0%;
transition: opacity 0.2s ease;
}
&.iconVisible {
padding-left: _.unit(7);
.icon {
left: 0;
opacity: 100%;
}
}
} }
} }
@ -45,6 +63,10 @@
&:active { &:active {
background: var(--color-brand-pressed); background: var(--color-brand-pressed);
} }
&.loading {
background-color: var(--color-brand-70);
}
} }
.secondary { .secondary {
@ -74,7 +96,7 @@
outline: 3px solid var(--color-overlay-brand-focused); 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); background: var(--color-brand-hover);
} }
} }
@ -84,7 +106,7 @@
outline: 3px solid var(--color-overlay-neutral-focused); 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); background: var(--color-overlay-neutral-hover);
} }
} }

View file

@ -1,9 +1,11 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { TFuncKey } from 'i18next'; import type { TFuncKey } from 'i18next';
import type { HTMLProps } from 'react'; import { type HTMLProps } from 'react';
import { useDebouncedLoader } from 'use-debounced-loader';
import DynamicT from '../DynamicT'; import DynamicT from '../DynamicT';
import RotatingRingIcon from './RotatingRingIcon';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
export type ButtonType = 'primary' | 'secondary'; export type ButtonType = 'primary' | 'secondary';
@ -13,6 +15,7 @@ type BaseProps = Omit<HTMLProps<HTMLButtonElement>, 'type' | 'size' | 'title'> &
readonly type?: ButtonType; readonly type?: ButtonType;
readonly size?: 'small' | 'large'; readonly size?: 'small' | 'large';
readonly isDisabled?: boolean; readonly isDisabled?: boolean;
readonly isLoading?: boolean;
readonly className?: string; readonly className?: string;
readonly onClick?: React.MouseEventHandler; readonly onClick?: React.MouseEventHandler;
}; };
@ -31,10 +34,13 @@ const Button = ({
i18nProps, i18nProps,
className, className,
isDisabled = false, isDisabled = false,
isLoading = false,
icon, icon,
onClick, onClick,
...rest ...rest
}: Props) => { }: Props) => {
const isLoadingActive = useDebouncedLoader(isLoading, 300);
return ( return (
<button <button
disabled={isDisabled} disabled={isDisabled}
@ -42,15 +48,23 @@ const Button = ({
styles.button, styles.button,
styles[type], styles[type],
styles[size], styles[size],
isDisabled && styles.isDisabled, isDisabled && styles.disabled,
isLoadingActive && styles.loading,
className className
)} )}
type={htmlType} type={htmlType}
onClick={onClick} onClick={onClick}
{...rest} {...rest}
> >
{icon && <span className={styles.icon}>{icon}</span>} <span
<DynamicT forKey={title} interpolation={i18nProps} /> className={classNames(
styles.content,
(isLoadingActive || Boolean(icon)) && styles.iconVisible
)}
>
<span className={styles.icon}>{isLoadingActive ? <RotatingRingIcon /> : icon}</span>
<DynamicT forKey={title} interpolation={i18nProps} />
</span>
</button> </button>
); );
}; };

View file

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