feat(packages/excalidraw): support nested dropdown menu (#10749)
Co-authored-by: Barnabás Molnár <38168628+barnabasmolnar@users.noreply.github.com>
This commit is contained in:
@@ -106,6 +106,7 @@ export const CLASSES = {
|
||||
CONVERT_ELEMENT_TYPE_POPUP: "ConvertElementTypePopup",
|
||||
SHAPE_ACTIONS_THEME_SCOPE: "shape-actions-theme-scope",
|
||||
FRAME_NAME: "frame-name",
|
||||
DROPDOWN_MENU_EVENT_WRAPPER: "dropdown-menu-event-wrapper",
|
||||
};
|
||||
|
||||
export const FONT_SIZES = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import clsx from "clsx";
|
||||
import { useRef, useState } from "react";
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { Popover } from "radix-ui";
|
||||
|
||||
import {
|
||||
CLASSES,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { Popover } from "radix-ui";
|
||||
import clsx from "clsx";
|
||||
import { useRef, useEffect } from "react";
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { KEYS } from "@excalidraw/common";
|
||||
|
||||
import { Excalidraw } from "../..";
|
||||
import { Keyboard } from "../../tests/helpers/ui";
|
||||
import { act, render } from "../../tests/test-utils";
|
||||
|
||||
describe("FontPicker", () => {
|
||||
it("should be able to open font picker", async () => {
|
||||
(global as any).ResizeObserver =
|
||||
(global as any).ResizeObserver ||
|
||||
class ResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
};
|
||||
|
||||
const { queryByTestId } = await render(
|
||||
<Excalidraw handleKeyboardGlobally={true} />,
|
||||
);
|
||||
|
||||
Keyboard.keyPress(KEYS.T);
|
||||
|
||||
const fontPickerTrigger = queryByTestId("font-family-show-fonts");
|
||||
|
||||
expect(fontPickerTrigger).not.toBeNull();
|
||||
|
||||
act(() => {
|
||||
fontPickerTrigger!.click();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { Popover } from "radix-ui";
|
||||
import clsx from "clsx";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
|
||||
|
||||
@@ -30,10 +30,12 @@ import { PropertiesPopover } from "../PropertiesPopover";
|
||||
import { QuickSearch } from "../QuickSearch";
|
||||
import { ScrollableList } from "../ScrollableList";
|
||||
import DropdownMenuGroup from "../dropdownMenu/DropdownMenuGroup";
|
||||
import DropdownMenuItem, {
|
||||
import {
|
||||
DropDownMenuItemBadgeType,
|
||||
DropDownMenuItemBadge,
|
||||
} from "../dropdownMenu/DropdownMenuItem";
|
||||
import MenuItemContent from "../dropdownMenu/DropdownMenuItemContent";
|
||||
import { getDropdownMenuItemClassName } from "../dropdownMenu/common";
|
||||
import {
|
||||
FontFamilyCodeIcon,
|
||||
FontFamilyHeadingIcon,
|
||||
@@ -269,45 +271,74 @@ export const FontPickerList = React.memo(
|
||||
[filteredFonts, sceneFamilies],
|
||||
);
|
||||
|
||||
const renderFont = (font: FontDescriptor, index: number) => (
|
||||
<DropdownMenuItem
|
||||
key={font.value}
|
||||
icon={font.icon}
|
||||
value={font.value}
|
||||
order={index}
|
||||
textStyle={{
|
||||
fontFamily: getFontFamilyString({ fontFamily: font.value }),
|
||||
}}
|
||||
hovered={font.value === hoveredFont?.value}
|
||||
selected={font.value === selectedFontFamily}
|
||||
// allow to tab between search and selected font
|
||||
tabIndex={font.value === selectedFontFamily ? 0 : -1}
|
||||
onClick={(e) => {
|
||||
wrappedOnSelect(Number(e.currentTarget.value));
|
||||
}}
|
||||
onMouseMove={() => {
|
||||
if (hoveredFont?.value !== font.value) {
|
||||
onHover(font.value);
|
||||
}
|
||||
}}
|
||||
badge={
|
||||
font.badge && (
|
||||
<DropDownMenuItemBadge type={font.badge.type}>
|
||||
{font.badge.placeholder}
|
||||
</DropDownMenuItemBadge>
|
||||
)
|
||||
const FontPickerListItem = ({
|
||||
font,
|
||||
order,
|
||||
}: {
|
||||
font: FontDescriptor;
|
||||
order: number;
|
||||
}) => {
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
const isHovered = font.value === hoveredFont?.value;
|
||||
const isSelected = font.value === selectedFontFamily;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isHovered) {
|
||||
return;
|
||||
}
|
||||
>
|
||||
{font.text}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
if (order === 0) {
|
||||
// scroll into the first item differently, so it's visible what is above (i.e. group title)
|
||||
ref.current?.scrollIntoView?.({ block: "end" });
|
||||
} else {
|
||||
ref.current?.scrollIntoView?.({ block: "nearest" });
|
||||
}
|
||||
}, [isHovered, order]);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
value={font.value}
|
||||
className={getDropdownMenuItemClassName("", isSelected, isHovered)}
|
||||
title={font.text}
|
||||
// allow to tab between search and selected font
|
||||
tabIndex={isSelected ? 0 : -1}
|
||||
onClick={(e) => {
|
||||
wrappedOnSelect(Number(e.currentTarget.value));
|
||||
}}
|
||||
onMouseMove={() => {
|
||||
if (hoveredFont?.value !== font.value) {
|
||||
onHover(font.value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItemContent
|
||||
icon={font.icon}
|
||||
badge={
|
||||
font.badge && (
|
||||
<DropDownMenuItemBadge type={font.badge.type}>
|
||||
{font.badge.placeholder}
|
||||
</DropDownMenuItemBadge>
|
||||
)
|
||||
}
|
||||
textStyle={{
|
||||
fontFamily: getFontFamilyString({ fontFamily: font.value }),
|
||||
}}
|
||||
>
|
||||
{font.text}
|
||||
</MenuItemContent>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const groups = [];
|
||||
|
||||
if (sceneFilteredFonts.length) {
|
||||
groups.push(
|
||||
<DropdownMenuGroup title={t("fontList.sceneFonts")} key="group_1">
|
||||
{sceneFilteredFonts.map(renderFont)}
|
||||
{sceneFilteredFonts.map((font, index) => (
|
||||
<FontPickerListItem key={font.value} font={font} order={index} />
|
||||
))}
|
||||
</DropdownMenuGroup>,
|
||||
);
|
||||
}
|
||||
@@ -315,9 +346,13 @@ export const FontPickerList = React.memo(
|
||||
if (availableFilteredFonts.length) {
|
||||
groups.push(
|
||||
<DropdownMenuGroup title={t("fontList.availableFonts")} key="group_2">
|
||||
{availableFilteredFonts.map((font, index) =>
|
||||
renderFont(font, index + sceneFilteredFonts.length),
|
||||
)}
|
||||
{availableFilteredFonts.map((font, index) => (
|
||||
<FontPickerListItem
|
||||
key={font.value}
|
||||
font={font}
|
||||
order={index + sceneFilteredFonts.length}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuGroup>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { Popover } from "radix-ui";
|
||||
|
||||
import { MOBILE_ACTION_BUTTON_BG } from "@excalidraw/common";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { Popover } from "radix-ui";
|
||||
import clsx from "clsx";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
|
||||
@@ -126,9 +126,10 @@
|
||||
|
||||
.dropdown-menu-container {
|
||||
width: 196px;
|
||||
box-shadow: var(--library-dropdown-shadow);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
--box-shadow: var(--library-dropdown-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -375,7 +375,7 @@ export const MobileToolBar = ({
|
||||
)}
|
||||
|
||||
{/* Other Shapes */}
|
||||
<DropdownMenu open={isOtherShapesMenuOpen} placement="top">
|
||||
<DropdownMenu open={isOtherShapesMenuOpen}>
|
||||
<DropdownMenu.Trigger
|
||||
className={clsx(
|
||||
"App-toolbar__extra-tools-trigger App-toolbar__extra-tools-trigger--mobile",
|
||||
@@ -403,6 +403,7 @@ export const MobileToolBar = ({
|
||||
onClickOutside={() => setIsOtherShapesMenuOpen(false)}
|
||||
onSelect={() => setIsOtherShapesMenuOpen(false)}
|
||||
className="App-toolbar__extra-tools-dropdown"
|
||||
align="start"
|
||||
>
|
||||
{!showTextToolOutside && (
|
||||
<DropdownMenu.Item
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { Popover } from "radix-ui";
|
||||
import clsx from "clsx";
|
||||
import React, { type ReactNode } from "react";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as RadixTabs from "@radix-ui/react-tabs";
|
||||
import { Tabs as RadixTabs } from "radix-ui";
|
||||
|
||||
import type { SidebarTabName } from "../../types";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as RadixTabs from "@radix-ui/react-tabs";
|
||||
import { Tabs as RadixTabs } from "radix-ui";
|
||||
|
||||
import type { SidebarTabName } from "../../types";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as RadixTabs from "@radix-ui/react-tabs";
|
||||
import { Tabs as RadixTabs } from "radix-ui";
|
||||
|
||||
export const SidebarTabTriggers = ({
|
||||
children,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as RadixTabs from "@radix-ui/react-tabs";
|
||||
import { Tabs as RadixTabs } from "radix-ui";
|
||||
|
||||
import { useUIAppState } from "../../context/ui-appState";
|
||||
import { useExcalidrawSetAppState } from "../App";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../../css/variables.module.scss";
|
||||
@use "../../../css/variables.module.scss" as *;
|
||||
|
||||
$verticalBreakpoint: 861px;
|
||||
|
||||
|
||||
@@ -52,11 +52,7 @@ export const ChatHistoryMenu = ({
|
||||
>
|
||||
{historyIcon}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
onClickOutside={onClose}
|
||||
onSelect={onClose}
|
||||
placement="bottom"
|
||||
>
|
||||
<DropdownMenu.Content onClickOutside={onClose} onSelect={onClose}>
|
||||
<>
|
||||
{savedChats.map((chat) => (
|
||||
<DropdownMenu.ItemCustom
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as RadixTabs from "@radix-ui/react-tabs";
|
||||
import { Tabs as RadixTabs } from "radix-ui";
|
||||
|
||||
export const TTDDialogTab = ({
|
||||
tab,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as RadixTabs from "@radix-ui/react-tabs";
|
||||
import { Tabs as RadixTabs } from "radix-ui";
|
||||
|
||||
export const TTDDialogTabTrigger = ({
|
||||
children,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as RadixTabs from "@radix-ui/react-tabs";
|
||||
import { Tabs as RadixTabs } from "radix-ui";
|
||||
|
||||
export const TTDDialogTabTriggers = ({
|
||||
children,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as RadixTabs from "@radix-ui/react-tabs";
|
||||
import { Tabs as RadixTabs } from "radix-ui";
|
||||
import { useRef } from "react";
|
||||
|
||||
import { isMemberOf } from "@excalidraw/common";
|
||||
|
||||
@@ -3,7 +3,7 @@ import clsx from "clsx";
|
||||
|
||||
import { capitalizeString } from "@excalidraw/common";
|
||||
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { Popover } from "radix-ui";
|
||||
|
||||
import { trackEvent } from "../analytics";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { Popover } from "radix-ui";
|
||||
import clsx from "clsx";
|
||||
import React, { useLayoutEffect } from "react";
|
||||
|
||||
|
||||
@@ -2,24 +2,35 @@
|
||||
|
||||
.excalidraw {
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 2.5rem;
|
||||
margin-top: 0.5rem;
|
||||
max-width: 16rem;
|
||||
z-index: 1;
|
||||
|
||||
&--placement-top {
|
||||
top: auto;
|
||||
bottom: 100%;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&__submenu-trigger {
|
||||
&[aria-expanded="true"] {
|
||||
.dropdown-menu-item {
|
||||
background-color: var(--button-hover-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__submenu-trigger-icon {
|
||||
margin-left: auto;
|
||||
opacity: 0.5;
|
||||
svg g {
|
||||
stroke-width: 2;
|
||||
}
|
||||
}
|
||||
|
||||
&--mobile {
|
||||
width: 100%;
|
||||
row-gap: 0.75rem;
|
||||
|
||||
// When main menu is in the top toolbar, position relative to trigger
|
||||
&.main-menu-dropdown {
|
||||
&.main-menu {
|
||||
min-width: 232px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
@@ -32,10 +43,6 @@
|
||||
.dropdown-menu-container {
|
||||
padding: 8px 8px;
|
||||
box-sizing: border-box;
|
||||
max-height: calc(
|
||||
100svh - var(--editor-container-padding) * 2 - 2.25rem
|
||||
);
|
||||
box-shadow: var(--shadow-island);
|
||||
border-radius: var(--border-radius-lg);
|
||||
position: relative;
|
||||
transition: box-shadow 0.5s ease-in-out;
|
||||
@@ -51,14 +58,25 @@
|
||||
|
||||
.dropdown-menu-container {
|
||||
background-color: var(--island-bg-color);
|
||||
|
||||
overflow-y: auto;
|
||||
--gap: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
|
||||
box-shadow: var(--box-shadow, var(--shadow-island));
|
||||
|
||||
max-height: calc(100svh - var(--editor-container-padding) * 2 - 2.25rem);
|
||||
|
||||
@at-root .excalidraw.theme--dark#{&} {
|
||||
box-shadow: var(--box-shadow, var(--shadow-island)),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-item-base {
|
||||
display: flex;
|
||||
column-gap: 0.625rem;
|
||||
padding: 0 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-on-surface);
|
||||
width: 100%;
|
||||
@@ -115,11 +133,9 @@
|
||||
|
||||
.dropdown-menu-item {
|
||||
height: 2rem;
|
||||
margin: 1px;
|
||||
padding: 0 0.5rem;
|
||||
width: calc(100% - 2px);
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
border: none;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radius-md);
|
||||
@@ -162,7 +178,7 @@
|
||||
|
||||
&:active {
|
||||
background-color: var(--button-hover-bg);
|
||||
border-color: var(--color-brand-active);
|
||||
box-shadow: 0 0 0 1px var(--color-brand-active);
|
||||
}
|
||||
|
||||
svg {
|
||||
@@ -223,7 +239,7 @@
|
||||
}
|
||||
|
||||
&:active {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 1px var(--color-primary);
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
@@ -235,7 +251,7 @@
|
||||
}
|
||||
|
||||
&:active {
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@at-root .excalidraw.theme--dark#{&} {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import React from "react";
|
||||
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
||||
|
||||
import { CLASSES } from "@excalidraw/common";
|
||||
|
||||
import DropdownMenuContent from "./DropdownMenuContent";
|
||||
import DropdownMenuGroup from "./DropdownMenuGroup";
|
||||
import DropdownMenuItem from "./DropdownMenuItem";
|
||||
import DropdownMenuItemCustom from "./DropdownMenuItemCustom";
|
||||
import DropdownMenuItemLink from "./DropdownMenuItemLink";
|
||||
import MenuSeparator from "./DropdownMenuSeparator";
|
||||
import DropdownMenuSub from "./DropdownMenuSub";
|
||||
import DropdownMenuTrigger from "./DropdownMenuTrigger";
|
||||
import {
|
||||
getMenuContentComponent,
|
||||
@@ -17,34 +22,35 @@ import "./DropdownMenu.scss";
|
||||
const DropdownMenu = ({
|
||||
children,
|
||||
open,
|
||||
placement,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
open: boolean;
|
||||
placement?: "top" | "bottom";
|
||||
}) => {
|
||||
const MenuTriggerComp = getMenuTriggerComponent(children);
|
||||
const MenuContentComp = getMenuContentComponent(children);
|
||||
|
||||
// clone the MenuContentComp to pass the placement prop
|
||||
const MenuContentCompWithPlacement =
|
||||
const MenuContentWithState =
|
||||
MenuContentComp && React.isValidElement(MenuContentComp)
|
||||
? React.cloneElement(MenuContentComp as React.ReactElement<any>, {
|
||||
placement,
|
||||
})
|
||||
? React.cloneElement(
|
||||
MenuContentComp as React.ReactElement<
|
||||
React.ComponentProps<typeof DropdownMenuContent>
|
||||
>,
|
||||
{ open },
|
||||
)
|
||||
: MenuContentComp;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="dropdown-menu-container"
|
||||
style={{
|
||||
// remove this div from box layout
|
||||
display: "contents",
|
||||
}}
|
||||
>
|
||||
{MenuTriggerComp}
|
||||
{open && MenuContentCompWithPlacement}
|
||||
</div>
|
||||
<DropdownMenuPrimitive.Root open={open} modal={false}>
|
||||
<div
|
||||
className={CLASSES.DROPDOWN_MENU_EVENT_WRAPPER}
|
||||
style={{
|
||||
// remove this div from box layout
|
||||
display: "contents",
|
||||
}}
|
||||
>
|
||||
{MenuTriggerComp}
|
||||
{MenuContentWithState}
|
||||
</div>
|
||||
</DropdownMenuPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -55,6 +61,7 @@ DropdownMenu.ItemLink = DropdownMenuItemLink;
|
||||
DropdownMenu.ItemCustom = DropdownMenuItemCustom;
|
||||
DropdownMenu.Group = DropdownMenuGroup;
|
||||
DropdownMenu.Separator = MenuSeparator;
|
||||
DropdownMenu.Sub = DropdownMenuSub;
|
||||
|
||||
export default DropdownMenu;
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import clsx from "clsx";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
import { EVENT, KEYS } from "@excalidraw/common";
|
||||
import { CLASSES, EVENT, KEYS } from "@excalidraw/common";
|
||||
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
||||
|
||||
import { useOutsideClick } from "../../hooks/useOutsideClick";
|
||||
import { useStable } from "../../hooks/useStable";
|
||||
@@ -16,8 +18,9 @@ const MenuContent = ({
|
||||
onClickOutside,
|
||||
className = "",
|
||||
onSelect,
|
||||
open = true,
|
||||
align = "end",
|
||||
style,
|
||||
placement = "bottom",
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
onClickOutside?: () => void;
|
||||
@@ -26,26 +29,36 @@ const MenuContent = ({
|
||||
* Called when any menu item is selected (clicked on).
|
||||
*/
|
||||
onSelect?: (event: Event) => void;
|
||||
open?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
placement?: "top" | "bottom";
|
||||
align?: "start" | "center" | "end";
|
||||
}) => {
|
||||
const editorInterface = useEditorInterface();
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const callbacksRef = useStable({ onClickOutside });
|
||||
|
||||
useOutsideClick(menuRef, (event) => {
|
||||
// prevents closing if clicking on the trigger button
|
||||
if (
|
||||
!menuRef.current
|
||||
?.closest(".dropdown-menu-container")
|
||||
?.contains(event.target)
|
||||
) {
|
||||
callbacksRef.onClickOutside?.();
|
||||
}
|
||||
});
|
||||
useOutsideClick(
|
||||
menuRef,
|
||||
useCallback(
|
||||
(event) => {
|
||||
// prevents closing if clicking on the trigger button
|
||||
if (
|
||||
!menuRef.current
|
||||
?.closest(`.${CLASSES.DROPDOWN_MENU_EVENT_WRAPPER}`)
|
||||
?.contains(event.target)
|
||||
) {
|
||||
callbacksRef.onClickOutside?.();
|
||||
}
|
||||
},
|
||||
[callbacksRef],
|
||||
),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === KEYS.ESCAPE) {
|
||||
event.stopImmediatePropagation();
|
||||
@@ -63,35 +76,33 @@ const MenuContent = ({
|
||||
return () => {
|
||||
document.removeEventListener(EVENT.KEYDOWN, onKeyDown, option);
|
||||
};
|
||||
}, [callbacksRef]);
|
||||
}, [callbacksRef, open]);
|
||||
|
||||
const classNames = clsx(`dropdown-menu ${className}`, {
|
||||
"dropdown-menu--mobile": editorInterface.formFactor === "phone",
|
||||
"dropdown-menu--placement-top": placement === "top",
|
||||
}).trim();
|
||||
|
||||
return (
|
||||
<DropdownMenuContentPropsContext.Provider value={{ onSelect }}>
|
||||
<div
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={menuRef}
|
||||
className={classNames}
|
||||
style={style}
|
||||
data-testid="dropdown-menu"
|
||||
align={align}
|
||||
sideOffset={8}
|
||||
onCloseAutoFocus={(event: Event) => event.preventDefault()}
|
||||
>
|
||||
{/* the zIndex ensures this menu has higher stacking order,
|
||||
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
||||
{editorInterface.formFactor === "phone" ? (
|
||||
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
||||
) : (
|
||||
<Island
|
||||
className="dropdown-menu-container"
|
||||
padding={2}
|
||||
style={{ zIndex: 2 }}
|
||||
>
|
||||
<Island className="dropdown-menu-container" padding={2}>
|
||||
{children}
|
||||
</Island>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuPrimitive.Content>
|
||||
</DropdownMenuContentPropsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,78 +1,63 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { THEME } from "@excalidraw/common";
|
||||
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
||||
|
||||
import type { ValueOf } from "@excalidraw/common/utility-types";
|
||||
|
||||
import { useExcalidrawAppState } from "../App";
|
||||
|
||||
import MenuItemContent from "./DropdownMenuItemContent";
|
||||
import {
|
||||
getDropdownMenuItemClassName,
|
||||
useHandleDropdownMenuItemClick,
|
||||
useHandleDropdownMenuItemSelect,
|
||||
} from "./common";
|
||||
import MenuItemContent from "./DropdownMenuItemContent";
|
||||
|
||||
import type { JSX } from "react";
|
||||
|
||||
const DropdownMenuItem = ({
|
||||
icon,
|
||||
badge,
|
||||
value,
|
||||
order,
|
||||
children,
|
||||
shortcut,
|
||||
className,
|
||||
hovered,
|
||||
selected,
|
||||
textStyle,
|
||||
onSelect,
|
||||
onClick,
|
||||
badge,
|
||||
...rest
|
||||
}: {
|
||||
icon?: JSX.Element;
|
||||
badge?: React.ReactNode;
|
||||
value?: string | number | undefined;
|
||||
order?: number;
|
||||
onSelect?: (event: Event) => void;
|
||||
children: React.ReactNode;
|
||||
shortcut?: string;
|
||||
hovered?: boolean;
|
||||
selected?: boolean;
|
||||
textStyle?: React.CSSProperties;
|
||||
className?: string;
|
||||
badge?: React.ReactNode;
|
||||
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
||||
const handleClick = useHandleDropdownMenuItemClick(onClick, onSelect);
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (hovered) {
|
||||
if (order === 0) {
|
||||
// scroll into the first item differently, so it's visible what is above (i.e. group title)
|
||||
ref.current?.scrollIntoView({ block: "end" });
|
||||
} else {
|
||||
ref.current?.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}
|
||||
}, [hovered, order]);
|
||||
} & Omit<
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
"onSelect" | "onClick"
|
||||
>) => {
|
||||
const handleSelect = useHandleDropdownMenuItemSelect(onSelect);
|
||||
|
||||
return (
|
||||
<button
|
||||
{...rest}
|
||||
ref={ref}
|
||||
value={value}
|
||||
onClick={handleClick}
|
||||
className={getDropdownMenuItemClassName(className, selected, hovered)}
|
||||
title={rest.title ?? rest["aria-label"]}
|
||||
<DropdownMenuPrimitive.Item
|
||||
className="radix-menu-item"
|
||||
onSelect={handleSelect}
|
||||
asChild
|
||||
>
|
||||
<MenuItemContent
|
||||
textStyle={textStyle}
|
||||
icon={icon}
|
||||
shortcut={shortcut}
|
||||
badge={badge}
|
||||
<button
|
||||
{...rest}
|
||||
value={value}
|
||||
className={getDropdownMenuItemClassName(className, selected)}
|
||||
title={rest.title ?? rest["aria-label"]}
|
||||
>
|
||||
{children}
|
||||
</MenuItemContent>
|
||||
</button>
|
||||
<MenuItemContent icon={icon} shortcut={shortcut} badge={badge}>
|
||||
{children}
|
||||
</MenuItemContent>
|
||||
</button>
|
||||
</DropdownMenuPrimitive.Item>
|
||||
);
|
||||
};
|
||||
DropdownMenuItem.displayName = "DropdownMenuItem";
|
||||
|
||||
@@ -27,9 +27,7 @@ const DropdownMenuItemContentRadio = <T,>({
|
||||
return (
|
||||
<>
|
||||
<div className="dropdown-menu-item-base dropdown-menu-item-bare">
|
||||
<label className="dropdown-menu-item__text" htmlFor={name}>
|
||||
{children}
|
||||
</label>
|
||||
<label className="dropdown-menu-item__text">{children}</label>
|
||||
<RadioGroup
|
||||
name={name}
|
||||
value={value}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
||||
|
||||
import MenuItemContent from "./DropdownMenuItemContent";
|
||||
import {
|
||||
getDropdownMenuItemClassName,
|
||||
useHandleDropdownMenuItemClick,
|
||||
useHandleDropdownMenuItemSelect,
|
||||
} from "./common";
|
||||
|
||||
import type { JSX } from "react";
|
||||
@@ -28,23 +30,28 @@ const DropdownMenuItemLink = ({
|
||||
onSelect?: (event: Event) => void;
|
||||
rel?: string;
|
||||
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||
const handleSelect = useHandleDropdownMenuItemSelect(onSelect);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-no-target-blank
|
||||
<a
|
||||
{...rest}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel={rel || "noopener"}
|
||||
className={getDropdownMenuItemClassName(className, selected)}
|
||||
title={rest.title ?? rest["aria-label"]}
|
||||
onClick={handleClick}
|
||||
<DropdownMenuPrimitive.Item
|
||||
className="radix-menu-item"
|
||||
onSelect={handleSelect}
|
||||
asChild
|
||||
>
|
||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||
{children}
|
||||
</MenuItemContent>
|
||||
</a>
|
||||
<a
|
||||
{...rest}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel={`noopener ${rel}`}
|
||||
className={getDropdownMenuItemClassName(className, selected)}
|
||||
title={rest.title ?? rest["aria-label"]}
|
||||
>
|
||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||
{children}
|
||||
</MenuItemContent>
|
||||
</a>
|
||||
</DropdownMenuPrimitive.Item>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const MenuSeparator = () => (
|
||||
style={{
|
||||
height: "1px",
|
||||
backgroundColor: "var(--default-border-color)",
|
||||
margin: ".5rem 0",
|
||||
margin: "6px 0",
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
||||
|
||||
import DropdownMenuSubContent from "./DropdownMenuSubContent";
|
||||
import DropdownMenuSubTrigger from "./DropdownMenuSubTrigger";
|
||||
import {
|
||||
getSubMenuContentComponent,
|
||||
getSubMenuTriggerComponent,
|
||||
} from "./dropdownMenuUtils";
|
||||
|
||||
const DropdownMenuSub = ({ children }: { children?: React.ReactNode }) => {
|
||||
const MenuTriggerComp = getSubMenuTriggerComponent(children);
|
||||
const MenuContentComp = getSubMenuContentComponent(children);
|
||||
return (
|
||||
<DropdownMenuPrimitive.Sub>
|
||||
{MenuTriggerComp}
|
||||
{MenuContentComp}
|
||||
</DropdownMenuPrimitive.Sub>
|
||||
);
|
||||
};
|
||||
|
||||
DropdownMenuSub.Trigger = DropdownMenuSubTrigger;
|
||||
DropdownMenuSub.Content = DropdownMenuSubContent;
|
||||
|
||||
DropdownMenuSub.displayName = "DropdownMenuSub";
|
||||
|
||||
export default DropdownMenuSub;
|
||||
@@ -0,0 +1,71 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { useEditorInterface } from "../App";
|
||||
import { Island } from "../Island";
|
||||
import Stack from "../Stack";
|
||||
|
||||
const BASE_ALIGN_OFFSET = -4;
|
||||
const BASE_SIDE_OFFSET = 4;
|
||||
|
||||
const DropdownMenuSubContent = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
const editorInterface = useEditorInterface();
|
||||
|
||||
const classNames = clsx(`dropdown-menu dropdown-submenu ${className}`, {
|
||||
"dropdown-menu--mobile": editorInterface.formFactor === "phone",
|
||||
}).trim();
|
||||
|
||||
const callbacksRef = useCallback((node: HTMLDivElement | null) => {
|
||||
if (node) {
|
||||
const parentContainer = node.closest(".dropdown-menu-container");
|
||||
const parentRect = parentContainer?.getBoundingClientRect();
|
||||
if (parentRect) {
|
||||
const menuWidth = node.getBoundingClientRect().width;
|
||||
|
||||
const viewportWidth = window.innerWidth;
|
||||
const spaceRemaining = viewportWidth - parentRect.right;
|
||||
if (spaceRemaining < menuWidth + 20) {
|
||||
setSideOffset(spaceRemaining - menuWidth + BASE_ALIGN_OFFSET);
|
||||
setAlignOffset(BASE_ALIGN_OFFSET + 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [sideOffset, setSideOffset] = useState(BASE_SIDE_OFFSET);
|
||||
const [alignOffset, setAlignOffset] = useState(BASE_ALIGN_OFFSET);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
className={classNames}
|
||||
sideOffset={sideOffset}
|
||||
alignOffset={alignOffset}
|
||||
collisionPadding={8}
|
||||
ref={callbacksRef}
|
||||
>
|
||||
{editorInterface.formFactor === "phone" ? (
|
||||
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
||||
) : (
|
||||
<Island
|
||||
className="dropdown-menu-container"
|
||||
padding={2}
|
||||
style={{ zIndex: 1 }}
|
||||
>
|
||||
{children}
|
||||
</Island>
|
||||
)}
|
||||
</DropdownMenuPrimitive.SubContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownMenuSubContent;
|
||||
DropdownMenuSubContent.displayName = "DropdownMenuSubContent";
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
||||
|
||||
import { chevronRight } from "../icons";
|
||||
|
||||
import { getDropdownMenuItemClassName } from "./common";
|
||||
import MenuItemContent from "./DropdownMenuItemContent";
|
||||
|
||||
import type { JSX } from "react";
|
||||
|
||||
const DropdownMenuSubTrigger = ({
|
||||
children,
|
||||
icon,
|
||||
shortcut,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
icon?: JSX.Element;
|
||||
shortcut?: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
className={`${getDropdownMenuItemClassName(
|
||||
className,
|
||||
)} dropdown-menu__submenu-trigger`}
|
||||
>
|
||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||
{children}
|
||||
</MenuItemContent>
|
||||
<div className="dropdown-menu__submenu-trigger-icon">{chevronRight}</div>
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownMenuSubTrigger;
|
||||
DropdownMenuSubTrigger.displayName = "DropdownMenuSubTrigger";
|
||||
@@ -1,5 +1,7 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
||||
|
||||
import { useEditorInterface } from "../App";
|
||||
|
||||
const MenuTrigger = ({
|
||||
@@ -23,7 +25,7 @@ const MenuTrigger = ({
|
||||
},
|
||||
).trim();
|
||||
return (
|
||||
<button
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
className={classNames}
|
||||
onClick={onToggle}
|
||||
type="button"
|
||||
@@ -32,7 +34,7 @@ const MenuTrigger = ({
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</DropdownMenuPrimitive.Trigger>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import { EVENT, composeEventHandlers } from "@excalidraw/common";
|
||||
import { composeEventHandlers } from "@excalidraw/common";
|
||||
|
||||
export const DropdownMenuContentPropsContext = React.createContext<{
|
||||
onSelect?: (event: Event) => void;
|
||||
@@ -11,28 +11,17 @@ export const getDropdownMenuItemClassName = (
|
||||
selected = false,
|
||||
hovered = false,
|
||||
) => {
|
||||
return `dropdown-menu-item dropdown-menu-item-base ${className}
|
||||
${selected ? "dropdown-menu-item--selected" : ""} ${
|
||||
hovered ? "dropdown-menu-item--hovered" : ""
|
||||
}`.trim();
|
||||
return `dropdown-menu-item dropdown-menu-item-base ${className} ${
|
||||
selected ? "dropdown-menu-item--selected" : ""
|
||||
} ${hovered ? "dropdown-menu-item--hovered" : ""}`.trim();
|
||||
};
|
||||
|
||||
export const useHandleDropdownMenuItemClick = (
|
||||
origOnClick:
|
||||
| React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement>
|
||||
| undefined,
|
||||
export const useHandleDropdownMenuItemSelect = (
|
||||
onSelect: ((event: Event) => void) | undefined,
|
||||
) => {
|
||||
const DropdownMenuContentProps = useContext(DropdownMenuContentPropsContext);
|
||||
|
||||
return composeEventHandlers(origOnClick, (event) => {
|
||||
const itemSelectEvent = new CustomEvent(EVENT.MENU_ITEM_SELECT, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
onSelect?.(itemSelectEvent);
|
||||
if (!itemSelectEvent.defaultPrevented) {
|
||||
DropdownMenuContentProps.onSelect?.(itemSelectEvent);
|
||||
}
|
||||
return composeEventHandlers(onSelect, (event) => {
|
||||
DropdownMenuContentProps.onSelect?.(event);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
export const getMenuTriggerComponent = (children: React.ReactNode) => {
|
||||
const getMenuComponent = (component: string) => (children: React.ReactNode) => {
|
||||
const comp = React.Children.toArray(children).find(
|
||||
(child) =>
|
||||
React.isValidElement(child) &&
|
||||
@@ -8,7 +8,7 @@ export const getMenuTriggerComponent = (children: React.ReactNode) => {
|
||||
//@ts-ignore
|
||||
child?.type.displayName &&
|
||||
//@ts-ignore
|
||||
child.type.displayName === "DropdownMenuTrigger",
|
||||
child.type.displayName === component,
|
||||
);
|
||||
if (!comp) {
|
||||
return null;
|
||||
@@ -17,19 +17,11 @@ export const getMenuTriggerComponent = (children: React.ReactNode) => {
|
||||
return comp;
|
||||
};
|
||||
|
||||
export const getMenuContentComponent = (children: React.ReactNode) => {
|
||||
const comp = React.Children.toArray(children).find(
|
||||
(child) =>
|
||||
React.isValidElement(child) &&
|
||||
typeof child.type !== "string" &&
|
||||
//@ts-ignore
|
||||
child?.type.displayName &&
|
||||
//@ts-ignore
|
||||
child.type.displayName === "DropdownMenuContent",
|
||||
);
|
||||
if (!comp) {
|
||||
return null;
|
||||
}
|
||||
//@ts-ignore
|
||||
return comp;
|
||||
};
|
||||
export const getMenuTriggerComponent = getMenuComponent("DropdownMenuTrigger");
|
||||
export const getMenuContentComponent = getMenuComponent("DropdownMenuContent");
|
||||
export const getSubMenuTriggerComponent = getMenuComponent(
|
||||
"DropdownMenuSubTrigger",
|
||||
);
|
||||
export const getSubMenuContentComponent = getMenuComponent(
|
||||
"DropdownMenuSubContent",
|
||||
);
|
||||
|
||||
@@ -2396,3 +2396,15 @@ export const presentationIcon = createIcon(
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
// empty placeholder icon (used for alignment in menus)
|
||||
export const emptyIcon = <div style={{ width: "1rem", height: "1rem" }} />;
|
||||
|
||||
//tabler-icons: chevron-right
|
||||
export const chevronRight = createIcon(
|
||||
<g strokeWidth="1.5">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<polyline points="9 6 15 12 9 18" />
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
@@ -306,10 +306,14 @@ export const ChangeCanvasBackground = () => {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div style={{ marginTop: "0.5rem" }}>
|
||||
<div style={{ marginTop: "0.75rem" }}>
|
||||
<div
|
||||
data-testid="canvas-background-label"
|
||||
style={{ fontSize: ".75rem", marginBottom: ".5rem" }}
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
marginBottom: "0.25rem",
|
||||
marginLeft: "0.5rem",
|
||||
}}
|
||||
>
|
||||
{t("labels.canvasBackground")}
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { t } from "../../i18n";
|
||||
import { useEditorInterface, useExcalidrawSetAppState } from "../App";
|
||||
import { UserList } from "../UserList";
|
||||
import DropdownMenu from "../dropdownMenu/DropdownMenu";
|
||||
import DropdownMenuSub from "../dropdownMenu/DropdownMenuSub";
|
||||
import { withInternalFallback } from "../hoc/withInternalFallback";
|
||||
import { HamburgerMenuIcon } from "../icons";
|
||||
|
||||
@@ -52,12 +53,8 @@ const MainMenu = Object.assign(
|
||||
onSelect={composeEventHandlers(onSelect, () => {
|
||||
setAppState({ openMenu: null });
|
||||
})}
|
||||
placement="bottom"
|
||||
className={
|
||||
editorInterface.formFactor === "phone"
|
||||
? "main-menu-dropdown"
|
||||
: ""
|
||||
}
|
||||
className="main-menu"
|
||||
align="start"
|
||||
>
|
||||
{children}
|
||||
{editorInterface.formFactor === "phone" &&
|
||||
@@ -84,6 +81,7 @@ const MainMenu = Object.assign(
|
||||
ItemCustom: DropdownMenu.ItemCustom,
|
||||
Group: DropdownMenu.Group,
|
||||
Separator: DropdownMenu.Separator,
|
||||
Sub: DropdownMenuSub,
|
||||
DefaultItems,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
--zIndex-ui-context-menu: 90;
|
||||
--zIndex-ui-styles-popup: 100;
|
||||
--zIndex-ui-top: 100;
|
||||
--zIndex-ui-main-menu: 110;
|
||||
--zIndex-ui-library: 120;
|
||||
|
||||
--zIndex-modal: 1000;
|
||||
@@ -223,6 +224,18 @@ body.excalidraw-cursor-resize * {
|
||||
box-shadow: 0 0 0 1px var(--color-brand-hover);
|
||||
}
|
||||
|
||||
// radix doesn't allow differntiating between hover and keyboard active
|
||||
// states (it's forcing :focus on both).
|
||||
//
|
||||
// proper handling would be to disable :focus-visible by default, and enable
|
||||
// on keyboard arrows (it'd then have to be disabled again, e.g. on keydown
|
||||
// or container focus)
|
||||
//
|
||||
// alas, that is left for another day
|
||||
[data-radix-collection-item]:focus-visible {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.buttonList {
|
||||
.ToolIcon__icon {
|
||||
all: unset !important;
|
||||
@@ -670,6 +683,9 @@ body.excalidraw-cursor-resize * {
|
||||
}
|
||||
}
|
||||
|
||||
.main-menu {
|
||||
z-index: var(--zIndex-ui-main-menu);
|
||||
}
|
||||
.main-menu-trigger {
|
||||
@include filledButtonOnCanvas;
|
||||
}
|
||||
|
||||
@@ -85,8 +85,7 @@
|
||||
"@excalidraw/math": "0.18.0",
|
||||
"@excalidraw/mermaid-to-excalidraw": "2.0.0-rfc3",
|
||||
"@excalidraw/random-username": "1.1.0",
|
||||
"@radix-ui/react-popover": "1.1.6",
|
||||
"@radix-ui/react-tabs": "1.1.3",
|
||||
"radix-ui": "1.4.3",
|
||||
"browser-fs-access": "0.29.1",
|
||||
"canvas-roundrect-polyfill": "0.0.1",
|
||||
"clsx": "1.1.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Test <MermaidToExcalidraw/> > should open mermaid popup when active tool is mermaid 1`] = `
|
||||
"<div class="Modal Dialog ttd-dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title"><div class="Modal__background"></div><div class="Modal__content" style="--max-width: 1520px;" tabindex="0"><div class="Island"><div class="Dialog__content"><div dir="ltr" data-orientation="horizontal" class="ttd-dialog-tabs-root"><p class="dialog-mermaid-title">Mermaid to Excalidraw</p><div data-state="active" data-orientation="horizontal" role="tabpanel" aria-labelledby="radix-:r0:-trigger-mermaid" id="radix-:r0:-content-mermaid" tabindex="0" class="ttd-dialog-content" style=""><div class="ttd-dialog-desc">Currently only <a href="https://mermaid.js.org/syntax/flowchart.html">Flowchart</a>,<a href="https://mermaid.js.org/syntax/sequenceDiagram.html"> Sequence, </a> and <a href="https://mermaid.js.org/syntax/classDiagram.html">Class </a>Diagrams are supported. The other types will be rendered as image in Excalidraw.</div><div class="ttd-dialog-panels"><div class="ttd-dialog-panel"><textarea class="ttd-dialog-input" placeholder="Write Mermaid diagram defintion here...">flowchart TD
|
||||
"<div class="Modal Dialog ttd-dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title"><div class="Modal__background"></div><div class="Modal__content" style="--max-width: 1520px;" tabindex="0"><div class="Island"><div class="Dialog__content"><div dir="ltr" data-orientation="horizontal" class="ttd-dialog-tabs-root"><p class="dialog-mermaid-title">Mermaid to Excalidraw</p><div data-state="active" data-orientation="horizontal" role="tabpanel" aria-labelledby="radix-:r4:-trigger-mermaid" id="radix-:r4:-content-mermaid" tabindex="0" class="ttd-dialog-content" style=""><div class="ttd-dialog-desc">Currently only <a href="https://mermaid.js.org/syntax/flowchart.html">Flowchart</a>,<a href="https://mermaid.js.org/syntax/sequenceDiagram.html"> Sequence, </a> and <a href="https://mermaid.js.org/syntax/classDiagram.html">Class </a>Diagrams are supported. The other types will be rendered as image in Excalidraw.</div><div class="ttd-dialog-panels"><div class="ttd-dialog-panel"><textarea class="ttd-dialog-input" placeholder="Write Mermaid diagram defintion here...">flowchart TD
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
C -->|One| D[Laptop]
|
||||
|
||||
@@ -2,15 +2,31 @@
|
||||
|
||||
exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu items if passed from host 1`] = `
|
||||
<div
|
||||
class="dropdown-menu"
|
||||
aria-labelledby="radix-:r7t:"
|
||||
aria-orientation="vertical"
|
||||
class="dropdown-menu main-menu"
|
||||
data-align="start"
|
||||
data-orientation="vertical"
|
||||
data-radix-menu-content=""
|
||||
data-side="bottom"
|
||||
data-state="open"
|
||||
data-testid="dropdown-menu"
|
||||
dir="ltr"
|
||||
id="radix-:r7u:"
|
||||
role="menu"
|
||||
style="outline: none; --radix-dropdown-menu-content-transform-origin: var(--radix-popper-transform-origin); --radix-dropdown-menu-content-available-width: var(--radix-popper-available-width); --radix-dropdown-menu-content-available-height: var(--radix-popper-available-height); --radix-dropdown-menu-trigger-width: var(--radix-popper-anchor-width); --radix-dropdown-menu-trigger-height: var(--radix-popper-anchor-height); animation: none;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="Island dropdown-menu-container"
|
||||
style="--padding: 2; z-index: 2;"
|
||||
style="--padding: 2;"
|
||||
>
|
||||
<button
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
class="radix-menu-item dropdown-menu-item dropdown-menu-item-base"
|
||||
data-orientation="vertical"
|
||||
data-radix-collection-item=""
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="dropdown-menu-item__text"
|
||||
@@ -23,9 +39,13 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
||||
</div>
|
||||
</button>
|
||||
<a
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
class="radix-menu-item dropdown-menu-item dropdown-menu-item-base"
|
||||
data-orientation="vertical"
|
||||
data-radix-collection-item=""
|
||||
href="blog.excalidaw.com"
|
||||
rel="noopener"
|
||||
rel="noopener noopener"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
@@ -49,8 +69,12 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
||||
</div>
|
||||
<button
|
||||
aria-label="Help"
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
class="radix-menu-item dropdown-menu-item dropdown-menu-item-base"
|
||||
data-orientation="vertical"
|
||||
data-radix-collection-item=""
|
||||
data-testid="help-menu-item"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
title="Help"
|
||||
>
|
||||
<div
|
||||
@@ -114,17 +138,33 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
||||
|
||||
exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should render menu with default items when "UIOPtions" is "undefined" 1`] = `
|
||||
<div
|
||||
class="dropdown-menu"
|
||||
aria-labelledby="radix-:rq:"
|
||||
aria-orientation="vertical"
|
||||
class="dropdown-menu main-menu"
|
||||
data-align="start"
|
||||
data-orientation="vertical"
|
||||
data-radix-menu-content=""
|
||||
data-side="bottom"
|
||||
data-state="open"
|
||||
data-testid="dropdown-menu"
|
||||
dir="ltr"
|
||||
id="radix-:rr:"
|
||||
role="menu"
|
||||
style="outline: none; --radix-dropdown-menu-content-transform-origin: var(--radix-popper-transform-origin); --radix-dropdown-menu-content-available-width: var(--radix-popper-available-width); --radix-dropdown-menu-content-available-height: var(--radix-popper-available-height); --radix-dropdown-menu-trigger-width: var(--radix-popper-anchor-width); --radix-dropdown-menu-trigger-height: var(--radix-popper-anchor-height); animation: none;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="Island dropdown-menu-container"
|
||||
style="--padding: 2; z-index: 2;"
|
||||
style="--padding: 2;"
|
||||
>
|
||||
<button
|
||||
aria-label="Open"
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
class="radix-menu-item dropdown-menu-item dropdown-menu-item-base"
|
||||
data-orientation="vertical"
|
||||
data-radix-collection-item=""
|
||||
data-testid="load-button"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
title="Open"
|
||||
>
|
||||
<div
|
||||
@@ -164,8 +204,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</button>
|
||||
<button
|
||||
aria-label="Save to..."
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
class="radix-menu-item dropdown-menu-item dropdown-menu-item-base"
|
||||
data-orientation="vertical"
|
||||
data-radix-collection-item=""
|
||||
data-testid="json-export-button"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
title="Save to..."
|
||||
>
|
||||
<div
|
||||
@@ -200,8 +244,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</button>
|
||||
<button
|
||||
aria-label="Export image..."
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
class="radix-menu-item dropdown-menu-item dropdown-menu-item-base"
|
||||
data-orientation="vertical"
|
||||
data-radix-collection-item=""
|
||||
data-testid="image-export-button"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
title="Export image..."
|
||||
>
|
||||
<div
|
||||
@@ -265,8 +313,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</button>
|
||||
<button
|
||||
aria-label="Find on canvas"
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
class="radix-menu-item dropdown-menu-item dropdown-menu-item-base"
|
||||
data-orientation="vertical"
|
||||
data-radix-collection-item=""
|
||||
data-testid="search-menu-button"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
title="Find on canvas"
|
||||
>
|
||||
<div
|
||||
@@ -318,8 +370,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</button>
|
||||
<button
|
||||
aria-label="Help"
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
class="radix-menu-item dropdown-menu-item dropdown-menu-item-base"
|
||||
data-orientation="vertical"
|
||||
data-radix-collection-item=""
|
||||
data-testid="help-menu-item"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
title="Help"
|
||||
>
|
||||
<div
|
||||
@@ -379,8 +435,12 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</button>
|
||||
<button
|
||||
aria-label="Reset the canvas"
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
class="radix-menu-item dropdown-menu-item dropdown-menu-item-base"
|
||||
data-orientation="vertical"
|
||||
data-radix-collection-item=""
|
||||
data-testid="clear-canvas-button"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
title="Reset the canvas"
|
||||
>
|
||||
<div
|
||||
@@ -414,7 +474,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
style="height: 1px; margin: .5rem 0px; flex: 0 0 auto;"
|
||||
style="height: 1px; margin: 6px 0px; flex: 0 0 auto;"
|
||||
/>
|
||||
<div
|
||||
class="dropdown-menu-group "
|
||||
@@ -426,9 +486,13 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</p>
|
||||
<a
|
||||
aria-label="GitHub"
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
class="radix-menu-item dropdown-menu-item dropdown-menu-item-base"
|
||||
data-orientation="vertical"
|
||||
data-radix-collection-item=""
|
||||
href="https://github.com/excalidraw/excalidraw"
|
||||
rel="noopener"
|
||||
rel="noopener noopener"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
target="_blank"
|
||||
title="GitHub"
|
||||
>
|
||||
@@ -464,9 +528,13 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</a>
|
||||
<a
|
||||
aria-label="X"
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
class="radix-menu-item dropdown-menu-item dropdown-menu-item-base"
|
||||
data-orientation="vertical"
|
||||
data-radix-collection-item=""
|
||||
href="https://x.com/excalidraw"
|
||||
rel="noopener"
|
||||
rel="noopener noopener"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
target="_blank"
|
||||
title="X"
|
||||
>
|
||||
@@ -514,9 +582,13 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</a>
|
||||
<a
|
||||
aria-label="Discord"
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
class="radix-menu-item dropdown-menu-item dropdown-menu-item-base"
|
||||
data-orientation="vertical"
|
||||
data-radix-collection-item=""
|
||||
href="https://discord.gg/UexuTaE"
|
||||
rel="noopener"
|
||||
rel="noopener noopener"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
target="_blank"
|
||||
title="Discord"
|
||||
>
|
||||
@@ -558,12 +630,16 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
style="height: 1px; margin: .5rem 0px; flex: 0 0 auto;"
|
||||
style="height: 1px; margin: 6px 0px; flex: 0 0 auto;"
|
||||
/>
|
||||
<button
|
||||
aria-label="Dark mode"
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
class="radix-menu-item dropdown-menu-item dropdown-menu-item-base"
|
||||
data-orientation="vertical"
|
||||
data-radix-collection-item=""
|
||||
data-testid="toggle-dark-mode"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
title="Dark mode"
|
||||
>
|
||||
<div
|
||||
@@ -603,11 +679,11 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
style="margin-top: 0.5rem;"
|
||||
style="margin-top: 0.75rem;"
|
||||
>
|
||||
<div
|
||||
data-testid="canvas-background-label"
|
||||
style="font-size: .75rem; margin-bottom: .5rem;"
|
||||
style="font-size: 0.875rem; margin-bottom: 0.25rem; margin-left: 0.5rem;"
|
||||
>
|
||||
Canvas background
|
||||
</div>
|
||||
@@ -683,7 +759,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
style="width: 1px; height: 1rem; margin: 0px auto;"
|
||||
/>
|
||||
<button
|
||||
aria-controls="radix-:r0:"
|
||||
aria-controls="radix-:r16:"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="dialog"
|
||||
aria-label="Canvas background"
|
||||
|
||||
@@ -1550,7 +1550,6 @@ describe("textWysiwyg", () => {
|
||||
expect(
|
||||
(h.elements[1] as ExcalidrawTextElementWithContainer).verticalAlign,
|
||||
).toBe(VERTICAL_ALIGN.BOTTOM);
|
||||
|
||||
// Attempt to Bind 2nd text using text tool
|
||||
UI.clickTool("text");
|
||||
mouse.clickAt(
|
||||
|
||||
Reference in New Issue
Block a user