1
0
mirror of https://github.com/excalidraw/excalidraw.git synced 2024-11-02 03:25:53 +01:00

feat: add loading state to FilledButton (#7650)

This commit is contained in:
David Luzar 2024-02-03 14:53:31 +01:00 committed by GitHub
parent d67eaa8710
commit a289c42830
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 119 additions and 31 deletions

@ -120,7 +120,7 @@ export const RoomModal = ({
size="large"
variant="icon"
label="Share"
startIcon={getShareIcon()}
icon={getShareIcon()}
className="RoomDialog__active__share"
onClick={shareRoomLink}
/>
@ -130,7 +130,7 @@ export const RoomModal = ({
<FilledButton
size="large"
label="Copy link"
startIcon={copyIcon}
icon={copyIcon}
onClick={copyRoomLink}
/>
</Popover.Trigger>
@ -166,7 +166,7 @@ export const RoomModal = ({
variant="outlined"
color="danger"
label={t("roomDialog.button_stopSession")}
startIcon={playerStopFilledIcon}
icon={playerStopFilledIcon}
onClick={() => {
trackEvent("share", "room closed");
onRoomDestroy();
@ -195,7 +195,7 @@ export const RoomModal = ({
<FilledButton
size="large"
label={t("roomDialog.button_startSession")}
startIcon={playerPlayIcon}
icon={playerPlayIcon}
onClick={() => {
trackEvent("share", "room creation", `ui (${getFrame()})`);
onRoomCreate();

@ -10,6 +10,7 @@ import {
import { ExcalidrawElement } from "../element/types";
import { AppClassProperties, AppState } from "../types";
import { trackEvent } from "../analytics";
import { isPromiseLike } from "../utils";
const trackAction = (
action: Action,
@ -55,7 +56,7 @@ export class ActionManager {
app: AppClassProperties,
) {
this.updater = (actionResult) => {
if (actionResult && "then" in actionResult) {
if (isPromiseLike(actionResult)) {
actionResult.then((actionResult) => {
return updater(actionResult);
});

@ -10,11 +10,39 @@
background-color: var(--back-color);
border-color: var(--border-color);
.Spinner {
--spinner-color: var(--color-surface-lowest);
position: absolute;
visibility: visible;
}
&[disabled] {
pointer-events: none;
.ExcButton__contents {
visibility: hidden;
}
}
&__contents {
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
flex-wrap: nowrap;
// needed because of .Spinner
position: relative;
}
&--color-primary {
&.ExcButton--variant-filled {
--text-color: var(--color-surface-lowest);
--back-color: var(--color-primary);
.Spinner {
--spinner-color: var(--text-color);
}
&:hover {
--back-color: var(--color-brand-hover);
}
@ -27,9 +55,13 @@
&.ExcButton--variant-outlined,
&.ExcButton--variant-icon {
--text-color: var(--color-primary);
--border-color: var(--color-border-outline);
--border-color: var(--color-primary);
--back-color: transparent;
.Spinner {
--spinner-color: var(--text-color);
}
&:hover {
--text-color: var(--color-brand-hover);
--border-color: var(--color-brand-hover);
@ -47,6 +79,10 @@
--text-color: var(--color-danger-text);
--back-color: var(--color-danger-dark);
.Spinner {
--spinner-color: var(--text-color);
}
&:hover {
--back-color: var(--color-danger-darker);
}
@ -62,6 +98,10 @@
--border-color: var(--color-danger);
--back-color: transparent;
.Spinner {
--spinner-color: var(--text-color);
}
&:hover {
--text-color: var(--color-danger-darkest);
--border-color: var(--color-danger-darkest);
@ -79,6 +119,10 @@
--text-color: var(--island-bg-color);
--back-color: var(--color-gray-50);
.Spinner {
--spinner-color: var(--text-color);
}
&:hover {
--back-color: var(--color-gray-60);
}
@ -94,6 +138,10 @@
--border-color: var(--color-muted);
--back-color: var(--island-bg-color);
.Spinner {
--spinner-color: var(--text-color);
}
&:hover {
--text-color: var(--color-muted-background-darker);
--border-color: var(--color-muted-darker);
@ -111,6 +159,10 @@
--text-color: black;
--back-color: var(--color-warning-dark);
.Spinner {
--spinner-color: var(--text-color);
}
&:hover {
--back-color: var(--color-warning-darker);
}
@ -126,6 +178,10 @@
--border-color: var(--color-warning-dark);
--back-color: var(--input-bg-color);
.Spinner {
--spinner-color: var(--text-color);
}
&:hover {
--text-color: var(--color-warning-darker);
--border-color: var(--color-warning-darker);
@ -138,17 +194,11 @@
}
}
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
flex-wrap: nowrap;
border-radius: 0.5rem;
border-width: 1px;
border-style: solid;
font-family: "Assistant";
font-family: var(--font-family);
user-select: none;
@ -159,9 +209,12 @@
font-size: 0.875rem;
min-height: 3rem;
padding: 0.5rem 1.5rem;
gap: 0.75rem;
letter-spacing: 0.4px;
.ExcButton__contents {
gap: 0.75rem;
}
}
&--size-medium {
@ -169,9 +222,12 @@
font-size: 0.75rem;
min-height: 2.5rem;
padding: 0.5rem 1rem;
gap: 0.5rem;
letter-spacing: normal;
.ExcButton__contents {
gap: 0.5rem;
}
}
&--variant-icon {

@ -1,7 +1,10 @@
import React, { forwardRef } from "react";
import React, { forwardRef, useState } from "react";
import clsx from "clsx";
import "./FilledButton.scss";
import { AbortError } from "../errors";
import Spinner from "./Spinner";
import { isPromiseLike } from "../utils";
export type ButtonVariant = "filled" | "outlined" | "icon";
export type ButtonColor = "primary" | "danger" | "warning" | "muted";
@ -11,7 +14,7 @@ export type FilledButtonProps = {
label: string;
children?: React.ReactNode;
onClick?: () => void;
onClick?: (event: React.MouseEvent) => void;
variant?: ButtonVariant;
color?: ButtonColor;
@ -19,14 +22,14 @@ export type FilledButtonProps = {
className?: string;
fullWidth?: boolean;
startIcon?: React.ReactNode;
icon?: React.ReactNode;
};
export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
(
{
children,
startIcon,
icon,
onClick,
label,
variant = "filled",
@ -37,6 +40,27 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
},
ref,
) => {
const [isLoading, setIsLoading] = useState(false);
const _onClick = async (event: React.MouseEvent) => {
const ret = onClick?.(event);
if (isPromiseLike(ret)) {
try {
setIsLoading(true);
await ret;
} catch (error: any) {
if (!(error instanceof AbortError)) {
throw error;
} else {
console.warn(error);
}
} finally {
setIsLoading(false);
}
}
};
return (
<button
className={clsx(
@ -47,17 +71,21 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
{ "ExcButton--fullWidth": fullWidth },
className,
)}
onClick={onClick}
onClick={_onClick}
type="button"
aria-label={label}
ref={ref}
disabled={isLoading}
>
{startIcon && (
<div className="ExcButton__icon" aria-hidden>
{startIcon}
</div>
)}
{variant !== "icon" && (children ?? label)}
<div className="ExcButton__contents">
{isLoading && <Spinner />}
{icon && (
<div className="ExcButton__icon" aria-hidden>
{icon}
</div>
)}
{variant !== "icon" && (children ?? label)}
</div>
</button>
);
},

@ -12,6 +12,8 @@
flex-direction: row;
justify-content: space-between;
user-select: none;
& h3 {
font-family: "Assistant";
font-style: normal;

@ -271,7 +271,7 @@ const ImageExportModal = ({
exportingFrame,
})
}
startIcon={downloadIcon}
icon={downloadIcon}
>
{t("imageExportDialog.button.exportToPng")}
</FilledButton>
@ -283,7 +283,7 @@ const ImageExportModal = ({
exportingFrame,
})
}
startIcon={downloadIcon}
icon={downloadIcon}
>
{t("imageExportDialog.button.exportToSvg")}
</FilledButton>
@ -296,7 +296,7 @@ const ImageExportModal = ({
exportingFrame,
})
}
startIcon={copyIcon}
icon={copyIcon}
>
{t("imageExportDialog.button.copyPngToClipboard")}
</FilledButton>

@ -66,7 +66,7 @@ export const ShareableLinkDialog = ({
<FilledButton
size="large"
label="Copy link"
startIcon={copyIcon}
icon={copyIcon}
onClick={copyRoomLink}
/>
</Popover.Trigger>

@ -6,6 +6,7 @@ import { useExcalidrawContainer } from "./App";
import { AbortError } from "../errors";
import Spinner from "./Spinner";
import { PointerType } from "../element/types";
import { isPromiseLike } from "../utils";
export type ToolButtonSize = "small" | "medium";
@ -65,7 +66,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
const onClick = async (event: React.MouseEvent) => {
const ret = "onClick" in props && props.onClick?.(event);
if (ret && "then" in ret) {
if (isPromiseLike(ret)) {
try {
setIsLoading(true);
await ret;