aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/components/Dialog.tsx
diff options
context:
space:
mode:
authorkj_sh6042026-03-15 16:19:35 -0400
committerkj_sh6042026-03-15 16:19:35 -0400
commit6ec259a0e71174651bae95d4628138bf6fd68742 (patch)
tree5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/components/Dialog.tsx
parent16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff)
refactor: packages/
Diffstat (limited to 'packages/excalidraw/components/Dialog.tsx')
-rw-r--r--packages/excalidraw/components/Dialog.tsx134
1 files changed, 134 insertions, 0 deletions
diff --git a/packages/excalidraw/components/Dialog.tsx b/packages/excalidraw/components/Dialog.tsx
new file mode 100644
index 0000000..0a105cf
--- /dev/null
+++ b/packages/excalidraw/components/Dialog.tsx
@@ -0,0 +1,134 @@
+import clsx from "clsx";
+import React, { useEffect, useState } from "react";
+import { useCallbackRefState } from "../hooks/useCallbackRefState";
+import {
+ useExcalidrawContainer,
+ useDevice,
+ useExcalidrawSetAppState,
+} from "./App";
+import { KEYS } from "../keys";
+import "./Dialog.scss";
+import { Island } from "./Island";
+import { Modal } from "./Modal";
+import { queryFocusableElements } from "../utils";
+import { isLibraryMenuOpenAtom } from "./LibraryMenu";
+import { useSetAtom } from "../editor-jotai";
+import { t } from "../i18n";
+import { CloseIcon } from "./icons";
+
+export type DialogSize = number | "small" | "regular" | "wide" | undefined;
+
+export interface DialogProps {
+ children: React.ReactNode;
+ className?: string;
+ size?: DialogSize;
+ onCloseRequest(): void;
+ title: React.ReactNode | false;
+ autofocus?: boolean;
+ closeOnClickOutside?: boolean;
+}
+
+function getDialogSize(size: DialogSize): number {
+ if (size && typeof size === "number") {
+ return size;
+ }
+
+ switch (size) {
+ case "small":
+ return 550;
+ case "wide":
+ return 1024;
+ case "regular":
+ default:
+ return 800;
+ }
+}
+
+export const Dialog = (props: DialogProps) => {
+ const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
+ const [lastActiveElement] = useState(document.activeElement);
+ const { id } = useExcalidrawContainer();
+ const isFullscreen = useDevice().viewport.isMobile;
+
+ useEffect(() => {
+ if (!islandNode) {
+ return;
+ }
+
+ const focusableElements = queryFocusableElements(islandNode);
+
+ setTimeout(() => {
+ if (focusableElements.length > 0 && props.autofocus !== false) {
+ // If there's an element other than close, focus it.
+ (focusableElements[1] || focusableElements[0]).focus();
+ }
+ });
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === KEYS.TAB) {
+ const focusableElements = queryFocusableElements(islandNode);
+ const { activeElement } = document;
+ const currentIndex = focusableElements.findIndex(
+ (element) => element === activeElement,
+ );
+
+ if (currentIndex === 0 && event.shiftKey) {
+ focusableElements[focusableElements.length - 1].focus();
+ event.preventDefault();
+ } else if (
+ currentIndex === focusableElements.length - 1 &&
+ !event.shiftKey
+ ) {
+ focusableElements[0].focus();
+ event.preventDefault();
+ }
+ }
+ };
+
+ islandNode.addEventListener("keydown", handleKeyDown);
+
+ return () => islandNode.removeEventListener("keydown", handleKeyDown);
+ }, [islandNode, props.autofocus]);
+
+ const setAppState = useExcalidrawSetAppState();
+ const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
+
+ const onClose = () => {
+ setAppState({ openMenu: null });
+ setIsLibraryMenuOpen(false);
+ (lastActiveElement as HTMLElement).focus();
+ props.onCloseRequest();
+ };
+
+ return (
+ <Modal
+ className={clsx("Dialog", props.className, {
+ "Dialog--fullscreen": isFullscreen,
+ })}
+ labelledBy="dialog-title"
+ maxWidth={getDialogSize(props.size)}
+ onCloseRequest={onClose}
+ closeOnClickOutside={props.closeOnClickOutside}
+ >
+ <Island ref={setIslandNode}>
+ {props.title && (
+ <h2 id={`${id}-dialog-title`} className="Dialog__title">
+ <span className="Dialog__titleContent">{props.title}</span>
+ </h2>
+ )}
+ {isFullscreen && (
+ <button
+ className="Dialog__close"
+ onClick={onClose}
+ title={t("buttons.close")}
+ aria-label={t("buttons.close")}
+ type="button"
+ >
+ {CloseIcon}
+ </button>
+ )}
+ <div className="Dialog__content">{props.children}</div>
+ </Island>
+ </Modal>
+ );
+};