소스 검색

feat: new Live Collaboration Component API (#6104)

* feat: new Live Collaboration Component API

* namespace export icons into `icons` dictionary and lowercase

* update readme and changelog

* review fixes

* fix

* fix

* update docs

* remove

* allow button rest props

* update docs

* docs

* add `WelcomeScreen.Center.MenuItemLiveCollaborationTrigger`

* fix lint

* update changelog

Co-authored-by: dwelle <luzar.david@gmail.com>
Aakansha Doshi 2 년 전
부모
커밋
faad8a65f1

+ 1 - 3
src/components/App.tsx

@@ -539,8 +539,7 @@ class App extends React.Component<AppProps, AppState> {
       this.scene.getNonDeletedElements(),
       this.state,
     );
-    const { onCollabButtonClick, renderTopRightUI, renderCustomStats } =
-      this.props;
+    const { renderTopRightUI, renderCustomStats } = this.props;
 
     return (
       <div
@@ -574,7 +573,6 @@ class App extends React.Component<AppProps, AppState> {
                       setAppState={this.setAppState}
                       actionManager={this.actionManager}
                       elements={this.scene.getNonDeletedElements()}
-                      onCollabButtonClick={onCollabButtonClick}
                       onLockToggle={this.toggleLock}
                       onPenModeToggle={this.togglePenMode}
                       onInsertElements={(elements) =>

+ 0 - 33
src/components/CollabButton.tsx

@@ -1,33 +0,0 @@
-import { t } from "../i18n";
-import { UsersIcon } from "./icons";
-
-import "./CollabButton.scss";
-import clsx from "clsx";
-import { Button } from "./Button";
-
-const CollabButton = ({
-  isCollaborating,
-  collaboratorCount,
-  onClick,
-}: {
-  isCollaborating: boolean;
-  collaboratorCount: number;
-  onClick: () => void;
-}) => {
-  return (
-    <Button
-      className={clsx("collab-button", { active: isCollaborating })}
-      type="button"
-      onSelect={onClick}
-      style={{ position: "relative" }}
-      title={t("labels.liveCollaboration")}
-    >
-      {UsersIcon}
-      {collaboratorCount > 0 && (
-        <div className="CollabButton-collaborators">{collaboratorCount}</div>
-      )}
-    </Button>
-  );
-};
-
-export default CollabButton;

+ 0 - 16
src/components/LayerUI.tsx

@@ -18,7 +18,6 @@ import {
 } from "../types";
 import { isShallowEqual, muteFSAbortError, getReactChildren } from "../utils";
 import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
-import CollabButton from "./CollabButton";
 import { ErrorDialog } from "./ErrorDialog";
 import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
 import { FixedSideContainer } from "./FixedSideContainer";
@@ -59,7 +58,6 @@ interface LayerUIProps {
   canvas: HTMLCanvasElement | null;
   setAppState: React.Component<any, AppState>["setState"];
   elements: readonly NonDeletedExcalidrawElement[];
-  onCollabButtonClick?: () => void;
   onLockToggle: () => void;
   onPenModeToggle: () => void;
   onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
@@ -86,7 +84,6 @@ const LayerUI = ({
   setAppState,
   elements,
   canvas,
-  onCollabButtonClick,
   onLockToggle,
   onPenModeToggle,
   onInsertElements,
@@ -207,12 +204,6 @@ const LayerUI = ({
           {UIOptions.canvasActions.saveAsImage && (
             <MainMenu.DefaultItems.SaveAsImage />
           )}
-          {onCollabButtonClick && (
-            <MainMenu.DefaultItems.LiveCollaboration
-              onSelect={onCollabButtonClick}
-              isCollaborating={isCollaborating}
-            />
-          )}
           <MainMenu.DefaultItems.Help />
           <MainMenu.DefaultItems.ClearCanvas />
           <MainMenu.Separator />
@@ -351,13 +342,6 @@ const LayerUI = ({
             )}
           >
             <UserList collaborators={appState.collaborators} />
-            {onCollabButtonClick && (
-              <CollabButton
-                isCollaborating={isCollaborating}
-                collaboratorCount={appState.collaborators.size}
-                onClick={onCollabButtonClick}
-              />
-            )}
             {renderTopRightUI?.(device.isMobile, appState)}
             {!appState.viewModeEnabled && (
               <LibraryButton appState={appState} setAppState={setAppState} />

+ 1 - 1
src/components/icons.tsx

@@ -883,7 +883,7 @@ export const CenterHorizontallyIcon = createIcon(
   modifiedTablerIconProps,
 );
 
-export const UsersIcon = createIcon(
+export const usersIcon = createIcon(
   <g strokeWidth="1.5">
     <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
     <circle cx="9" cy="7" r="4"></circle>

+ 1 - 1
src/components/CollabButton.scss → src/components/live-collaboration/LiveCollaborationTrigger.scss

@@ -1,4 +1,4 @@
-@import "../css/variables.module";
+@import "../../css/variables.module";
 
 .excalidraw {
   .collab-button {

+ 40 - 0
src/components/live-collaboration/LiveCollaborationTrigger.tsx

@@ -0,0 +1,40 @@
+import { t } from "../../i18n";
+import { usersIcon } from "../icons";
+import { Button } from "../Button";
+
+import clsx from "clsx";
+import { useExcalidrawAppState } from "../App";
+
+import "./LiveCollaborationTrigger.scss";
+
+const LiveCollaborationTrigger = ({
+  isCollaborating,
+  onSelect,
+  ...rest
+}: {
+  isCollaborating: boolean;
+  onSelect: () => void;
+} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
+  const appState = useExcalidrawAppState();
+
+  return (
+    <Button
+      {...rest}
+      className={clsx("collab-button", { active: isCollaborating })}
+      type="button"
+      onSelect={onSelect}
+      style={{ position: "relative" }}
+      title={t("labels.liveCollaboration")}
+    >
+      {usersIcon}
+      {appState.collaborators.size > 0 && (
+        <div className="CollabButton-collaborators">
+          {appState.collaborators.size}
+        </div>
+      )}
+    </Button>
+  );
+};
+
+export default LiveCollaborationTrigger;
+LiveCollaborationTrigger.displayName = "LiveCollaborationTrigger";

+ 5 - 5
src/components/main-menu/DefaultItems.tsx

@@ -1,4 +1,3 @@
-import clsx from "clsx";
 import { getShortcutFromShortcutName } from "../../actions/shortcuts";
 import { t } from "../../i18n";
 import {
@@ -15,7 +14,7 @@ import {
   save,
   SunIcon,
   TrashIcon,
-  UsersIcon,
+  usersIcon,
 } from "../icons";
 import { GithubIcon, DiscordIcon, TwitterIcon } from "../icons";
 import DropdownMenuItem from "../dropdownMenu/DropdownMenuItem";
@@ -31,6 +30,7 @@ import {
 import "./DefaultItems.scss";
 import { useState } from "react";
 import ConfirmDialog from "../ConfirmDialog";
+import clsx from "clsx";
 
 export const LoadScene = () => {
   // FIXME Hack until we tie "t" to lang state
@@ -258,7 +258,7 @@ export const Socials = () => (
 );
 Socials.displayName = "Socials";
 
-export const LiveCollaboration = ({
+export const LiveCollaborationTrigger = ({
   onSelect,
   isCollaborating,
 }: {
@@ -271,7 +271,7 @@ export const LiveCollaboration = ({
   return (
     <DropdownMenuItem
       data-testid="collab-button"
-      icon={UsersIcon}
+      icon={usersIcon}
       className={clsx({
         "active-collab": isCollaborating,
       })}
@@ -282,4 +282,4 @@ export const LiveCollaboration = ({
   );
 };
 
-LiveCollaboration.displayName = "LiveCollaboration";
+LiveCollaborationTrigger.displayName = "LiveCollaborationTrigger";

+ 20 - 1
src/components/welcome-screen/WelcomeScreen.Center.tsx

@@ -6,7 +6,7 @@ import {
   useExcalidrawActionManager,
   useExcalidrawAppState,
 } from "../App";
-import { ExcalLogo, HelpIcon, LoadIcon } from "../icons";
+import { ExcalLogo, HelpIcon, LoadIcon, usersIcon } from "../icons";
 
 const WelcomeScreenMenuItemContent = ({
   icon,
@@ -163,6 +163,24 @@ const MenuItemLoadScene = () => {
 };
 MenuItemLoadScene.displayName = "MenuItemLoadScene";
 
+const MenuItemLiveCollaborationTrigger = ({
+  onSelect,
+}: {
+  onSelect: () => any;
+}) => {
+  // FIXME when we tie t() to lang state
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  const appState = useExcalidrawAppState();
+
+  return (
+    <WelcomeScreenMenuItem shortcut={null} onSelect={onSelect} icon={usersIcon}>
+      {t("labels.liveCollaboration")}
+    </WelcomeScreenMenuItem>
+  );
+};
+MenuItemLiveCollaborationTrigger.displayName =
+  "MenuItemLiveCollaborationTrigger";
+
 // -----------------------------------------------------------------------------
 
 Center.Logo = Logo;
@@ -172,5 +190,6 @@ Center.MenuItem = WelcomeScreenMenuItem;
 Center.MenuItemLink = WelcomeScreenMenuItemLink;
 Center.MenuItemHelp = MenuItemHelp;
 Center.MenuItemLoadScene = MenuItemLoadScene;
+Center.MenuItemLiveCollaborationTrigger = MenuItemLiveCollaborationTrigger;
 
 export { Center };

+ 17 - 11
src/excalidraw-app/index.tsx

@@ -26,6 +26,7 @@ import {
   defaultLang,
   Footer,
   MainMenu,
+  LiveCollaborationTrigger,
   WelcomeScreen,
 } from "../packages/excalidraw/index";
 import {
@@ -87,7 +88,7 @@ import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
 import { EncryptedIcon } from "./components/EncryptedIcon";
 import { ExcalidrawPlusAppLink } from "./components/ExcalidrawPlusAppLink";
 import { LanguageList } from "./components/LanguageList";
-import { PlusPromoIcon, UsersIcon } from "../components/icons";
+import { PlusPromoIcon } from "../components/icons";
 
 polyfill();
 
@@ -610,7 +611,7 @@ const ExcalidrawWrapper = () => {
         <MainMenu.DefaultItems.SaveToActiveFile />
         <MainMenu.DefaultItems.Export />
         <MainMenu.DefaultItems.SaveAsImage />
-        <MainMenu.DefaultItems.LiveCollaboration
+        <MainMenu.DefaultItems.LiveCollaborationTrigger
           isCollaborating={isCollaborating}
           onSelect={() => setCollabDialogShown(true)}
         />
@@ -675,15 +676,9 @@ const ExcalidrawWrapper = () => {
           <WelcomeScreen.Center.Menu>
             <WelcomeScreen.Center.MenuItemLoadScene />
             <WelcomeScreen.Center.MenuItemHelp />
-
-            <WelcomeScreen.Center.MenuItem
-              shortcut={null}
+            <WelcomeScreen.Center.MenuItemLiveCollaborationTrigger
               onSelect={() => setCollabDialogShown(true)}
-              icon={UsersIcon}
-            >
-              {t("labels.liveCollaboration")}
-            </WelcomeScreen.Center.MenuItem>
-
+            />
             {!isExcalidrawPlusSignedUser && (
               <WelcomeScreen.Center.MenuItemLink
                 href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
@@ -710,7 +705,6 @@ const ExcalidrawWrapper = () => {
         ref={excalidrawRefCallback}
         onChange={onChange}
         initialData={initialStatePromiseRef.current.promise}
-        onCollabButtonClick={() => setCollabDialogShown(true)}
         isCollaborating={isCollaborating}
         onPointerUpdate={collabAPI?.onPointerUpdate}
         UIOptions={{
@@ -744,8 +738,20 @@ const ExcalidrawWrapper = () => {
         onLibraryChange={onLibraryChange}
         autoFocus={true}
         theme={theme}
+        renderTopRightUI={(isMobile) => {
+          if (isMobile) {
+            return null;
+          }
+          return (
+            <LiveCollaborationTrigger
+              isCollaborating={isCollaborating}
+              onSelect={() => setCollabDialogShown(true)}
+            />
+          );
+        }}
       >
         {renderMenu()}
+
         <Footer>
           <div style={{ display: "flex", gap: ".5rem", alignItems: "center" }}>
             <ExcalidrawPlusAppLink />

+ 4 - 1
src/packages/excalidraw/CHANGELOG.md

@@ -25,8 +25,11 @@ Please add the latest change on the top under the correct section.
 
 - Any top-level children passed to the `<Excalidraw/>` component that do not belong to one of the officially supported Excalidraw children components are now rendered directly inside the Excalidraw container (previously, they weren't rendered at all) [#6096](https://github.com/excalidraw/excalidraw/pull/6096).
 
-#### BREAKING CHANGE
+- Expose [LiveCollaborationTrigger](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#LiveCollaborationTrigger) component. Replaces `props.onCollabButtonClick` [#6104](https://github.com/excalidraw/excalidraw/pull/6104).
+
+#### BREAKING CHANGES
 
+- `props.onCollabButtonClick` is now removed. You need to render the main menu item yourself, and optionally also render the `<LiveCollaborationTrigger>` component using [renderTopRightUI](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#renderTopRightUI) prop if you want to retain the canvas button at top-right.
 - The prop `renderFooter` is now removed in favor of rendering as a child component.
 
 ### Excalidraw schema

+ 31 - 9
src/packages/excalidraw/README.md

@@ -138,9 +138,6 @@ export default function App() {
             console.log("Elements :", elements, "State : ", state)
           }
           onPointerUpdate={(payload) => console.log(payload)}
-          onCollabButtonClick={() =>
-            window.alert("You clicked on collab button")
-          }
           viewModeEnabled={viewModeEnabled}
           zenModeEnabled={zenModeEnabled}
           gridModeEnabled={gridModeEnabled}
@@ -331,7 +328,6 @@ const App = () => {
         onChange: (elements, state) =>
           console.log("Elements :", elements, "State : ", state),
         onPointerUpdate: (payload) => console.log(payload),
-        onCollabButtonClick: () => window.alert("You clicked on collab button"),
         viewModeEnabled: viewModeEnabled,
         zenModeEnabled: zenModeEnabled,
         gridModeEnabled: gridModeEnabled,
@@ -655,6 +651,7 @@ The default menu items are:
 
 - `<WelcomeScreen.Center.MenuItemHelp/>` - opens the help dialog.
 - `<WelcomeScreen.Center.MenuItemLoadScene/>` - open the load file dialog.
+- `<WelcomeScreen.Center.MenuItemLiveCollaborationTrigger/>` - intended to open the live collaboration dialog. Works similarly to [`<LiveCollaborationTrigger>`](#LiveCollaborationTrigger) and you must supply `onSelect()` handler to integrate with your collaboration implementation.
 
 **Usage**
 
@@ -719,6 +716,36 @@ Hint for the toolbar. Supply `children` to customize the hint text.
 
 Hint for the help dialog. Supply `children` to customize the hint text.
 
+### LiveCollaborationTrigger
+
+If you implement live collaboration support and want to expose the same UI button as on excalidraw.com, you can render the `<LiveCollaborationTrigger>` component using the [renderTopRightUI](#rendertoprightui) prop. You'll need to supply `onSelect()` to handle opening of your collaboration dialog, but the button will display current `appState.collaborators` count for you.
+
+| Prop | Type | Required | Default | Description |
+| --- | --- | --- | --- | --- |
+| `onSelect` | `() => any` | Yes |  | Handler called when the user click on the button |
+| `isCollaborating` | `boolean` | Yes | false | Whether live collaboration session is in effect. Modifies button style. |
+
+**Usage**
+
+```jsx
+import { LiveCollaborationTrigger } from "@excalidraw/excalidraw";
+const App = () => (
+  <Excalidraw
+    renderTopRightUI={(isMobile) => {
+      if (isMobile) {
+        return null;
+      }
+      return (
+        <LiveCollaborationTrigger
+          isCollaborating={isCollaborating}
+          onSelect={() => setCollabDialogShown(true)}
+        />
+      );
+    }}
+  />
+);
+```
+
 ### Props
 
 | Name | Type | Default | Description |
@@ -726,7 +753,6 @@ Hint for the help dialog. Supply `children` to customize the hint text.
 | [`onChange`](#onChange) | Function |  | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw elements and the current app state. |
 | [`initialData`](#initialData) | <code>{elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L106">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L79">AppState<a> } </code> | null | The initial data with which app loads. |
 | [`ref`](#ref) | [`createRef`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs) &#124; [`useRef`](https://reactjs.org/docs/hooks-reference.html#useref) &#124; [`callbackRef`](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs) &#124; <code>{ current: { readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L317">resolvablePromise</a> } }</code> |  | Ref to be passed to Excalidraw |
-| [`onCollabButtonClick`](#onCollabButtonClick) | Function |  | Callback to be triggered when the collab button is clicked |
 | [`isCollaborating`](#isCollaborating) | `boolean` |  | This implies if the app is in collaboration mode |
 | [`onPointerUpdate`](#onPointerUpdate) | Function |  | Callback triggered when mouse pointer is updated. |
 | [`langCode`](#langCode) | string | `en` | Language code string |
@@ -900,10 +926,6 @@ You can use this function to update the library. It accepts the below attributes
 
 Adds supplied files data to the `appState.files` cache on top of existing files present in the cache.
 
-#### `onCollabButtonClick`
-
-This callback is triggered when clicked on the collab button in excalidraw. If not supplied, the collab dialog button is not rendered.
-
 #### `isCollaborating`
 
 This prop indicates if the app is in collaboration mode.

+ 28 - 21
src/packages/excalidraw/example/App.tsx

@@ -72,24 +72,13 @@ const {
   Sidebar,
   Footer,
   MainMenu,
+  LiveCollaborationTrigger,
 } = window.ExcalidrawLib;
 
 const COMMENT_ICON_DIMENSION = 32;
 const COMMENT_INPUT_HEIGHT = 50;
 const COMMENT_INPUT_WIDTH = 150;
 
-const renderTopRightUI = () => {
-  return (
-    <button
-      onClick={() => alert("This is dummy top right UI")}
-      style={{ height: "2.5rem" }}
-    >
-      {" "}
-      Click me{" "}
-    </button>
-  );
-};
-
 export default function App() {
   const appRef = useRef<any>(null);
   const [viewModeEnabled, setViewModeEnabled] = useState(false);
@@ -148,6 +137,28 @@ export default function App() {
     fetchData();
   }, [excalidrawAPI]);
 
+  const renderTopRightUI = (isMobile: boolean) => {
+    return (
+      <>
+        {!isMobile && (
+          <LiveCollaborationTrigger
+            isCollaborating={isCollaborating}
+            onSelect={() => {
+              window.alert("Collab dialog clicked");
+            }}
+          />
+        )}
+        <button
+          onClick={() => alert("This is dummy top right UI")}
+          style={{ height: "2.5rem" }}
+        >
+          {" "}
+          Click me{" "}
+        </button>
+      </>
+    );
+  };
+
   const loadSceneOrLibrary = async () => {
     const file = await fileOpen({ description: "Excalidraw or library file" });
     const contents = await loadSceneOrLibraryFromBlob(file, null, null);
@@ -489,12 +500,10 @@ export default function App() {
         <MainMenu.DefaultItems.SaveAsImage />
         <MainMenu.DefaultItems.Export />
         <MainMenu.Separator />
-        {isCollaborating && (
-          <MainMenu.DefaultItems.LiveCollaboration
-            onSelect={() => window.alert("You clicked on collab button")}
-            isCollaborating={isCollaborating}
-          />
-        )}
+        <MainMenu.DefaultItems.LiveCollaborationTrigger
+          isCollaborating={isCollaborating}
+          onSelect={() => window.alert("You clicked on collab button")}
+        />
         <MainMenu.Group title="Excalidraw links">
           <MainMenu.DefaultItems.Socials />
         </MainMenu.Group>
@@ -508,6 +517,7 @@ export default function App() {
           </button>
         </MainMenu.ItemCustom>
         <MainMenu.DefaultItems.Help />
+
         {excalidrawAPI && <MobileFooter excalidrawAPI={excalidrawAPI} />}
       </MainMenu>
     );
@@ -677,9 +687,6 @@ export default function App() {
               button: "down" | "up";
               pointersMap: Gesture["pointers"];
             }) => setPointerData(payload)}
-            onCollabButtonClick={() =>
-              window.alert("You clicked on collab button")
-            }
             viewModeEnabled={viewModeEnabled}
             zenModeEnabled={zenModeEnabled}
             gridModeEnabled={gridModeEnabled}

+ 2 - 2
src/packages/excalidraw/index.tsx

@@ -14,13 +14,13 @@ import { jotaiScope, jotaiStore } from "../../jotai";
 import Footer from "../../components/footer/FooterCenter";
 import MainMenu from "../../components/main-menu/MainMenu";
 import WelcomeScreen from "../../components/welcome-screen/WelcomeScreen";
+import LiveCollaborationTrigger from "../../components/live-collaboration/LiveCollaborationTrigger";
 
 const ExcalidrawBase = (props: ExcalidrawProps) => {
   const {
     onChange,
     initialData,
     excalidrawRef,
-    onCollabButtonClick,
     isCollaborating = false,
     onPointerUpdate,
     renderTopRightUI,
@@ -94,7 +94,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
           onChange={onChange}
           initialData={initialData}
           excalidrawRef={excalidrawRef}
-          onCollabButtonClick={onCollabButtonClick}
           isCollaborating={isCollaborating}
           onPointerUpdate={onPointerUpdate}
           renderTopRightUI={renderTopRightUI}
@@ -246,3 +245,4 @@ export { Footer };
 export { MainMenu };
 export { useDevice } from "../../components/App";
 export { WelcomeScreen };
+export { LiveCollaborationTrigger };

+ 0 - 1
src/types.ts

@@ -287,7 +287,6 @@ export interface ExcalidrawProps {
     | null
     | Promise<ExcalidrawInitialDataState | null>;
   excalidrawRef?: ForwardRef<ExcalidrawAPIRefValue>;
-  onCollabButtonClick?: () => void;
   isCollaborating?: boolean;
   onPointerUpdate?: (payload: {
     pointer: { x: number; y: number };