8420aecb34
* feat: new Menu Component API * allow valid children types * introduce menu group to group items * Add lang footer * use display name * displayName * define types inside * fix default menu * add json export to menu * fix * simplify expression * put open menu into own compo to optimize perf So that we don't rerun `useOutsideClickHook` (and rebind event listeners all the time) * naming tweaks * rename MenuComponents->MenuDefaultItems and export default items from Menu.Items * import Menu.scss in Menu.tsx * move menu scss to excal app * Don't filter children inside menu group * move E+ out of socials * support style prop for MenuItem and MenuGroup * Support header in menu group and add Excalidraw links header for default items in social section * rename header to title * fix padding for lang * render menu in mobile * review fixes * tweaks * Export collaborators and show in mobile menu * revert .env * lint :p * again lint * show correct actions in view mode for mobile * Whitelist Collaborators Comp * mobile styling * padding * don't show nerds when menu open in mobile * lint :( * hide shortcuts * refactor userlist to support mobile and keep a wrapper comp for excal app * use only UserList * render only on mobile for default items * remove unused hooks * Show collab button in menu when onCollabButtonClick present and hide export when UIOptions.canvasActions.export is false * fix tests * lint * inject userlist inside menu on mobile * revert userlist * move menu socials to default menu * fix collab * use meny in library * Make Menu generic and create hamburgemenu for public excal menu and use menu in library as well * use appState.openMenu for mobile * fix tests * styling fixes and support style and class name in menu content * fix test * rename MenuDefaultItems->DefaultItems * move footer css to its own comp * rename HamburgerMenu -> MainMenu * rename menu -> dropdownMenu and update classes, onClick->onToggle * close main menu when dialog closes * by bye filtering * update docs * fix lint * update example, docs for useDevice and footer in mobile, rename menu ->DropDownMenu everywhere * spec * remove isMenuOpenAtom and set openMenu as canvas for main menu, render decreases in specs :) * [temp] remove cyclic depenedency to fix build * hack- update appstate to sync lang change * Add more specs * wip: rewrite MainMenu footer * fix margin * fix snaps * not needed as lang list no more imported * simplify custom footer rendering * Add DropdownMenuItemLink and DropdownMenuItemCustom and update API, docs * fix `MainMenu.ItemCustom` * naming * use onSelect and base class for custom items * fix lint * fix snap * use custom item for lang * update docs * fix * properly use `MainMenu.ItemCustom` for `LanguageList` * add margin top to custom items * flex Co-authored-by: dwelle <luzar.david@gmail.com>
110 lines
3.3 KiB
TypeScript
110 lines
3.3 KiB
TypeScript
import clsx from "clsx";
|
|
import React, { useEffect, useState } from "react";
|
|
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
|
import { t } from "../i18n";
|
|
import {
|
|
useExcalidrawContainer,
|
|
useDevice,
|
|
useExcalidrawSetAppState,
|
|
} from "../components/App";
|
|
import { KEYS } from "../keys";
|
|
import "./Dialog.scss";
|
|
import { back, CloseIcon } from "./icons";
|
|
import { Island } from "./Island";
|
|
import { Modal } from "./Modal";
|
|
import { AppState } from "../types";
|
|
import { queryFocusableElements } from "../utils";
|
|
import { useSetAtom } from "jotai";
|
|
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
|
|
|
export interface DialogProps {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
small?: boolean;
|
|
onCloseRequest(): void;
|
|
title: React.ReactNode;
|
|
autofocus?: boolean;
|
|
theme?: AppState["theme"];
|
|
closeOnClickOutside?: boolean;
|
|
}
|
|
|
|
export const Dialog = (props: DialogProps) => {
|
|
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
|
const [lastActiveElement] = useState(document.activeElement);
|
|
const { id } = useExcalidrawContainer();
|
|
|
|
useEffect(() => {
|
|
if (!islandNode) {
|
|
return;
|
|
}
|
|
|
|
const focusableElements = queryFocusableElements(islandNode);
|
|
|
|
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)}
|
|
labelledBy="dialog-title"
|
|
maxWidth={props.small ? 550 : 800}
|
|
onCloseRequest={onClose}
|
|
theme={props.theme}
|
|
closeOnClickOutside={props.closeOnClickOutside}
|
|
>
|
|
<Island ref={setIslandNode}>
|
|
<h2 id={`${id}-dialog-title`} className="Dialog__title">
|
|
<span className="Dialog__titleContent">{props.title}</span>
|
|
<button
|
|
className="Modal__close"
|
|
onClick={onClose}
|
|
title={t("buttons.close")}
|
|
aria-label={t("buttons.close")}
|
|
>
|
|
{useDevice().isMobile ? back : CloseIcon}
|
|
</button>
|
|
</h2>
|
|
<div className="Dialog__content">{props.children}</div>
|
|
</Island>
|
|
</Modal>
|
|
);
|
|
};
|