diff --git a/excalidraw-app/collab/RoomDialog.tsx b/excalidraw-app/collab/RoomDialog.tsx deleted file mode 100644 index 74266d3d9..000000000 --- a/excalidraw-app/collab/RoomDialog.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import { useRef, useState } from "react"; -import * as Popover from "@radix-ui/react-popover"; - -import { copyTextToSystemClipboard } from "../../packages/excalidraw/clipboard"; -import { trackEvent } from "../../packages/excalidraw/analytics"; -import { getFrame } from "../../packages/excalidraw/utils"; -import { useI18n } from "../../packages/excalidraw/i18n"; -import { KEYS } from "../../packages/excalidraw/keys"; - -import { Dialog } from "../../packages/excalidraw/components/Dialog"; -import { - copyIcon, - playerPlayIcon, - playerStopFilledIcon, - share, - shareIOS, - shareWindows, - tablerCheckIcon, -} from "../../packages/excalidraw/components/icons"; -import { TextField } from "../../packages/excalidraw/components/TextField"; -import { FilledButton } from "../../packages/excalidraw/components/FilledButton"; - -import { ReactComponent as CollabImage } from "../../packages/excalidraw/assets/lock.svg"; -import "./RoomDialog.scss"; - -const getShareIcon = () => { - const navigator = window.navigator as any; - const isAppleBrowser = /Apple/.test(navigator.vendor); - const isWindowsBrowser = navigator.appVersion.indexOf("Win") !== -1; - - if (isAppleBrowser) { - return shareIOS; - } else if (isWindowsBrowser) { - return shareWindows; - } - - return share; -}; - -export type RoomModalProps = { - handleClose: () => void; - activeRoomLink: string; - username: string; - onUsernameChange: (username: string) => void; - onRoomCreate: () => void; - onRoomDestroy: () => void; - setErrorMessage: (message: string) => void; -}; - -export const RoomModal = ({ - activeRoomLink, - onRoomCreate, - onRoomDestroy, - setErrorMessage, - username, - onUsernameChange, - handleClose, -}: RoomModalProps) => { - const { t } = useI18n(); - const [justCopied, setJustCopied] = useState(false); - const timerRef = useRef(0); - const ref = useRef(null); - const isShareSupported = "share" in navigator; - - const copyRoomLink = async () => { - try { - await copyTextToSystemClipboard(activeRoomLink); - } catch (e) { - setErrorMessage(t("errors.copyToSystemClipboardFailed")); - } - setJustCopied(true); - - if (timerRef.current) { - window.clearTimeout(timerRef.current); - } - - timerRef.current = window.setTimeout(() => { - setJustCopied(false); - }, 3000); - - ref.current?.select(); - }; - - const shareRoomLink = async () => { - try { - await navigator.share({ - title: t("roomDialog.shareTitle"), - text: t("roomDialog.shareTitle"), - url: activeRoomLink, - }); - } catch (error: any) { - // Just ignore. - } - }; - - if (activeRoomLink) { - return ( - <> -

- {t("labels.liveCollaboration")} -

- event.key === KEYS.ENTER && handleClose()} - /> -
- - {isShareSupported && ( - - )} - - - - - event.preventDefault()} - onCloseAutoFocus={(event) => event.preventDefault()} - className="RoomDialog__popover" - side="top" - align="end" - sideOffset={5.5} - > - {tablerCheckIcon} copied - - -
-
-

- - {t("roomDialog.desc_privacy")} -

-

{t("roomDialog.desc_exitSession")}

-
- -
- { - trackEvent("share", "room closed"); - onRoomDestroy(); - }} - /> -
- - ); - } - - return ( - <> -
- -
-
- {t("labels.liveCollaboration")} -
- -
- {t("roomDialog.desc_intro")} - {t("roomDialog.desc_privacy")} -
- -
- { - trackEvent("share", "room creation", `ui (${getFrame()})`); - onRoomCreate(); - }} - /> -
- - ); -}; - -const RoomDialog = (props: RoomModalProps) => { - return ( - -
- -
-
- ); -}; - -export default RoomDialog; diff --git a/excalidraw-app/share/ShareDialog.scss b/excalidraw-app/share/ShareDialog.scss index 87fde8491..436f41124 100644 --- a/excalidraw-app/share/ShareDialog.scss +++ b/excalidraw-app/share/ShareDialog.scss @@ -58,8 +58,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; diff --git a/excalidraw-app/share/ShareDialog.tsx b/excalidraw-app/share/ShareDialog.tsx index 6511eec12..d0a078cd2 100644 --- a/excalidraw-app/share/ShareDialog.tsx +++ b/excalidraw-app/share/ShareDialog.tsx @@ -1,5 +1,4 @@ import { useEffect, useRef, useState } from "react"; -import * as Popover from "@radix-ui/react-popover"; import { copyTextToSystemClipboard } from "../../packages/excalidraw/clipboard"; import { trackEvent } from "../../packages/excalidraw/analytics"; import { getFrame } from "../../packages/excalidraw/utils"; @@ -14,7 +13,6 @@ import { share, shareIOS, shareWindows, - tablerCheckIcon, } from "../../packages/excalidraw/components/icons"; import { TextField } from "../../packages/excalidraw/components/TextField"; import { FilledButton } from "../../packages/excalidraw/components/FilledButton"; @@ -24,6 +22,7 @@ import { atom, useAtom, useAtomValue } from "jotai"; import "./ShareDialog.scss"; import { useUIAppState } from "../../packages/excalidraw/context/ui-appState"; +import { useCopyStatus } from "../../packages/excalidraw/hooks/useCopiedIndicator"; type OnExportToBackend = () => void; type ShareDialogType = "share" | "collaborationOnly"; @@ -63,10 +62,11 @@ const ActiveRoomDialog = ({ handleClose: () => void; }) => { const { t } = useI18n(); - const [justCopied, setJustCopied] = useState(false); + const [, setJustCopied] = useState(false); const timerRef = useRef(0); const ref = useRef(null); const isShareSupported = "share" in navigator; + const { onCopy, copyStatus } = useCopyStatus(); const copyRoomLink = async () => { try { @@ -130,26 +130,16 @@ const ActiveRoomDialog = ({ onClick={shareRoomLink} /> )} - - - - - event.preventDefault()} - onCloseAutoFocus={(event) => event.preventDefault()} - className="ShareDialog__popover" - side="top" - align="end" - sideOffset={5.5} - > - {tablerCheckIcon} copied - - + { + copyRoomLink(); + onCopy(); + }} + />

diff --git a/packages/excalidraw/components/FilledButton.scss b/packages/excalidraw/components/FilledButton.scss index d23c9d104..771f36403 100644 --- a/packages/excalidraw/components/FilledButton.scss +++ b/packages/excalidraw/components/FilledButton.scss @@ -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); diff --git a/packages/excalidraw/components/FilledButton.tsx b/packages/excalidraw/components/FilledButton.tsx index ff17db623..136090848 100644 --- a/packages/excalidraw/components/FilledButton.tsx +++ b/packages/excalidraw/components/FilledButton.tsx @@ -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( size = "medium", fullWidth, className, + status, }, ref, ) => { @@ -46,8 +54,11 @@ export const FilledButton = forwardRef( 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( console.warn(error); } } finally { + clearTimeout(timer); setIsLoading(false); } } }; + const _status = isLoading ? "loading" : status; + color = _status === "success" ? "success" : color; + return (