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:
parent
d67eaa8710
commit
a289c42830
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user