mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
fix: fixed copy to clipboard button (#8426)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
afb68a6467
commit
26d2296578
12 changed files with 160 additions and 284 deletions
|
@ -16,11 +16,19 @@
|
|||
|
||||
.Spinner {
|
||||
--spinner-color: var(--color-surface-lowest);
|
||||
position: absolute;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
.ExcButton__statusIcon {
|
||||
visibility: visible;
|
||||
position: absolute;
|
||||
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&.ExcButton--status-loading,
|
||||
&.ExcButton--status-success {
|
||||
pointer-events: none;
|
||||
|
||||
.ExcButton__contents {
|
||||
|
@ -28,6 +36,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&,
|
||||
&__contents {
|
||||
display: flex;
|
||||
|
@ -119,6 +131,46 @@
|
|||
}
|
||||
}
|
||||
|
||||
&--color-success {
|
||||
&.ExcButton--variant-filled {
|
||||
--text-color: var(--color-success-text);
|
||||
--back-color: var(--color-success);
|
||||
|
||||
.Spinner {
|
||||
--spinner-color: var(--color-success);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--back-color: var(--color-success-darker);
|
||||
}
|
||||
|
||||
&:active {
|
||||
--back-color: var(--color-success-darkest);
|
||||
}
|
||||
}
|
||||
|
||||
&.ExcButton--variant-outlined,
|
||||
&.ExcButton--variant-icon {
|
||||
--text-color: var(--color-success-contrast);
|
||||
--border-color: var(--color-success-contrast);
|
||||
--back-color: transparent;
|
||||
|
||||
.Spinner {
|
||||
--spinner-color: var(--color-success-contrast);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--text-color: var(--color-success-contrast-hover);
|
||||
--border-color: var(--color-success-contrast-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
--text-color: var(--color-success-contrast-active);
|
||||
--border-color: var(--color-success-contrast-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--color-muted {
|
||||
&.ExcButton--variant-filled {
|
||||
--text-color: var(--island-bg-color);
|
||||
|
|
|
@ -5,9 +5,15 @@ import "./FilledButton.scss";
|
|||
import { AbortError } from "../errors";
|
||||
import Spinner from "./Spinner";
|
||||
import { isPromiseLike } from "../utils";
|
||||
import { tablerCheckIcon } from "./icons";
|
||||
|
||||
export type ButtonVariant = "filled" | "outlined" | "icon";
|
||||
export type ButtonColor = "primary" | "danger" | "warning" | "muted";
|
||||
export type ButtonColor =
|
||||
| "primary"
|
||||
| "danger"
|
||||
| "warning"
|
||||
| "muted"
|
||||
| "success";
|
||||
export type ButtonSize = "medium" | "large";
|
||||
|
||||
export type FilledButtonProps = {
|
||||
|
@ -15,6 +21,7 @@ export type FilledButtonProps = {
|
|||
|
||||
children?: React.ReactNode;
|
||||
onClick?: (event: React.MouseEvent) => void;
|
||||
status?: null | "loading" | "success";
|
||||
|
||||
variant?: ButtonVariant;
|
||||
color?: ButtonColor;
|
||||
|
@ -37,6 +44,7 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
|||
size = "medium",
|
||||
fullWidth,
|
||||
className,
|
||||
status,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
|
@ -46,8 +54,11 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
|||
const ret = onClick?.(event);
|
||||
|
||||
if (isPromiseLike(ret)) {
|
||||
try {
|
||||
// delay loading state to prevent flicker in case of quick response
|
||||
const timer = window.setTimeout(() => {
|
||||
setIsLoading(true);
|
||||
}, 50);
|
||||
try {
|
||||
await ret;
|
||||
} catch (error: any) {
|
||||
if (!(error instanceof AbortError)) {
|
||||
|
@ -56,11 +67,15 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
|||
console.warn(error);
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const _status = isLoading ? "loading" : status;
|
||||
color = _status === "success" ? "success" : color;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
|
@ -68,6 +83,7 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
|||
`ExcButton--color-${color}`,
|
||||
`ExcButton--variant-${variant}`,
|
||||
`ExcButton--size-${size}`,
|
||||
`ExcButton--status-${_status}`,
|
||||
{ "ExcButton--fullWidth": fullWidth },
|
||||
className,
|
||||
)}
|
||||
|
@ -75,10 +91,16 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
|||
type="button"
|
||||
aria-label={label}
|
||||
ref={ref}
|
||||
disabled={isLoading}
|
||||
disabled={_status === "loading" || _status === "success"}
|
||||
>
|
||||
<div className="ExcButton__contents">
|
||||
{isLoading && <Spinner />}
|
||||
{_status === "loading" ? (
|
||||
<Spinner className="ExcButton__statusIcon" />
|
||||
) : (
|
||||
_status === "success" && (
|
||||
<div className="ExcButton__statusIcon">{tablerCheckIcon}</div>
|
||||
)
|
||||
)}
|
||||
{icon && (
|
||||
<div className="ExcButton__icon" aria-hidden>
|
||||
{icon}
|
||||
|
|
|
@ -35,6 +35,7 @@ import "./ImageExportDialog.scss";
|
|||
import { FilledButton } from "./FilledButton";
|
||||
import { cloneJSON } from "../utils";
|
||||
import { prepareElementsForExport } from "../data";
|
||||
import { useCopyStatus } from "../hooks/useCopiedIndicator";
|
||||
|
||||
const supportsContextFilters =
|
||||
"filter" in document.createElement("canvas").getContext("2d")!;
|
||||
|
@ -89,6 +90,8 @@ const ImageExportModal = ({
|
|||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
const [renderError, setRenderError] = useState<Error | null>(null);
|
||||
|
||||
const { onCopy, copyStatus } = useCopyStatus();
|
||||
|
||||
const { exportedElements, exportingFrame } = prepareElementsForExport(
|
||||
elementsSnapshot,
|
||||
appStateSnapshot,
|
||||
|
@ -294,11 +297,17 @@ const ImageExportModal = ({
|
|||
<FilledButton
|
||||
className="ImageExportModal__settings__buttons__button"
|
||||
label={t("imageExportDialog.title.copyPngToClipboard")}
|
||||
onClick={() =>
|
||||
onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements, {
|
||||
exportingFrame,
|
||||
})
|
||||
}
|
||||
status={copyStatus}
|
||||
onClick={async () => {
|
||||
await onExportImage(
|
||||
EXPORT_IMAGE_TYPES.clipboard,
|
||||
exportedElements,
|
||||
{
|
||||
exportingFrame,
|
||||
},
|
||||
);
|
||||
onCopy();
|
||||
}}
|
||||
icon={copyIcon}
|
||||
>
|
||||
{t("imageExportDialog.button.copyPngToClipboard")}
|
||||
|
|
|
@ -52,8 +52,8 @@
|
|||
font-size: 0.75rem;
|
||||
line-height: 110%;
|
||||
|
||||
background: var(--color-success-lighter);
|
||||
color: var(--color-success);
|
||||
background: var(--color-success);
|
||||
color: var(--color-success-text);
|
||||
|
||||
& > svg {
|
||||
width: 0.875rem;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useRef, useState } from "react";
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
|
||||
import { copyTextToSystemClipboard } from "../clipboard";
|
||||
import { useI18n } from "../i18n";
|
||||
|
@ -7,7 +6,8 @@ import { useI18n } from "../i18n";
|
|||
import { Dialog } from "./Dialog";
|
||||
import { TextField } from "./TextField";
|
||||
import { FilledButton } from "./FilledButton";
|
||||
import { copyIcon, tablerCheckIcon } from "./icons";
|
||||
import { useCopyStatus } from "../hooks/useCopiedIndicator";
|
||||
import { copyIcon } from "./icons";
|
||||
|
||||
import "./ShareableLinkDialog.scss";
|
||||
|
||||
|
@ -24,7 +24,7 @@ export const ShareableLinkDialog = ({
|
|||
setErrorMessage,
|
||||
}: ShareableLinkDialogProps) => {
|
||||
const { t } = useI18n();
|
||||
const [justCopied, setJustCopied] = useState(false);
|
||||
const [, setJustCopied] = useState(false);
|
||||
const timerRef = useRef<number>(0);
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
|
@ -46,7 +46,7 @@ export const ShareableLinkDialog = ({
|
|||
|
||||
ref.current?.select();
|
||||
};
|
||||
|
||||
const { onCopy, copyStatus } = useCopyStatus();
|
||||
return (
|
||||
<Dialog onCloseRequest={onCloseRequest} title={false} size="small">
|
||||
<div className="ShareableLinkDialog">
|
||||
|
@ -60,26 +60,16 @@ export const ShareableLinkDialog = ({
|
|||
value={link}
|
||||
selectOnRender
|
||||
/>
|
||||
<Popover.Root open={justCopied}>
|
||||
<Popover.Trigger asChild>
|
||||
<FilledButton
|
||||
size="large"
|
||||
label="Copy link"
|
||||
icon={copyIcon}
|
||||
onClick={copyRoomLink}
|
||||
/>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
||||
className="ShareableLinkDialog__popover"
|
||||
side="top"
|
||||
align="end"
|
||||
sideOffset={5.5}
|
||||
>
|
||||
{tablerCheckIcon} copied
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
<FilledButton
|
||||
size="large"
|
||||
label={t("buttons.copyLink")}
|
||||
icon={copyIcon}
|
||||
status={copyStatus}
|
||||
onClick={() => {
|
||||
onCopy();
|
||||
copyRoomLink();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="ShareableLinkDialog__description">
|
||||
🔒 {t("alerts.uploadedSecurly")}
|
||||
|
|
|
@ -6,16 +6,18 @@ const Spinner = ({
|
|||
size = "1em",
|
||||
circleWidth = 8,
|
||||
synchronized = false,
|
||||
className = "",
|
||||
}: {
|
||||
size?: string | number;
|
||||
circleWidth?: number;
|
||||
synchronized?: boolean;
|
||||
className?: string;
|
||||
}) => {
|
||||
const mountTime = React.useRef(Date.now());
|
||||
const mountDelay = -(mountTime.current % 1600);
|
||||
|
||||
return (
|
||||
<div className="Spinner">
|
||||
<div className={`Spinner ${className}`}>
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
style={{
|
||||
|
|
|
@ -129,8 +129,14 @@
|
|||
--color-muted-background-darker: var(--color-gray-100);
|
||||
|
||||
--color-promo: var(--color-primary);
|
||||
--color-success: #268029;
|
||||
--color-success-lighter: #cafccc;
|
||||
|
||||
--color-success: #cafccc;
|
||||
--color-success-darker: #bafabc;
|
||||
--color-success-darkest: #a5eba8;
|
||||
--color-success-text: #268029;
|
||||
--color-success-contrast: #65bb6a;
|
||||
--color-success-contrast-hover: #6bcf70;
|
||||
--color-success-contrast-active: #6edf74;
|
||||
|
||||
--color-logo-icon: var(--color-primary);
|
||||
--color-logo-text: #190064;
|
||||
|
|
22
packages/excalidraw/hooks/useCopiedIndicator.ts
Normal file
22
packages/excalidraw/hooks/useCopiedIndicator.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { useRef, useState } from "react";
|
||||
|
||||
const TIMEOUT = 2000;
|
||||
|
||||
export const useCopyStatus = () => {
|
||||
const [copyStatus, setCopyStatus] = useState<"success" | null>(null);
|
||||
const timeoutRef = useRef<number>(0);
|
||||
|
||||
const onCopy = () => {
|
||||
clearTimeout(timeoutRef.current);
|
||||
setCopyStatus("success");
|
||||
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
setCopyStatus(null);
|
||||
}, TIMEOUT);
|
||||
};
|
||||
|
||||
return {
|
||||
copyStatus,
|
||||
onCopy,
|
||||
};
|
||||
};
|
|
@ -168,6 +168,7 @@
|
|||
"exportImage": "Export image...",
|
||||
"export": "Save to...",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"copyLink": "Copy link",
|
||||
"save": "Save to current file",
|
||||
"saveAs": "Save as",
|
||||
"load": "Open",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue