feat: render library into Sidebar on mobile (#5774)

This commit is contained in:
David Luzar 2022-10-18 06:59:14 +02:00 committed by GitHub
parent e9067de173
commit 941b2d7042
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 692 additions and 668 deletions

View File

@ -1,12 +1,12 @@
import clsx from "clsx"; import clsx from "clsx";
import React, { useCallback } from "react"; import React from "react";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { CLASSES, LIBRARY_SIDEBAR_WIDTH } from "../constants"; import { CLASSES, LIBRARY_SIDEBAR_WIDTH } from "../constants";
import { exportCanvas } from "../data"; import { exportCanvas } from "../data";
import { isTextElement, showSelectedShapeActions } from "../element"; import { isTextElement, showSelectedShapeActions } from "../element";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { Language, t } from "../i18n"; import { Language, t } from "../i18n";
import { calculateScrollCenter, getSelectedElements } from "../scene"; import { calculateScrollCenter } from "../scene";
import { ExportType } from "../scene/types"; import { ExportType } from "../scene/types";
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types"; import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
import { muteFSAbortError } from "../utils"; import { muteFSAbortError } from "../utils";
@ -26,7 +26,7 @@ import { Section } from "./Section";
import { HelpDialog } from "./HelpDialog"; import { HelpDialog } from "./HelpDialog";
import Stack from "./Stack"; import Stack from "./Stack";
import { UserList } from "./UserList"; import { UserList } from "./UserList";
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library"; import Library from "../data/library";
import { JSONExportDialog } from "./JSONExportDialog"; import { JSONExportDialog } from "./JSONExportDialog";
import { LibraryButton } from "./LibraryButton"; import { LibraryButton } from "./LibraryButton";
import { isImageFileHandle } from "../data/blob"; import { isImageFileHandle } from "../data/blob";
@ -40,7 +40,7 @@ import { useDevice } from "../components/App";
import { Stats } from "./Stats"; import { Stats } from "./Stats";
import { actionToggleStats } from "../actions/actionToggleStats"; import { actionToggleStats } from "../actions/actionToggleStats";
import Footer from "./Footer"; import Footer from "./Footer";
import { hostSidebarCountersAtom, Sidebar } from "./Sidebar/Sidebar"; import { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
import { jotaiScope } from "../jotai"; import { jotaiScope } from "../jotai";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
@ -247,42 +247,6 @@ const LayerUI = ({
</Section> </Section>
); );
const closeLibrary = useCallback(() => {
const isDialogOpen = !!document.querySelector(".Dialog");
// Prevent closing if any dialog is open
if (isDialogOpen) {
return;
}
setAppState({ openSidebar: null });
}, [setAppState]);
const deselectItems = useCallback(() => {
setAppState({
selectedElementIds: {},
selectedGroupIds: {},
});
}, [setAppState]);
const libraryMenu =
appState.openSidebar === "library" ? (
<LibraryMenu
pendingElements={getSelectedElements(elements, appState, true)}
onClose={closeLibrary}
onInsertLibraryItems={(libraryItems) => {
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
}}
onAddToLibrary={deselectItems}
setAppState={setAppState}
libraryReturnUrl={libraryReturnUrl}
focusContainer={focusContainer}
library={library}
files={files}
id={id}
appState={appState}
/>
) : null;
const renderFixedSideContainer = () => { const renderFixedSideContainer = () => {
const shouldRenderSelectedShapeActions = showSelectedShapeActions( const shouldRenderSelectedShapeActions = showSelectedShapeActions(
appState, appState,
@ -381,6 +345,21 @@ const LayerUI = ({
); );
}; };
const renderSidebars = () => {
return appState.openSidebar === "customSidebar" ? (
renderCustomSidebar?.() || null
) : appState.openSidebar === "library" ? (
<LibraryMenu
appState={appState}
onInsertElements={onInsertElements}
libraryReturnUrl={libraryReturnUrl}
focusContainer={focusContainer}
library={library}
id={id}
/>
) : null;
};
const [hostSidebarCounters] = useAtom(hostSidebarCountersAtom, jotaiScope); const [hostSidebarCounters] = useAtom(hostSidebarCountersAtom, jotaiScope);
return ( return (
@ -416,7 +395,6 @@ const LayerUI = ({
appState={appState} appState={appState}
elements={elements} elements={elements}
actionManager={actionManager} actionManager={actionManager}
libraryMenu={libraryMenu}
renderJSONExportDialog={renderJSONExportDialog} renderJSONExportDialog={renderJSONExportDialog}
renderImageExportDialog={renderImageExportDialog} renderImageExportDialog={renderImageExportDialog}
setAppState={setAppState} setAppState={setAppState}
@ -429,7 +407,7 @@ const LayerUI = ({
onImageAction={onImageAction} onImageAction={onImageAction}
renderTopRightUI={renderTopRightUI} renderTopRightUI={renderTopRightUI}
renderCustomStats={renderCustomStats} renderCustomStats={renderCustomStats}
renderCustomSidebar={renderCustomSidebar} renderSidebars={renderSidebars}
device={device} device={device}
/> />
)} )}
@ -484,26 +462,7 @@ const LayerUI = ({
</button> </button>
)} )}
</div> </div>
{appState.openSidebar === "customSidebar" ? ( {renderSidebars()}
renderCustomSidebar?.()
) : appState.openSidebar === "library" ? (
<Sidebar
__isInternal
// necessary to remount when switching between internal
// and custom (host app) sidebar, so that the `props.onClose`
// is colled correctly
key="library"
onDock={(docked) => {
trackEvent(
"library",
`toggleLibraryDock (${docked ? "dock" : "undock"})`,
`sidebar (${device.isMobile ? "mobile" : "desktop"})`,
);
}}
>
{libraryMenu}
</Sidebar>
) : null}
</> </>
)} )}
</> </>

View File

@ -1,10 +1,16 @@
@import "open-color/open-color"; @import "open-color/open-color";
.excalidraw { .excalidraw {
.layer-ui__library-sidebar {
display: flex;
flex-direction: column;
}
.layer-ui__library { .layer-ui__library {
display: flex; display: flex;
align-items: center; flex-direction: column;
justify-content: center;
flex: 1 1 auto;
.layer-ui__library-header { .layer-ui__library-header {
display: flex; display: flex;
@ -23,16 +29,100 @@
} }
.layer-ui__sidebar { .layer-ui__sidebar {
.layer-ui__library {
padding: 0;
height: 100%;
}
.library-menu-items-container { .library-menu-items-container {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
} }
.library-actions {
width: 100%;
display: flex;
margin-right: auto;
align-items: center;
button .library-actions-counter {
position: absolute;
right: 2px;
bottom: 2px;
border-radius: 50%;
width: 1em;
height: 1em;
padding: 1px;
font-size: 0.7rem;
background: #fff;
}
&--remove {
background-color: $oc-red-7;
&:hover {
background-color: $oc-red-8;
}
&:active {
background-color: $oc-red-9;
}
svg {
color: $oc-white;
}
.library-actions-counter {
color: $oc-red-7;
}
}
&--export {
background-color: $oc-lime-5;
&:hover {
background-color: $oc-lime-7;
}
&:active {
background-color: $oc-lime-8;
}
svg {
color: $oc-white;
}
.library-actions-counter {
color: $oc-lime-5;
}
}
&--publish {
background-color: $oc-cyan-6;
&:hover {
background-color: $oc-cyan-7;
}
&:active {
background-color: $oc-cyan-9;
}
svg {
color: $oc-white;
}
label {
margin-left: -0.2em;
margin-right: 1.1em;
color: $oc-white;
font-size: 0.86em;
}
.library-actions-counter {
color: $oc-cyan-6;
}
}
&--load {
background-color: $oc-blue-6;
&:hover {
background-color: $oc-blue-7;
}
&:active {
background-color: $oc-blue-9;
}
svg {
color: $oc-white;
}
}
}
.layer-ui__library-message { .layer-ui__library-message {
padding: 2em 4em; padding: 2em 4em;
min-width: 200px; min-width: 200px;

View File

@ -6,29 +6,31 @@ import {
RefObject, RefObject,
forwardRef, forwardRef,
} from "react"; } from "react";
import Library, { libraryItemsAtom } from "../data/library"; import Library, {
distributeLibraryItemsOnSquareGrid,
libraryItemsAtom,
} from "../data/library";
import { t } from "../i18n"; import { t } from "../i18n";
import { randomId } from "../random"; import { randomId } from "../random";
import { import { LibraryItems, LibraryItem, AppState, ExcalidrawProps } from "../types";
LibraryItems,
LibraryItem,
AppState,
BinaryFiles,
ExcalidrawProps,
} from "../types";
import { Dialog } from "./Dialog";
import PublishLibrary from "./PublishLibrary";
import { ToolButton } from "./ToolButton";
import "./LibraryMenu.scss"; import "./LibraryMenu.scss";
import LibraryMenuItems from "./LibraryMenuItems"; import LibraryMenuItems from "./LibraryMenuItems";
import { EVENT } from "../constants"; import { EVENT, VERSIONS } from "../constants";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { jotaiScope } from "../jotai"; import { jotaiScope } from "../jotai";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
import { useDevice } from "./App"; import {
useDevice,
useExcalidrawElements,
useExcalidrawSetAppState,
} from "./App";
import { Sidebar } from "./Sidebar/Sidebar";
import { getSelectedElements } from "../scene";
import { NonDeletedExcalidrawElement } from "../element/types";
import { LibraryMenuHeader } from "./LibraryMenuHeaderContent";
const useOnClickOutside = ( const useOnClickOutside = (
ref: RefObject<HTMLElement>, ref: RefObject<HTMLElement>,
@ -58,11 +60,6 @@ const useOnClickOutside = (
}, [ref, cb]); }, [ref, cb]);
}; };
const getSelectedItems = (
libraryItems: LibraryItems,
selectedItems: LibraryItem["id"][],
) => libraryItems.filter((item) => selectedItems.includes(item.id));
const LibraryMenuWrapper = forwardRef< const LibraryMenuWrapper = forwardRef<
HTMLDivElement, HTMLDivElement,
{ children: React.ReactNode } { children: React.ReactNode }
@ -74,94 +71,34 @@ const LibraryMenuWrapper = forwardRef<
); );
}); });
export const LibraryMenu = ({ export const LibraryMenuContent = ({
onClose,
onInsertLibraryItems, onInsertLibraryItems,
pendingElements, pendingElements,
onAddToLibrary, onAddToLibrary,
setAppState, setAppState,
files,
libraryReturnUrl, libraryReturnUrl,
focusContainer,
library, library,
id, id,
appState, appState,
selectedItems,
onSelectItems,
}: { }: {
pendingElements: LibraryItem["elements"]; pendingElements: LibraryItem["elements"];
onClose: () => void;
onInsertLibraryItems: (libraryItems: LibraryItems) => void; onInsertLibraryItems: (libraryItems: LibraryItems) => void;
onAddToLibrary: () => void; onAddToLibrary: () => void;
files: BinaryFiles;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
focusContainer: () => void;
library: Library; library: Library;
id: string; id: string;
appState: AppState; appState: AppState;
selectedItems: LibraryItem["id"][];
onSelectItems: (id: LibraryItem["id"][]) => void;
}) => { }) => {
const ref = useRef<HTMLDivElement | null>(null); const referrer =
libraryReturnUrl || window.location.origin + window.location.pathname;
const device = useDevice();
useOnClickOutside(
ref,
useCallback(
(event) => {
// If click on the library icon, do nothing so that LibraryButton
// can toggle library menu
if ((event.target as Element).closest(".ToolIcon__library")) {
return;
}
if (!appState.isSidebarDocked || !device.canDeviceFitSidebar) {
onClose();
}
},
[onClose, appState.isSidebarDocked, device.canDeviceFitSidebar],
),
);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === KEYS.ESCAPE &&
(!appState.isSidebarDocked || !device.canDeviceFitSidebar)
) {
onClose();
}
};
document.addEventListener(EVENT.KEYDOWN, handleKeyDown);
return () => {
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
};
}, [onClose, appState.isSidebarDocked, device.canDeviceFitSidebar]);
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
const [showPublishLibraryDialog, setShowPublishLibraryDialog] =
useState(false);
const [publishLibSuccess, setPublishLibSuccess] = useState<null | {
url: string;
authorName: string;
}>(null);
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope); const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
const removeFromLibrary = useCallback(
async (libraryItems: LibraryItems) => {
const nextItems = libraryItems.filter(
(item) => !selectedItems.includes(item.id),
);
library.setLibrary(nextItems).catch(() => {
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
});
setSelectedItems([]);
},
[library, setAppState, selectedItems, setSelectedItems],
);
const resetLibrary = useCallback(() => {
library.resetLibrary();
focusContainer();
}, [library, focusContainer]);
const addToLibrary = useCallback( const addToLibrary = useCallback(
async (elements: LibraryItem["elements"], libraryItems: LibraryItems) => { async (elements: LibraryItem["elements"], libraryItems: LibraryItems) => {
trackEvent("element", "addToLibrary", "ui"); trackEvent("element", "addToLibrary", "ui");
@ -187,60 +124,12 @@ export const LibraryMenu = ({
[onAddToLibrary, library, setAppState], [onAddToLibrary, library, setAppState],
); );
const renderPublishSuccess = useCallback(() => {
return (
<Dialog
onCloseRequest={() => setPublishLibSuccess(null)}
title={t("publishSuccessDialog.title")}
className="publish-library-success"
small={true}
>
<p>
{t("publishSuccessDialog.content", {
authorName: publishLibSuccess!.authorName,
})}{" "}
<a
href={publishLibSuccess?.url}
target="_blank"
rel="noopener noreferrer"
>
{t("publishSuccessDialog.link")}
</a>
</p>
<ToolButton
type="button"
title={t("buttons.close")}
aria-label={t("buttons.close")}
label={t("buttons.close")}
onClick={() => setPublishLibSuccess(null)}
data-testid="publish-library-success-close"
className="publish-library-success-close"
/>
</Dialog>
);
}, [setPublishLibSuccess, publishLibSuccess]);
const onPublishLibSuccess = useCallback(
(data: { url: string; authorName: string }, libraryItems: LibraryItems) => {
setShowPublishLibraryDialog(false);
setPublishLibSuccess({ url: data.url, authorName: data.authorName });
const nextLibItems = libraryItems.slice();
nextLibItems.forEach((libItem) => {
if (selectedItems.includes(libItem.id)) {
libItem.status = "published";
}
});
library.setLibrary(nextLibItems);
},
[setShowPublishLibraryDialog, setPublishLibSuccess, selectedItems, library],
);
if ( if (
libraryItemsData.status === "loading" && libraryItemsData.status === "loading" &&
!libraryItemsData.isInitialized !libraryItemsData.isInitialized
) { ) {
return ( return (
<LibraryMenuWrapper ref={ref}> <LibraryMenuWrapper>
<div className="layer-ui__library-message"> <div className="layer-ui__library-message">
<Spinner size="2em" /> <Spinner size="2em" />
<span>{t("labels.libraryLoadingMessage")}</span> <span>{t("labels.libraryLoadingMessage")}</span>
@ -250,51 +139,168 @@ export const LibraryMenu = ({
} }
return ( return (
<LibraryMenuWrapper ref={ref}> <LibraryMenuWrapper>
{showPublishLibraryDialog && (
<PublishLibrary
onClose={() => setShowPublishLibraryDialog(false)}
libraryItems={getSelectedItems(
libraryItemsData.libraryItems,
selectedItems,
)}
appState={appState}
onSuccess={(data) =>
onPublishLibSuccess(data, libraryItemsData.libraryItems)
}
onError={(error) => window.alert(error)}
updateItemsInStorage={() =>
library.setLibrary(libraryItemsData.libraryItems)
}
onRemove={(id: string) =>
setSelectedItems(selectedItems.filter((_id) => _id !== id))
}
/>
)}
{publishLibSuccess && renderPublishSuccess()}
<LibraryMenuItems <LibraryMenuItems
isLoading={libraryItemsData.status === "loading"} isLoading={libraryItemsData.status === "loading"}
libraryItems={libraryItemsData.libraryItems} libraryItems={libraryItemsData.libraryItems}
onRemoveFromLibrary={() =>
removeFromLibrary(libraryItemsData.libraryItems)
}
onAddToLibrary={(elements) => onAddToLibrary={(elements) =>
addToLibrary(elements, libraryItemsData.libraryItems) addToLibrary(elements, libraryItemsData.libraryItems)
} }
onInsertLibraryItems={onInsertLibraryItems} onInsertLibraryItems={onInsertLibraryItems}
pendingElements={pendingElements} pendingElements={pendingElements}
setAppState={setAppState}
appState={appState}
libraryReturnUrl={libraryReturnUrl}
library={library}
theme={appState.theme}
files={files}
id={id}
selectedItems={selectedItems} selectedItems={selectedItems}
onSelectItems={(ids) => setSelectedItems(ids)} onSelectItems={onSelectItems}
onPublish={() => setShowPublishLibraryDialog(true)}
resetLibrary={resetLibrary}
/> />
<a
className="library-menu-browse-button"
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
window.name || "_blank"
}&referrer=${referrer}&useHash=true&token=${id}&theme=${
appState.theme
}&version=${VERSIONS.excalidrawLibrary}`}
target="_excalidraw_libraries"
>
{t("labels.libraries")}
</a>
</LibraryMenuWrapper> </LibraryMenuWrapper>
); );
}; };
export const LibraryMenu: React.FC<{
appState: AppState;
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
focusContainer: () => void;
library: Library;
id: string;
}> = ({
appState,
onInsertElements,
libraryReturnUrl,
focusContainer,
library,
id,
}) => {
const setAppState = useExcalidrawSetAppState();
const elements = useExcalidrawElements();
const device = useDevice();
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
const ref = useRef<HTMLDivElement | null>(null);
const closeLibrary = useCallback(() => {
const isDialogOpen = !!document.querySelector(".Dialog");
// Prevent closing if any dialog is open
if (isDialogOpen) {
return;
}
setAppState({ openSidebar: null });
}, [setAppState]);
useOnClickOutside(
ref,
useCallback(
(event) => {
// If click on the library icon, do nothing so that LibraryButton
// can toggle library menu
if ((event.target as Element).closest(".ToolIcon__library")) {
return;
}
if (!appState.isSidebarDocked || !device.canDeviceFitSidebar) {
closeLibrary();
}
},
[closeLibrary, appState.isSidebarDocked, device.canDeviceFitSidebar],
),
);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === KEYS.ESCAPE &&
(!appState.isSidebarDocked || !device.canDeviceFitSidebar)
) {
closeLibrary();
}
};
document.addEventListener(EVENT.KEYDOWN, handleKeyDown);
return () => {
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
};
}, [closeLibrary, appState.isSidebarDocked, device.canDeviceFitSidebar]);
const deselectItems = useCallback(() => {
setAppState({
selectedElementIds: {},
selectedGroupIds: {},
});
}, [setAppState]);
const removeFromLibrary = useCallback(
async (libraryItems: LibraryItems) => {
const nextItems = libraryItems.filter(
(item) => !selectedItems.includes(item.id),
);
library.setLibrary(nextItems).catch(() => {
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
});
setSelectedItems([]);
},
[library, setAppState, selectedItems, setSelectedItems],
);
const resetLibrary = useCallback(() => {
library.resetLibrary();
focusContainer();
}, [library, focusContainer]);
return (
<Sidebar
__isInternal
// necessary to remount when switching between internal
// and custom (host app) sidebar, so that the `props.onClose`
// is colled correctly
key="library"
className="layer-ui__library-sidebar"
onDock={(docked) => {
trackEvent(
"library",
`toggleLibraryDock (${docked ? "dock" : "undock"})`,
`sidebar (${device.isMobile ? "mobile" : "desktop"})`,
);
}}
ref={ref}
>
<Sidebar.Header className="layer-ui__library-header">
<LibraryMenuHeader
appState={appState}
setAppState={setAppState}
selectedItems={selectedItems}
onSelectItems={setSelectedItems}
library={library}
onRemoveFromLibrary={() =>
removeFromLibrary(libraryItemsData.libraryItems)
}
resetLibrary={resetLibrary}
/>
</Sidebar.Header>
<LibraryMenuContent
pendingElements={getSelectedElements(elements, appState, true)}
onInsertLibraryItems={(libraryItems) => {
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
}}
onAddToLibrary={deselectItems}
setAppState={setAppState}
libraryReturnUrl={libraryReturnUrl}
library={library}
id={id}
appState={appState}
selectedItems={selectedItems}
onSelectItems={setSelectedItems}
/>
</Sidebar>
);
};

View File

@ -0,0 +1,258 @@
import React, { useCallback, useState } from "react";
import { saveLibraryAsJSON } from "../data/json";
import Library, { libraryItemsAtom } from "../data/library";
import { t } from "../i18n";
import { AppState, LibraryItem, LibraryItems } from "../types";
import { exportToFileIcon, load, publishIcon, trash } from "./icons";
import { ToolButton } from "./ToolButton";
import { Tooltip } from "./Tooltip";
import { fileOpen } from "../data/filesystem";
import { muteFSAbortError } from "../utils";
import { useAtom } from "jotai";
import { jotaiScope } from "../jotai";
import ConfirmDialog from "./ConfirmDialog";
import PublishLibrary from "./PublishLibrary";
import { Dialog } from "./Dialog";
const getSelectedItems = (
libraryItems: LibraryItems,
selectedItems: LibraryItem["id"][],
) => libraryItems.filter((item) => selectedItems.includes(item.id));
export const LibraryMenuHeader: React.FC<{
setAppState: React.Component<any, AppState>["setState"];
selectedItems: LibraryItem["id"][];
library: Library;
onRemoveFromLibrary: () => void;
resetLibrary: () => void;
onSelectItems: (items: LibraryItem["id"][]) => void;
appState: AppState;
}> = ({
setAppState,
selectedItems,
library,
onRemoveFromLibrary,
resetLibrary,
onSelectItems,
appState,
}) => {
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
const renderRemoveLibAlert = useCallback(() => {
const content = selectedItems.length
? t("alerts.removeItemsFromsLibrary", { count: selectedItems.length })
: t("alerts.resetLibrary");
const title = selectedItems.length
? t("confirmDialog.removeItemsFromLib")
: t("confirmDialog.resetLibrary");
return (
<ConfirmDialog
onConfirm={() => {
if (selectedItems.length) {
onRemoveFromLibrary();
} else {
resetLibrary();
}
setShowRemoveLibAlert(false);
}}
onCancel={() => {
setShowRemoveLibAlert(false);
}}
title={title}
>
<p>{content}</p>
</ConfirmDialog>
);
}, [selectedItems, onRemoveFromLibrary, resetLibrary]);
const [showRemoveLibAlert, setShowRemoveLibAlert] = useState(false);
const itemsSelected = !!selectedItems.length;
const items = itemsSelected
? libraryItemsData.libraryItems.filter((item) =>
selectedItems.includes(item.id),
)
: libraryItemsData.libraryItems;
const resetLabel = itemsSelected
? t("buttons.remove")
: t("buttons.resetLibrary");
const [showPublishLibraryDialog, setShowPublishLibraryDialog] =
useState(false);
const [publishLibSuccess, setPublishLibSuccess] = useState<null | {
url: string;
authorName: string;
}>(null);
const renderPublishSuccess = useCallback(() => {
return (
<Dialog
onCloseRequest={() => setPublishLibSuccess(null)}
title={t("publishSuccessDialog.title")}
className="publish-library-success"
small={true}
>
<p>
{t("publishSuccessDialog.content", {
authorName: publishLibSuccess!.authorName,
})}{" "}
<a
href={publishLibSuccess?.url}
target="_blank"
rel="noopener noreferrer"
>
{t("publishSuccessDialog.link")}
</a>
</p>
<ToolButton
type="button"
title={t("buttons.close")}
aria-label={t("buttons.close")}
label={t("buttons.close")}
onClick={() => setPublishLibSuccess(null)}
data-testid="publish-library-success-close"
className="publish-library-success-close"
/>
</Dialog>
);
}, [setPublishLibSuccess, publishLibSuccess]);
const onPublishLibSuccess = useCallback(
(data: { url: string; authorName: string }, libraryItems: LibraryItems) => {
setShowPublishLibraryDialog(false);
setPublishLibSuccess({ url: data.url, authorName: data.authorName });
const nextLibItems = libraryItems.slice();
nextLibItems.forEach((libItem) => {
if (selectedItems.includes(libItem.id)) {
libItem.status = "published";
}
});
library.setLibrary(nextLibItems);
},
[setShowPublishLibraryDialog, setPublishLibSuccess, selectedItems, library],
);
const onLibraryImport = async () => {
try {
await library.updateLibrary({
libraryItems: fileOpen({
description: "Excalidraw library files",
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
/*
extensions: [".json", ".excalidrawlib"],
*/
}),
merge: true,
openLibraryMenu: true,
});
} catch (error: any) {
if (error?.name === "AbortError") {
console.warn(error);
return;
}
setAppState({ errorMessage: t("errors.importLibraryError") });
}
};
const onLibraryExport = async () => {
const libraryItems = itemsSelected
? items
: await library.getLatestLibrary();
saveLibraryAsJSON(libraryItems)
.catch(muteFSAbortError)
.catch((error) => {
setAppState({ errorMessage: error.message });
});
};
return (
<div className="library-actions">
{showRemoveLibAlert && renderRemoveLibAlert()}
{showPublishLibraryDialog && (
<PublishLibrary
onClose={() => setShowPublishLibraryDialog(false)}
libraryItems={getSelectedItems(
libraryItemsData.libraryItems,
selectedItems,
)}
appState={appState}
onSuccess={(data) =>
onPublishLibSuccess(data, libraryItemsData.libraryItems)
}
onError={(error) => window.alert(error)}
updateItemsInStorage={() =>
library.setLibrary(libraryItemsData.libraryItems)
}
onRemove={(id: string) =>
onSelectItems(selectedItems.filter((_id) => _id !== id))
}
/>
)}
{publishLibSuccess && renderPublishSuccess()}
{!itemsSelected && (
<ToolButton
key="import"
type="button"
title={t("buttons.load")}
aria-label={t("buttons.load")}
icon={load}
onClick={onLibraryImport}
className="library-actions--load"
/>
)}
{!!items.length && (
<>
<ToolButton
key="export"
type="button"
title={t("buttons.export")}
aria-label={t("buttons.export")}
icon={exportToFileIcon}
onClick={onLibraryExport}
className="library-actions--export"
>
{selectedItems.length > 0 && (
<span className="library-actions-counter">
{selectedItems.length}
</span>
)}
</ToolButton>
<ToolButton
key="reset"
type="button"
title={resetLabel}
aria-label={resetLabel}
icon={trash}
onClick={() => setShowRemoveLibAlert(true)}
className="library-actions--remove"
>
{selectedItems.length > 0 && (
<span className="library-actions-counter">
{selectedItems.length}
</span>
)}
</ToolButton>
</>
)}
{itemsSelected && (
<Tooltip label={t("hints.publishLibrary")}>
<ToolButton
type="button"
aria-label={t("buttons.publishLibrary")}
label={t("buttons.publishLibrary")}
icon={publishIcon}
className="library-actions--publish"
onClick={() => setShowPublishLibraryDialog(true)}
>
<label>{t("buttons.publishLibrary")}</label>
{selectedItems.length > 0 && (
<span className="library-actions-counter">
{selectedItems.length}
</span>
)}
</ToolButton>
</Tooltip>
)}
</div>
);
};

View File

@ -6,93 +6,6 @@
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
.library-actions {
width: 100%;
display: flex;
margin-right: auto;
align-items: center;
button .library-actions-counter {
position: absolute;
right: 2px;
bottom: 2px;
border-radius: 50%;
width: 1em;
height: 1em;
padding: 1px;
font-size: 0.7rem;
background: #fff;
}
&--remove {
background-color: $oc-red-7;
&:hover {
background-color: $oc-red-8;
}
&:active {
background-color: $oc-red-9;
}
svg {
color: $oc-white;
}
.library-actions-counter {
color: $oc-red-7;
}
}
&--export {
background-color: $oc-lime-5;
&:hover {
background-color: $oc-lime-7;
}
&:active {
background-color: $oc-lime-8;
}
svg {
color: $oc-white;
}
.library-actions-counter {
color: $oc-lime-5;
}
}
&--publish {
background-color: $oc-cyan-6;
&:hover {
background-color: $oc-cyan-7;
}
&:active {
background-color: $oc-cyan-9;
}
svg {
color: $oc-white;
}
label {
margin-left: -0.2em;
margin-right: 1.1em;
color: $oc-white;
font-size: 0.86em;
}
.library-actions-counter {
color: $oc-cyan-6;
}
}
&--load {
background-color: $oc-blue-6;
&:hover {
background-color: $oc-blue-7;
}
&:active {
background-color: $oc-blue-9;
}
svg {
color: $oc-white;
}
}
}
&__items { &__items {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;

View File

@ -1,223 +1,35 @@
import React, { useCallback, useState } from "react"; import React, { useState } from "react";
import { saveLibraryAsJSON, serializeLibraryAsJSON } from "../data/json"; import { serializeLibraryAsJSON } from "../data/json";
import Library from "../data/library";
import { ExcalidrawElement, NonDeleted } from "../element/types"; import { ExcalidrawElement, NonDeleted } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { import { LibraryItem, LibraryItems } from "../types";
AppState, import { arrayToMap, chunk } from "../utils";
BinaryFiles,
ExcalidrawProps,
LibraryItem,
LibraryItems,
} from "../types";
import { arrayToMap, chunk, muteFSAbortError } from "../utils";
import { useDevice } from "./App";
import ConfirmDialog from "./ConfirmDialog";
import { exportToFileIcon, load, publishIcon, trash } from "./icons";
import { LibraryUnit } from "./LibraryUnit"; import { LibraryUnit } from "./LibraryUnit";
import Stack from "./Stack"; import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
import { Tooltip } from "./Tooltip";
import "./LibraryMenuItems.scss"; import "./LibraryMenuItems.scss";
import { MIME_TYPES, VERSIONS } from "../constants"; import { MIME_TYPES } from "../constants";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
import { fileOpen } from "../data/filesystem";
import { Sidebar } from "./Sidebar/Sidebar"; const CELLS_PER_ROW = 4;
const LibraryMenuItems = ({ const LibraryMenuItems = ({
isLoading, isLoading,
libraryItems, libraryItems,
onRemoveFromLibrary,
onAddToLibrary, onAddToLibrary,
onInsertLibraryItems, onInsertLibraryItems,
pendingElements, pendingElements,
theme,
setAppState,
appState,
libraryReturnUrl,
library,
files,
id,
selectedItems, selectedItems,
onSelectItems, onSelectItems,
onPublish,
resetLibrary,
}: { }: {
isLoading: boolean; isLoading: boolean;
libraryItems: LibraryItems; libraryItems: LibraryItems;
pendingElements: LibraryItem["elements"]; pendingElements: LibraryItem["elements"];
onRemoveFromLibrary: () => void;
onInsertLibraryItems: (libraryItems: LibraryItems) => void; onInsertLibraryItems: (libraryItems: LibraryItems) => void;
onAddToLibrary: (elements: LibraryItem["elements"]) => void; onAddToLibrary: (elements: LibraryItem["elements"]) => void;
theme: AppState["theme"];
files: BinaryFiles;
setAppState: React.Component<any, AppState>["setState"];
appState: AppState;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
library: Library;
id: string;
selectedItems: LibraryItem["id"][]; selectedItems: LibraryItem["id"][];
onSelectItems: (id: LibraryItem["id"][]) => void; onSelectItems: (id: LibraryItem["id"][]) => void;
onPublish: () => void;
resetLibrary: () => void;
}) => { }) => {
const renderRemoveLibAlert = useCallback(() => {
const content = selectedItems.length
? t("alerts.removeItemsFromsLibrary", { count: selectedItems.length })
: t("alerts.resetLibrary");
const title = selectedItems.length
? t("confirmDialog.removeItemsFromLib")
: t("confirmDialog.resetLibrary");
return (
<ConfirmDialog
onConfirm={() => {
if (selectedItems.length) {
onRemoveFromLibrary();
} else {
resetLibrary();
}
setShowRemoveLibAlert(false);
}}
onCancel={() => {
setShowRemoveLibAlert(false);
}}
title={title}
>
<p>{content}</p>
</ConfirmDialog>
);
}, [selectedItems, onRemoveFromLibrary, resetLibrary]);
const [showRemoveLibAlert, setShowRemoveLibAlert] = useState(false);
const device = useDevice();
const renderLibraryActions = () => {
const itemsSelected = !!selectedItems.length;
const items = itemsSelected
? libraryItems.filter((item) => selectedItems.includes(item.id))
: libraryItems;
const resetLabel = itemsSelected
? t("buttons.remove")
: t("buttons.resetLibrary");
return (
<div className="library-actions">
{!itemsSelected && (
<ToolButton
key="import"
type="button"
title={t("buttons.load")}
aria-label={t("buttons.load")}
icon={load}
onClick={async () => {
try {
await library.updateLibrary({
libraryItems: fileOpen({
description: "Excalidraw library files",
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
/*
extensions: [".json", ".excalidrawlib"],
*/
}),
merge: true,
openLibraryMenu: true,
});
} catch (error: any) {
if (error?.name === "AbortError") {
console.warn(error);
return;
}
setAppState({ errorMessage: t("errors.importLibraryError") });
}
}}
className="library-actions--load"
/>
)}
{!!items.length && (
<>
<ToolButton
key="export"
type="button"
title={t("buttons.export")}
aria-label={t("buttons.export")}
icon={exportToFileIcon}
onClick={async () => {
const libraryItems = itemsSelected
? items
: await library.getLatestLibrary();
saveLibraryAsJSON(libraryItems)
.catch(muteFSAbortError)
.catch((error) => {
setAppState({ errorMessage: error.message });
});
}}
className="library-actions--export"
>
{selectedItems.length > 0 && (
<span className="library-actions-counter">
{selectedItems.length}
</span>
)}
</ToolButton>
<ToolButton
key="reset"
type="button"
title={resetLabel}
aria-label={resetLabel}
icon={trash}
onClick={() => setShowRemoveLibAlert(true)}
className="library-actions--remove"
>
{selectedItems.length > 0 && (
<span className="library-actions-counter">
{selectedItems.length}
</span>
)}
</ToolButton>
</>
)}
{itemsSelected && (
<Tooltip label={t("hints.publishLibrary")}>
<ToolButton
type="button"
aria-label={t("buttons.publishLibrary")}
label={t("buttons.publishLibrary")}
icon={publishIcon}
className="library-actions--publish"
onClick={onPublish}
>
{!device.isMobile && <label>{t("buttons.publishLibrary")}</label>}
{selectedItems.length > 0 && (
<span className="library-actions-counter">
{selectedItems.length}
</span>
)}
</ToolButton>
</Tooltip>
)}
{device.isMobile && (
<div className="library-menu-browse-button--mobile">
<a
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
window.name || "_blank"
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
VERSIONS.excalidrawLibrary
}`}
target="_excalidraw_libraries"
>
{t("labels.libraries")}
</a>
</div>
)}
</div>
);
};
const CELLS_PER_ROW = device.isMobile && !device.isSmScreen ? 6 : 4;
const referrer =
libraryReturnUrl || window.location.origin + window.location.pathname;
const [lastSelectedItem, setLastSelectedItem] = useState< const [lastSelectedItem, setLastSelectedItem] = useState<
LibraryItem["id"] | null LibraryItem["id"] | null
>(null); >(null);
@ -294,7 +106,6 @@ const LibraryMenuItems = ({
<Stack.Col key={params.key}> <Stack.Col key={params.key}>
<LibraryUnit <LibraryUnit
elements={params.item?.elements} elements={params.item?.elements}
files={files}
isPending={!params.item?.id && !!params.item?.elements} isPending={!params.item?.id && !!params.item?.elements}
onClick={params.onClick || (() => {})} onClick={params.onClick || (() => {})}
id={params.item?.id || null} id={params.item?.id || null}
@ -370,8 +181,21 @@ const LibraryMenuItems = ({
(item) => item.status === "published", (item) => item.status === "published",
); );
const renderLibraryMenuItems = () => { return (
return ( <div
className="library-menu-items-container"
style={
publishedItems.length || unpublishedItems.length
? {
flex: "1 1 0",
overflowY: "auto",
}
: {
marginBottom: "2rem",
flex: 0,
}
}
>
<Stack.Col <Stack.Col
className="library-menu-items-container__items" className="library-menu-items-container__items"
align="start" align="start"
@ -443,8 +267,8 @@ const LibraryMenuItems = ({
<> <>
{(publishedItems.length > 0 || {(publishedItems.length > 0 ||
(!device.isMobile && pendingElements.length > 0 ||
(pendingElements.length > 0 || unpublishedItems.length > 0))) && ( unpublishedItems.length > 0) && (
<div className="separator">{t("labels.excalidrawLib")}</div> <div className="separator">{t("labels.excalidrawLib")}</div>
)} )}
{publishedItems.length > 0 ? ( {publishedItems.length > 0 ? (
@ -466,45 +290,6 @@ const LibraryMenuItems = ({
) : null} ) : null}
</> </>
</Stack.Col> </Stack.Col>
);
};
const renderLibraryFooter = () => {
return (
<a
className="library-menu-browse-button"
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
window.name || "_blank"
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
VERSIONS.excalidrawLibrary
}`}
target="_excalidraw_libraries"
>
{t("labels.libraries")}
</a>
);
};
return (
<div
className="library-menu-items-container"
style={
device.isMobile
? {
minHeight: "200px",
maxHeight: "70vh",
}
: undefined
}
>
{showRemoveLibAlert && renderRemoveLibAlert()}
{/* NOTE using SidebarHeader here isn't semantic since this may render
outside of a sidebar, but for now it doesn't matter */}
<Sidebar.Header className="layer-ui__library-header">
{renderLibraryActions()}
</Sidebar.Header>
{renderLibraryMenuItems()}
{!device.isMobile && renderLibraryFooter()}
</div> </div>
); );
}; };

View File

@ -3,7 +3,7 @@ import oc from "open-color";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useDevice } from "../components/App"; import { useDevice } from "../components/App";
import { exportToSvg } from "../scene/export"; import { exportToSvg } from "../scene/export";
import { BinaryFiles, LibraryItem } from "../types"; import { LibraryItem } from "../types";
import "./LibraryUnit.scss"; import "./LibraryUnit.scss";
import { CheckboxItem } from "./CheckboxItem"; import { CheckboxItem } from "./CheckboxItem";
@ -23,7 +23,6 @@ const PLUS_ICON = (
export const LibraryUnit = ({ export const LibraryUnit = ({
id, id,
elements, elements,
files,
isPending, isPending,
onClick, onClick,
selected, selected,
@ -32,7 +31,6 @@ export const LibraryUnit = ({
}: { }: {
id: LibraryItem["id"] | /** for pending item */ null; id: LibraryItem["id"] | /** for pending item */ null;
elements?: LibraryItem["elements"]; elements?: LibraryItem["elements"];
files: BinaryFiles;
isPending?: boolean; isPending?: boolean;
onClick: () => void; onClick: () => void;
selected: boolean; selected: boolean;
@ -56,7 +54,7 @@ export const LibraryUnit = ({
exportBackground: false, exportBackground: false,
viewBackgroundColor: oc.white, viewBackgroundColor: oc.white,
}, },
files, null,
); );
node.innerHTML = svg.outerHTML; node.innerHTML = svg.outerHTML;
})(); })();
@ -64,7 +62,7 @@ export const LibraryUnit = ({
return () => { return () => {
node.innerHTML = ""; node.innerHTML = "";
}; };
}, [elements, files]); }, [elements]);
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const isMobile = useDevice().isMobile; const isMobile = useDevice().isMobile;

View File

@ -28,7 +28,6 @@ type MobileMenuProps = {
renderImageExportDialog: () => React.ReactNode; renderImageExportDialog: () => React.ReactNode;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
libraryMenu: JSX.Element | null;
onCollabButtonClick?: () => void; onCollabButtonClick?: () => void;
onLockToggle: () => void; onLockToggle: () => void;
onPenModeToggle: () => void; onPenModeToggle: () => void;
@ -44,14 +43,13 @@ type MobileMenuProps = {
appState: AppState, appState: AppState,
) => JSX.Element | null; ) => JSX.Element | null;
renderCustomStats?: ExcalidrawProps["renderCustomStats"]; renderCustomStats?: ExcalidrawProps["renderCustomStats"];
renderCustomSidebar?: ExcalidrawProps["renderSidebar"]; renderSidebars: () => JSX.Element | null;
device: Device; device: Device;
}; };
export const MobileMenu = ({ export const MobileMenu = ({
appState, appState,
elements, elements,
libraryMenu,
actionManager, actionManager,
renderJSONExportDialog, renderJSONExportDialog,
renderImageExportDialog, renderImageExportDialog,
@ -65,7 +63,7 @@ export const MobileMenu = ({
onImageAction, onImageAction,
renderTopRightUI, renderTopRightUI,
renderCustomStats, renderCustomStats,
renderCustomSidebar, renderSidebars,
device, device,
}: MobileMenuProps) => { }: MobileMenuProps) => {
const renderToolbar = () => { const renderToolbar = () => {
@ -111,7 +109,6 @@ export const MobileMenu = ({
penDetected={appState.penDetected} penDetected={appState.penDetected}
/> />
</Stack.Row> </Stack.Row>
{libraryMenu && <Island padding={2}>{libraryMenu}</Island>}
</Stack.Col> </Stack.Col>
)} )}
</Section> </Section>
@ -184,7 +181,7 @@ export const MobileMenu = ({
}; };
return ( return (
<> <>
{appState.openSidebar === "customSidebar" && renderCustomSidebar?.()} {renderSidebars()}
{!appState.viewModeEnabled && renderToolbar()} {!appState.viewModeEnabled && renderToolbar()}
{!appState.openMenu && appState.showStats && ( {!appState.openMenu && appState.showStats && (
<Stats <Stats

View File

@ -1,4 +1,10 @@
import { useEffect, useLayoutEffect, useRef, useState } from "react"; import {
useEffect,
useLayoutEffect,
useRef,
useState,
forwardRef,
} from "react";
import { Island } from ".././Island"; import { Island } from ".././Island";
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import { jotaiScope } from "../../jotai"; import { jotaiScope } from "../../jotai";
@ -19,103 +25,115 @@ import { updateObject } from "../../utils";
* the host app may render (mount/unmount) multiple different sidebar */ * the host app may render (mount/unmount) multiple different sidebar */
export const hostSidebarCountersAtom = atom({ rendered: 0, docked: 0 }); export const hostSidebarCountersAtom = atom({ rendered: 0, docked: 0 });
export const Sidebar = ({ export const Sidebar = Object.assign(
children, forwardRef(
onClose, (
onDock, {
docked, children,
dockable = true, onClose,
className, onDock,
__isInternal, docked,
}: SidebarProps<{ dockable = true,
// NOTE sidebars we use internally inside the editor must have this flag set. className,
// It indicates that this sidebar should have lower precedence over host __isInternal,
// sidebars, if both are open. }: SidebarProps<{
/** @private internal */ // NOTE sidebars we use internally inside the editor must have this flag set.
__isInternal?: boolean; // It indicates that this sidebar should have lower precedence over host
}>) => { // sidebars, if both are open.
const [hostSidebarCounters, setHostSidebarCounters] = useAtom( /** @private internal */
hostSidebarCountersAtom, __isInternal?: boolean;
jotaiScope, }>,
); ref: React.ForwardedRef<HTMLDivElement>,
) => {
const [hostSidebarCounters, setHostSidebarCounters] = useAtom(
hostSidebarCountersAtom,
jotaiScope,
);
const setAppState = useExcalidrawSetAppState(); const setAppState = useExcalidrawSetAppState();
const [isDockedFallback, setIsDockedFallback] = useState(docked ?? false); const [isDockedFallback, setIsDockedFallback] = useState(docked ?? false);
useLayoutEffect(() => { useLayoutEffect(() => {
if (docked === undefined) { if (docked === undefined) {
// ugly hack to get initial state out of AppState without susbcribing // ugly hack to get initial state out of AppState without subscribing
// to it as a whole (once we have granular subscriptions, we'll move // to it as a whole (once we have granular subscriptions, we'll move
// to that) // to that)
// //
// NOTE this means that is updated `state.isSidebarDocked` changes outside // NOTE this means that is updated `state.isSidebarDocked` changes outside
// of this compoent, it won't be reflected here. Currently doesn't happen. // of this compoent, it won't be reflected here. Currently doesn't happen.
setAppState((state) => { setAppState((state) => {
setIsDockedFallback(state.isSidebarDocked); setIsDockedFallback(state.isSidebarDocked);
// bail from update // bail from update
return null; return null;
}); });
} }
}, [setAppState, docked]); }, [setAppState, docked]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!__isInternal) { if (!__isInternal) {
setHostSidebarCounters((s) => ({ setHostSidebarCounters((s) => ({
rendered: s.rendered + 1, rendered: s.rendered + 1,
docked: isDockedFallback ? s.docked + 1 : s.docked, docked: isDockedFallback ? s.docked + 1 : s.docked,
})); }));
return () => { return () => {
setHostSidebarCounters((s) => ({ setHostSidebarCounters((s) => ({
rendered: s.rendered - 1, rendered: s.rendered - 1,
docked: isDockedFallback ? s.docked - 1 : s.docked, docked: isDockedFallback ? s.docked - 1 : s.docked,
})); }));
};
}
}, [__isInternal, setHostSidebarCounters, isDockedFallback]);
const onCloseRef = useRef(onClose);
onCloseRef.current = onClose;
useEffect(() => {
return () => {
onCloseRef.current?.();
};
}, []);
const headerPropsRef = useRef<SidebarPropsContextValue>({});
headerPropsRef.current.onClose = () => {
setAppState({ openSidebar: null });
}; };
} headerPropsRef.current.onDock = (isDocked) => {
}, [__isInternal, setHostSidebarCounters, isDockedFallback]); if (docked === undefined) {
setAppState({ isSidebarDocked: isDocked });
setIsDockedFallback(isDocked);
}
onDock?.(isDocked);
};
// renew the ref object if the following props change since we want to
// rerender. We can't pass down as component props manually because
// the <Sidebar.Header/> can be rendered upsream.
headerPropsRef.current = updateObject(headerPropsRef.current, {
docked: docked ?? isDockedFallback,
dockable,
});
const onCloseRef = useRef(onClose); if (hostSidebarCounters.rendered > 0 && __isInternal) {
onCloseRef.current = onClose; return null;
}
useEffect(() => { return (
return () => { <Island
onCloseRef.current?.(); padding={2}
}; className={clsx("layer-ui__sidebar", className)}
}, []); ref={ref}
>
const headerPropsRef = useRef<SidebarPropsContextValue>({}); <SidebarPropsContext.Provider value={headerPropsRef.current}>
headerPropsRef.current.onClose = () => { <SidebarHeaderComponents.Context>
setAppState({ openSidebar: null }); <SidebarHeaderComponents.Component __isFallback />
}; {children}
headerPropsRef.current.onDock = (isDocked) => { </SidebarHeaderComponents.Context>
if (docked === undefined) { </SidebarPropsContext.Provider>
setAppState({ isSidebarDocked: isDocked }); </Island>
setIsDockedFallback(isDocked); );
} },
onDock?.(isDocked); ),
}; {
// renew the ref object if the following props change since we want to Header: SidebarHeaderComponents.Component,
// rerender. We can't pass down as component props manually because },
// the <Sidebar.Header/> can be rendered upsream. );
headerPropsRef.current = updateObject(headerPropsRef.current, {
docked: docked ?? isDockedFallback,
dockable,
});
if (hostSidebarCounters.rendered > 0 && __isInternal) {
return null;
}
return (
<Island padding={2} className={clsx("layer-ui__sidebar", className)}>
<SidebarPropsContext.Provider value={headerPropsRef.current}>
<SidebarHeaderComponents.Context>
<SidebarHeaderComponents.Component __isFallback />
{children}
</SidebarHeaderComponents.Context>
</SidebarPropsContext.Provider>
</Island>
);
};
Sidebar.Header = SidebarHeaderComponents.Component;