Compare commits
33 Commits
master
...
barnabasmo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06dae6edf2 | ||
|
|
ecbaeb1701 | ||
|
|
40c5c743b1 | ||
|
|
5082142b36 | ||
|
|
74cb027fd7 | ||
|
|
bc09ac757f | ||
|
|
66e347f7d2 | ||
|
|
d5974e66b2 | ||
|
|
2a1b22a504 | ||
|
|
b3d241ba7f | ||
|
|
8ff1ac8097 | ||
|
|
d967123383 | ||
|
|
05cd1a79cc | ||
|
|
bd08bdf4c7 | ||
|
|
011b268dde | ||
|
|
b6a7f05761 | ||
|
|
8787c7d8cf | ||
|
|
6d21d7cab1 | ||
|
|
c9df3e143b | ||
|
|
5b11660cc0 | ||
|
|
bf0b2965e6 | ||
|
|
8f8b6e7144 | ||
|
|
b63d17045e | ||
|
|
70d48d5472 | ||
|
|
097000a2b7 | ||
|
|
461661afc6 | ||
|
|
c88f3c84eb | ||
|
|
7d791b86f8 | ||
|
|
e615056302 | ||
|
|
14ad745d00 | ||
|
|
9c3ff73a73 | ||
|
|
79cf71cccb | ||
|
|
e094b8b539 |
@ -615,6 +615,52 @@ export default function ExampleApp({
|
|||||||
const renderMenu = () => {
|
const renderMenu = () => {
|
||||||
return (
|
return (
|
||||||
<MainMenu>
|
<MainMenu>
|
||||||
|
<MainMenu.Sub>
|
||||||
|
<MainMenu.Sub.Trigger
|
||||||
|
title="Custom trigger"
|
||||||
|
icon={
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M15.042 21.672L13.684 16.6m0 0l-2.51 2.225.569-9.47 5.227 7.917-3.286-.672zm-7.518-.267A8.25 8.25 0 1120.25 10.5M8.288 14.212A5.25 5.25 0 1117.25 10.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Submenu trigger
|
||||||
|
</MainMenu.Sub.Trigger>
|
||||||
|
<MainMenu.Sub.Content>
|
||||||
|
<MainMenu.Sub.Item
|
||||||
|
icon={
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 01-2.25 2.25M16.5 7.5V18a2.25 2.25 0 002.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 002.25 2.25h13.5M6 7.5h3v3H6v-3z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
onSelect={() => window.alert("You clicked on sub item")}
|
||||||
|
>
|
||||||
|
Sub item
|
||||||
|
</MainMenu.Sub.Item>
|
||||||
|
</MainMenu.Sub.Content>
|
||||||
|
</MainMenu.Sub>
|
||||||
<MainMenu.DefaultItems.SaveAsImage />
|
<MainMenu.DefaultItems.SaveAsImage />
|
||||||
<MainMenu.DefaultItems.Export />
|
<MainMenu.DefaultItems.Export />
|
||||||
<MainMenu.Separator />
|
<MainMenu.Separator />
|
||||||
@ -622,10 +668,57 @@ export default function ExampleApp({
|
|||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
onSelect={() => window.alert("You clicked on collab button")}
|
onSelect={() => window.alert("You clicked on collab button")}
|
||||||
/>
|
/>
|
||||||
|
<MainMenu.Sub>
|
||||||
|
<MainMenu.Sub.Trigger>Trigger</MainMenu.Sub.Trigger>
|
||||||
|
<MainMenu.Sub.Content>
|
||||||
|
<MainMenu.Sub.Item
|
||||||
|
onSelect={() => window.alert("You clicked on sub item")}
|
||||||
|
>
|
||||||
|
Sub item
|
||||||
|
</MainMenu.Sub.Item>
|
||||||
|
</MainMenu.Sub.Content>
|
||||||
|
</MainMenu.Sub>
|
||||||
<MainMenu.Group title="Excalidraw links">
|
<MainMenu.Group title="Excalidraw links">
|
||||||
<MainMenu.DefaultItems.Socials />
|
<MainMenu.DefaultItems.Socials />
|
||||||
</MainMenu.Group>
|
</MainMenu.Group>
|
||||||
<MainMenu.Separator />
|
{/* <MainMenu.Separator /> */}
|
||||||
|
<MainMenu.Sub>
|
||||||
|
<MainMenu.Sub.Trigger className="custom-classname">
|
||||||
|
Another submenu trigger
|
||||||
|
</MainMenu.Sub.Trigger>
|
||||||
|
<MainMenu.Sub.Content className="custom-classname-for-content">
|
||||||
|
<MainMenu.Sub.Item
|
||||||
|
title="Sub item"
|
||||||
|
onSelect={() => window.alert("You clicked on sub item")}
|
||||||
|
>
|
||||||
|
Sub item
|
||||||
|
</MainMenu.Sub.Item>
|
||||||
|
</MainMenu.Sub.Content>
|
||||||
|
</MainMenu.Sub>
|
||||||
|
<MainMenu.Sub>
|
||||||
|
<MainMenu.Sub.Trigger>Trigger me</MainMenu.Sub.Trigger>
|
||||||
|
<MainMenu.Sub.Content>
|
||||||
|
<MainMenu.Sub>
|
||||||
|
<MainMenu.Sub.Trigger>Trigger me inside</MainMenu.Sub.Trigger>
|
||||||
|
<MainMenu.Sub.Content>
|
||||||
|
<MainMenu.Sub.Item
|
||||||
|
onSelect={() => {
|
||||||
|
alert("wow, nested submenus!");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Item wow
|
||||||
|
</MainMenu.Sub.Item>
|
||||||
|
</MainMenu.Sub.Content>
|
||||||
|
</MainMenu.Sub>
|
||||||
|
<MainMenu.Sub.Item
|
||||||
|
onSelect={() => {
|
||||||
|
alert("wow, nested submenus! very cool");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Another one
|
||||||
|
</MainMenu.Sub.Item>
|
||||||
|
</MainMenu.Sub.Content>
|
||||||
|
</MainMenu.Sub>
|
||||||
<MainMenu.ItemCustom>
|
<MainMenu.ItemCustom>
|
||||||
<button
|
<button
|
||||||
style={{ height: "2rem" }}
|
style={{ height: "2rem" }}
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export const AppMainMenu: React.FC<{
|
|||||||
<MainMenu.DefaultItems.SearchMenu />
|
<MainMenu.DefaultItems.SearchMenu />
|
||||||
<MainMenu.DefaultItems.Help />
|
<MainMenu.DefaultItems.Help />
|
||||||
<MainMenu.DefaultItems.ClearCanvas />
|
<MainMenu.DefaultItems.ClearCanvas />
|
||||||
|
<MainMenu.DefaultItems.Preferences />
|
||||||
<MainMenu.Separator />
|
<MainMenu.Separator />
|
||||||
<MainMenu.ItemLink
|
<MainMenu.ItemLink
|
||||||
icon={ExcalLogo}
|
icon={ExcalLogo}
|
||||||
|
|||||||
@ -54,7 +54,8 @@ export type ShortcutName =
|
|||||||
| "saveScene"
|
| "saveScene"
|
||||||
| "imageExport"
|
| "imageExport"
|
||||||
| "commandPalette"
|
| "commandPalette"
|
||||||
| "searchMenu";
|
| "searchMenu"
|
||||||
|
| "toolLock";
|
||||||
|
|
||||||
const shortcutMap: Record<ShortcutName, string[]> = {
|
const shortcutMap: Record<ShortcutName, string[]> = {
|
||||||
toggleTheme: [getShortcutKey("Shift+Alt+D")],
|
toggleTheme: [getShortcutKey("Shift+Alt+D")],
|
||||||
@ -116,6 +117,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
toggleShortcuts: [getShortcutKey("?")],
|
toggleShortcuts: [getShortcutKey("?")],
|
||||||
searchMenu: [getShortcutKey("CtrlOrCmd+F")],
|
searchMenu: [getShortcutKey("CtrlOrCmd+F")],
|
||||||
wrapSelectionInFrame: [],
|
wrapSelectionInFrame: [],
|
||||||
|
toolLock: [getShortcutKey("Q")],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getShortcutFromShortcutName = (name: ShortcutName, idx = 0) => {
|
export const getShortcutFromShortcutName = (name: ShortcutName, idx = 0) => {
|
||||||
|
|||||||
@ -397,6 +397,7 @@ export const ShapesSwitcher = ({
|
|||||||
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
||||||
onSelect={() => setIsExtraToolsMenuOpen(false)}
|
onSelect={() => setIsExtraToolsMenuOpen(false)}
|
||||||
className="App-toolbar__extra-tools-dropdown"
|
className="App-toolbar__extra-tools-dropdown"
|
||||||
|
align="end"
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={() => app.setActiveTool({ type: "frame" })}
|
onSelect={() => app.setActiveTool({ type: "frame" })}
|
||||||
@ -450,10 +451,10 @@ export const ShapesSwitcher = ({
|
|||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={() => app.onMagicframeToolSelect()}
|
onSelect={() => app.onMagicframeToolSelect()}
|
||||||
icon={MagicIcon}
|
icon={MagicIcon}
|
||||||
|
badge={<DropdownMenu.Item.Badge>AI</DropdownMenu.Item.Badge>}
|
||||||
data-testid="toolbar-magicframe"
|
data-testid="toolbar-magicframe"
|
||||||
>
|
>
|
||||||
{t("toolBar.magicframe")}
|
{t("toolBar.magicframe")}
|
||||||
<DropdownMenu.Item.Badge>AI</DropdownMenu.Item.Badge>
|
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ interface ButtonProps
|
|||||||
HTMLButtonElement
|
HTMLButtonElement
|
||||||
> {
|
> {
|
||||||
type?: "button" | "submit" | "reset";
|
type?: "button" | "submit" | "reset";
|
||||||
onSelect: () => any;
|
onSelect: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => any;
|
||||||
/** whether button is in active state */
|
/** whether button is in active state */
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -34,7 +34,7 @@ export const Button = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={composeEventHandlers(rest.onClick, (event) => {
|
onClick={composeEventHandlers(rest.onClick, (event) => {
|
||||||
onSelect();
|
onSelect(event);
|
||||||
})}
|
})}
|
||||||
type={type}
|
type={type}
|
||||||
className={clsx("excalidraw-button", className, { selected })}
|
className={clsx("excalidraw-button", className, { selected })}
|
||||||
|
|||||||
@ -30,6 +30,18 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#canvas-bg-color-picker-container {
|
||||||
|
.color-picker__top-picks {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker-container {
|
||||||
|
@include isMobile {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.color-picker__button {
|
.color-picker__button {
|
||||||
--radius: 4px;
|
--radius: 4px;
|
||||||
--size: 1.375rem;
|
--size: 1.375rem;
|
||||||
|
|||||||
@ -25,10 +25,6 @@ import { PropertiesPopover } from "../PropertiesPopover";
|
|||||||
import { QuickSearch } from "../QuickSearch";
|
import { QuickSearch } from "../QuickSearch";
|
||||||
import { ScrollableList } from "../ScrollableList";
|
import { ScrollableList } from "../ScrollableList";
|
||||||
import DropdownMenuGroup from "../dropdownMenu/DropdownMenuGroup";
|
import DropdownMenuGroup from "../dropdownMenu/DropdownMenuGroup";
|
||||||
import DropdownMenuItem, {
|
|
||||||
DropDownMenuItemBadgeType,
|
|
||||||
DropDownMenuItemBadge,
|
|
||||||
} from "../dropdownMenu/DropdownMenuItem";
|
|
||||||
import {
|
import {
|
||||||
FontFamilyCodeIcon,
|
FontFamilyCodeIcon,
|
||||||
FontFamilyHeadingIcon,
|
FontFamilyHeadingIcon,
|
||||||
@ -36,8 +32,15 @@ import {
|
|||||||
FreedrawIcon,
|
FreedrawIcon,
|
||||||
} from "../icons";
|
} from "../icons";
|
||||||
|
|
||||||
|
import { Ellipsify } from "../Ellipsify";
|
||||||
|
|
||||||
import { fontPickerKeyHandler } from "./keyboardNavHandlers";
|
import { fontPickerKeyHandler } from "./keyboardNavHandlers";
|
||||||
|
|
||||||
|
import {
|
||||||
|
FontPickerListItem,
|
||||||
|
FontPickerListItemBadgeType,
|
||||||
|
} from "./FontPickerListItem";
|
||||||
|
|
||||||
import type { JSX } from "react";
|
import type { JSX } from "react";
|
||||||
|
|
||||||
export interface FontDescriptor {
|
export interface FontDescriptor {
|
||||||
@ -46,7 +49,7 @@ export interface FontDescriptor {
|
|||||||
text: string;
|
text: string;
|
||||||
deprecated?: true;
|
deprecated?: true;
|
||||||
badge?: {
|
badge?: {
|
||||||
type: ValueOf<typeof DropDownMenuItemBadgeType>;
|
type: ValueOf<typeof FontPickerListItemBadgeType>;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -112,7 +115,7 @@ export const FontPickerList = React.memo(
|
|||||||
Object.assign(fontDescriptor, {
|
Object.assign(fontDescriptor, {
|
||||||
deprecated: metadata.deprecated,
|
deprecated: metadata.deprecated,
|
||||||
badge: {
|
badge: {
|
||||||
type: DropDownMenuItemBadgeType.RED,
|
type: FontPickerListItemBadgeType.RED,
|
||||||
placeholder: t("fontList.badge.old"),
|
placeholder: t("fontList.badge.old"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -227,7 +230,7 @@ export const FontPickerList = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const renderFont = (font: FontDescriptor, index: number) => (
|
const renderFont = (font: FontDescriptor, index: number) => (
|
||||||
<DropdownMenuItem
|
<FontPickerListItem
|
||||||
key={font.value}
|
key={font.value}
|
||||||
icon={font.icon}
|
icon={font.icon}
|
||||||
value={font.value}
|
value={font.value}
|
||||||
@ -239,8 +242,8 @@ export const FontPickerList = React.memo(
|
|||||||
selected={font.value === selectedFontFamily}
|
selected={font.value === selectedFontFamily}
|
||||||
// allow to tab between search and selected font
|
// allow to tab between search and selected font
|
||||||
tabIndex={font.value === selectedFontFamily ? 0 : -1}
|
tabIndex={font.value === selectedFontFamily ? 0 : -1}
|
||||||
onClick={(e) => {
|
onSelect={() => {
|
||||||
onSelect(Number(e.currentTarget.value));
|
onSelect(font.value);
|
||||||
}}
|
}}
|
||||||
onMouseMove={() => {
|
onMouseMove={() => {
|
||||||
if (hoveredFont?.value !== font.value) {
|
if (hoveredFont?.value !== font.value) {
|
||||||
@ -248,13 +251,13 @@ export const FontPickerList = React.memo(
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{font.text}
|
<Ellipsify>{font.text}</Ellipsify>
|
||||||
{font.badge && (
|
{font.badge && (
|
||||||
<DropDownMenuItemBadge type={font.badge.type}>
|
<FontPickerListItem.Badge type={font.badge.type}>
|
||||||
{font.badge.placeholder}
|
{font.badge.placeholder}
|
||||||
</DropDownMenuItemBadge>
|
</FontPickerListItem.Badge>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuItem>
|
</FontPickerListItem>
|
||||||
);
|
);
|
||||||
|
|
||||||
const groups = [];
|
const groups = [];
|
||||||
|
|||||||
151
packages/excalidraw/components/FontPicker/FontPickerListItem.tsx
Normal file
151
packages/excalidraw/components/FontPicker/FontPickerListItem.tsx
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { THEME } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { ValueOf } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import { Button } from "../Button";
|
||||||
|
|
||||||
|
import { useExcalidrawAppState } from "../App";
|
||||||
|
|
||||||
|
import { useDevice } from "../App";
|
||||||
|
|
||||||
|
import { getDropdownMenuItemClassName } from "../dropdownMenu/common";
|
||||||
|
|
||||||
|
import type { JSX } from "react";
|
||||||
|
|
||||||
|
const MenuItemContent = ({
|
||||||
|
textStyle,
|
||||||
|
icon,
|
||||||
|
shortcut,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
shortcut?: string;
|
||||||
|
textStyle?: React.CSSProperties;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const device = useDevice();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{icon && <div className="dropdown-menu-item__icon">{icon}</div>}
|
||||||
|
<div style={textStyle} className="dropdown-menu-item__text">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{shortcut && !device.editor.isMobile && (
|
||||||
|
<div className="dropdown-menu-item__shortcut">{shortcut}</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FontPickerListItem = ({
|
||||||
|
icon,
|
||||||
|
value,
|
||||||
|
order,
|
||||||
|
children,
|
||||||
|
shortcut,
|
||||||
|
className,
|
||||||
|
hovered,
|
||||||
|
selected,
|
||||||
|
textStyle,
|
||||||
|
onSelect,
|
||||||
|
onClick,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
icon?: JSX.Element;
|
||||||
|
value?: string | number | undefined;
|
||||||
|
order?: number;
|
||||||
|
onSelect: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
shortcut?: string;
|
||||||
|
hovered?: boolean;
|
||||||
|
selected?: boolean;
|
||||||
|
textStyle?: React.CSSProperties;
|
||||||
|
className?: string;
|
||||||
|
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "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]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="radix-menu-item">
|
||||||
|
<Button
|
||||||
|
{...rest}
|
||||||
|
ref={ref}
|
||||||
|
onSelect={onSelect}
|
||||||
|
className={getDropdownMenuItemClassName(className, selected, hovered)}
|
||||||
|
title={rest.title ?? rest["aria-label"]}
|
||||||
|
>
|
||||||
|
<MenuItemContent textStyle={textStyle} icon={icon} shortcut={shortcut}>
|
||||||
|
{children}
|
||||||
|
</MenuItemContent>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
FontPickerListItem.displayName = "FontPickerListItem";
|
||||||
|
|
||||||
|
export const FontPickerListItemBadgeType = {
|
||||||
|
GREEN: "green",
|
||||||
|
RED: "red",
|
||||||
|
BLUE: "blue",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const FontPickerListItemBadge = ({
|
||||||
|
type = FontPickerListItemBadgeType.BLUE,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
type?: ValueOf<typeof FontPickerListItemBadgeType>;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const { theme } = useExcalidrawAppState();
|
||||||
|
const style = {
|
||||||
|
display: "inline-flex",
|
||||||
|
marginLeft: "auto",
|
||||||
|
padding: "2px 4px",
|
||||||
|
borderRadius: 6,
|
||||||
|
fontSize: 9,
|
||||||
|
fontFamily: "Cascadia, monospace",
|
||||||
|
border: theme === THEME.LIGHT ? "1.5px solid white" : "none",
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case FontPickerListItemBadgeType.GREEN:
|
||||||
|
Object.assign(style, {
|
||||||
|
backgroundColor: "var(--background-color-badge)",
|
||||||
|
color: "var(--color-badge)",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case FontPickerListItemBadgeType.RED:
|
||||||
|
Object.assign(style, {
|
||||||
|
backgroundColor: "pink",
|
||||||
|
color: "darkred",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case FontPickerListItemBadgeType.BLUE:
|
||||||
|
default:
|
||||||
|
Object.assign(style, {
|
||||||
|
background: "var(--color-promo)",
|
||||||
|
color: "var(--color-surface-lowest)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="DropDownMenuItemBadge" style={style}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
FontPickerListItemBadge.displayName = "DropdownMenuItemBadge";
|
||||||
|
|
||||||
|
FontPickerListItem.Badge = FontPickerListItemBadge;
|
||||||
@ -238,7 +238,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
shortcuts={[getShortcutKey("Enter"), getShortcutKey("Escape")]}
|
shortcuts={[getShortcutKey("Enter"), getShortcutKey("Escape")]}
|
||||||
isOr={true}
|
isOr={true}
|
||||||
/>
|
/>
|
||||||
<Shortcut label={t("toolBar.lock")} shortcuts={[KEYS.Q]} />
|
<Shortcut
|
||||||
|
label={t("toolBar.lock")}
|
||||||
|
shortcuts={[getShortcutFromShortcutName("toolLock")]}
|
||||||
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("helpDialog.preventBinding")}
|
label={t("helpDialog.preventBinding")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd")]}
|
||||||
|
|||||||
@ -194,6 +194,7 @@ export const LibraryDropdownMenuButton: React.FC<{
|
|||||||
<DropdownMenu open={isLibraryMenuOpen}>
|
<DropdownMenu open={isLibraryMenuOpen}>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
onToggle={() => setIsLibraryMenuOpen(!isLibraryMenuOpen)}
|
onToggle={() => setIsLibraryMenuOpen(!isLibraryMenuOpen)}
|
||||||
|
aria-label="Library menu"
|
||||||
>
|
>
|
||||||
{DotsIcon}
|
{DotsIcon}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
@ -201,6 +202,7 @@ export const LibraryDropdownMenuButton: React.FC<{
|
|||||||
onClickOutside={() => setIsLibraryMenuOpen(false)}
|
onClickOutside={() => setIsLibraryMenuOpen(false)}
|
||||||
onSelect={() => setIsLibraryMenuOpen(false)}
|
onSelect={() => setIsLibraryMenuOpen(false)}
|
||||||
className="library-menu"
|
className="library-menu"
|
||||||
|
align="end"
|
||||||
>
|
>
|
||||||
{!itemsSelected && (
|
{!itemsSelected && (
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
|
|||||||
@ -26,9 +26,9 @@ export const TTDDialogTrigger = ({
|
|||||||
setAppState({ openDialog: { name: "ttd", tab: "text-to-diagram" } });
|
setAppState({ openDialog: { name: "ttd", tab: "text-to-diagram" } });
|
||||||
}}
|
}}
|
||||||
icon={icon ?? brainIcon}
|
icon={icon ?? brainIcon}
|
||||||
|
badge={<DropdownMenu.Item.Badge>AI</DropdownMenu.Item.Badge>}
|
||||||
>
|
>
|
||||||
{children ?? t("labels.textToDiagram")}
|
{children ?? t("labels.textToDiagram")}
|
||||||
<DropdownMenu.Item.Badge>AI</DropdownMenu.Item.Badge>
|
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</TTDDialogTriggerTunnel.In>
|
</TTDDialogTriggerTunnel.In>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,20 +1,45 @@
|
|||||||
@import "../../css/variables.module.scss";
|
@import "../../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
|
[data-dropdown-menu-trigger] + [data-radix-popper-content-wrapper] {
|
||||||
|
z-index: 2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
position: absolute;
|
max-width: 16rem;
|
||||||
top: 100%;
|
margin-top: 0.25rem;
|
||||||
margin-top: 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radix-menu-item {
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-submenu {
|
||||||
|
margin-left: -0.75rem;
|
||||||
|
min-width: 16rem;
|
||||||
|
max-width: 20rem;
|
||||||
|
}
|
||||||
|
|
||||||
&--mobile {
|
&--mobile {
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
row-gap: 0.75rem;
|
|
||||||
|
|
||||||
.dropdown-menu-container {
|
.dropdown-menu-container {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
padding: 8px 8px;
|
padding: 8px 8px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
// background-color: var(--island-bg-color);
|
background-color: var(--island-bg-color);
|
||||||
box-shadow: var(--shadow-island);
|
box-shadow: var(--shadow-island);
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: var(--border-radius-lg);
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -30,13 +55,14 @@
|
|||||||
|
|
||||||
.dropdown-menu-container {
|
.dropdown-menu-container {
|
||||||
background-color: var(--island-bg-color);
|
background-color: var(--island-bg-color);
|
||||||
max-height: calc(100vh - 150px);
|
max-height: var(--radix-popper-available-height);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
--gap: 2;
|
--gap: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu-item-base {
|
.dropdown-menu-item-base {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
padding: 0 0.625rem;
|
||||||
column-gap: 0.625rem;
|
column-gap: 0.625rem;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: var(--color-on-surface);
|
color: var(--color-on-surface);
|
||||||
@ -44,6 +70,7 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.manual-hover {
|
&.manual-hover {
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
import DropdownMenuContent from "./DropdownMenuContent";
|
import DropdownMenuContent from "./DropdownMenuContent";
|
||||||
import DropdownMenuGroup from "./DropdownMenuGroup";
|
import DropdownMenuGroup from "./DropdownMenuGroup";
|
||||||
import DropdownMenuItem from "./DropdownMenuItem";
|
import DropdownMenuItem from "./DropdownMenuItem";
|
||||||
@ -23,11 +25,12 @@ const DropdownMenu = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const MenuTriggerComp = getMenuTriggerComponent(children);
|
const MenuTriggerComp = getMenuTriggerComponent(children);
|
||||||
const MenuContentComp = getMenuContentComponent(children);
|
const MenuContentComp = getMenuContentComponent(children);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DropdownMenuPrimitive.Root open={open} modal={false}>
|
||||||
{MenuTriggerComp}
|
{MenuTriggerComp}
|
||||||
{open && MenuContentComp}
|
{MenuContentComp}
|
||||||
</>
|
</DropdownMenuPrimitive.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import React, { useEffect, useRef } from "react";
|
|||||||
|
|
||||||
import { EVENT, KEYS } from "@excalidraw/common";
|
import { EVENT, KEYS } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
import { useOutsideClick } from "../../hooks/useOutsideClick";
|
import { useOutsideClick } from "../../hooks/useOutsideClick";
|
||||||
import { useStable } from "../../hooks/useStable";
|
import { useStable } from "../../hooks/useStable";
|
||||||
import { useDevice } from "../App";
|
import { useDevice } from "../App";
|
||||||
@ -17,6 +19,9 @@ const MenuContent = ({
|
|||||||
className = "",
|
className = "",
|
||||||
onSelect,
|
onSelect,
|
||||||
style,
|
style,
|
||||||
|
sideOffset = 4,
|
||||||
|
align = "start",
|
||||||
|
collisionPadding,
|
||||||
}: {
|
}: {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
onClickOutside?: () => void;
|
onClickOutside?: () => void;
|
||||||
@ -26,6 +31,11 @@ const MenuContent = ({
|
|||||||
*/
|
*/
|
||||||
onSelect?: (event: Event) => void;
|
onSelect?: (event: Event) => void;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
sideOffset?: number;
|
||||||
|
align?: "start" | "center" | "end";
|
||||||
|
collisionPadding?:
|
||||||
|
| number
|
||||||
|
| Partial<Record<"top" | "right" | "bottom" | "left", number>>;
|
||||||
}) => {
|
}) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
@ -62,11 +72,15 @@ const MenuContent = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuContentPropsContext.Provider value={{ onSelect }}>
|
<DropdownMenuContentPropsContext.Provider value={{ onSelect }}>
|
||||||
<div
|
<DropdownMenuPrimitive.Content
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
className={classNames}
|
className={classNames}
|
||||||
style={style}
|
style={style}
|
||||||
data-testid="dropdown-menu"
|
data-testid="dropdown-menu"
|
||||||
|
side="bottom"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
align={align}
|
||||||
|
collisionPadding={collisionPadding}
|
||||||
>
|
>
|
||||||
{/* the zIndex ensures this menu has higher stacking order,
|
{/* the zIndex ensures this menu has higher stacking order,
|
||||||
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
||||||
@ -81,7 +95,7 @@ const MenuContent = ({
|
|||||||
{children}
|
{children}
|
||||||
</Island>
|
</Island>
|
||||||
)}
|
)}
|
||||||
</div>
|
</DropdownMenuPrimitive.Content>
|
||||||
</DropdownMenuContentPropsContext.Provider>
|
</DropdownMenuContentPropsContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,12 +1,17 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
|
|
||||||
import { THEME } from "@excalidraw/common";
|
import { THEME } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
import type { ValueOf } from "@excalidraw/common/utility-types";
|
import type { ValueOf } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import { Button } from "../Button";
|
||||||
|
|
||||||
import { useExcalidrawAppState } from "../App";
|
import { useExcalidrawAppState } from "../App";
|
||||||
|
|
||||||
import MenuItemContent from "./DropdownMenuItemContent";
|
import MenuItemContent from "./DropdownMenuItemContent";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getDropdownMenuItemClassName,
|
getDropdownMenuItemClassName,
|
||||||
useHandleDropdownMenuItemClick,
|
useHandleDropdownMenuItemClick,
|
||||||
@ -17,55 +22,45 @@ import type { JSX } from "react";
|
|||||||
const DropdownMenuItem = ({
|
const DropdownMenuItem = ({
|
||||||
icon,
|
icon,
|
||||||
value,
|
value,
|
||||||
|
badge,
|
||||||
order,
|
order,
|
||||||
children,
|
children,
|
||||||
shortcut,
|
shortcut,
|
||||||
className,
|
className,
|
||||||
hovered,
|
|
||||||
selected,
|
selected,
|
||||||
textStyle,
|
|
||||||
onSelect,
|
onSelect,
|
||||||
onClick,
|
onClick,
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
|
badge?: React.ReactNode;
|
||||||
value?: string | number | undefined;
|
value?: string | number | undefined;
|
||||||
order?: number;
|
order?: number;
|
||||||
onSelect?: (event: Event) => void;
|
onSelect?: (event: Event) => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
hovered?: boolean;
|
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
textStyle?: React.CSSProperties;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
||||||
const handleClick = useHandleDropdownMenuItemClick(onClick, onSelect);
|
const handleClick = useHandleDropdownMenuItemClick(onClick, onSelect);
|
||||||
const ref = useRef<HTMLButtonElement>(null);
|
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<DropdownMenuPrimitive.Item className="radix-menu-item">
|
||||||
{...rest}
|
<Button
|
||||||
ref={ref}
|
{...rest}
|
||||||
value={value}
|
ref={ref}
|
||||||
onClick={handleClick}
|
onSelect={handleClick}
|
||||||
className={getDropdownMenuItemClassName(className, selected, hovered)}
|
className={getDropdownMenuItemClassName(className)}
|
||||||
title={rest.title ?? rest["aria-label"]}
|
title={rest.title ?? rest["aria-label"]}
|
||||||
>
|
>
|
||||||
<MenuItemContent textStyle={textStyle} icon={icon} shortcut={shortcut}>
|
<MenuItemContent icon={icon} shortcut={shortcut} badge={badge}>
|
||||||
{children}
|
{children}
|
||||||
</MenuItemContent>
|
</MenuItemContent>
|
||||||
</button>
|
</Button>
|
||||||
|
</DropdownMenuPrimitive.Item>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
DropdownMenuItem.displayName = "DropdownMenuItem";
|
DropdownMenuItem.displayName = "DropdownMenuItem";
|
||||||
|
|||||||
@ -2,25 +2,24 @@ import { useDevice } from "../App";
|
|||||||
|
|
||||||
import { Ellipsify } from "../Ellipsify";
|
import { Ellipsify } from "../Ellipsify";
|
||||||
|
|
||||||
import type { JSX } from "react";
|
|
||||||
|
|
||||||
const MenuItemContent = ({
|
const MenuItemContent = ({
|
||||||
textStyle,
|
|
||||||
icon,
|
icon,
|
||||||
|
badge,
|
||||||
shortcut,
|
shortcut,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
icon?: JSX.Element;
|
icon?: React.ReactNode;
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
textStyle?: React.CSSProperties;
|
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
badge?: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{icon && <div className="dropdown-menu-item__icon">{icon}</div>}
|
{icon && <div className="dropdown-menu-item__icon">{icon}</div>}
|
||||||
<div style={textStyle} className="dropdown-menu-item__text">
|
<div className="dropdown-menu-item__text">
|
||||||
<Ellipsify>{children}</Ellipsify>
|
<Ellipsify>{children}</Ellipsify>
|
||||||
|
{badge}
|
||||||
</div>
|
</div>
|
||||||
{shortcut && !device.editor.isMobile && (
|
{shortcut && !device.editor.isMobile && (
|
||||||
<div className="dropdown-menu-item__shortcut">{shortcut}</div>
|
<div className="dropdown-menu-item__shortcut">{shortcut}</div>
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getSubMenuContentComponent,
|
||||||
|
getSubMenuTriggerComponent,
|
||||||
|
} from "./dropdownMenuUtils";
|
||||||
|
import DropdownMenuSubTrigger from "./DropdownMenuSubTrigger";
|
||||||
|
import DropdownMenuSubContent from "./DropdownMenuSubContent";
|
||||||
|
import DropdownMenuSubItem from "./DropdownMenuSubItem";
|
||||||
|
|
||||||
|
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.Item = DropdownMenuSubItem;
|
||||||
|
|
||||||
|
export default DropdownMenuSub;
|
||||||
|
DropdownMenuSub.displayName = "DropdownMenuSub";
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import { useDevice } from "../App";
|
||||||
|
import Stack from "../Stack";
|
||||||
|
import { Island } from "../Island";
|
||||||
|
|
||||||
|
const DropdownMenuSubContent = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
const device = useDevice();
|
||||||
|
|
||||||
|
const classNames = clsx(`dropdown-menu dropdown-submenu ${className}`, {
|
||||||
|
"dropdown-menu--mobile": device.editor.isMobile,
|
||||||
|
}).trim();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
className={classNames}
|
||||||
|
sideOffset={8}
|
||||||
|
alignOffset={-4}
|
||||||
|
>
|
||||||
|
{device.editor.isMobile ? (
|
||||||
|
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
||||||
|
) : (
|
||||||
|
<Island
|
||||||
|
className="dropdown-menu-container"
|
||||||
|
padding={1}
|
||||||
|
style={{ zIndex: 1 }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Island>
|
||||||
|
)}
|
||||||
|
</DropdownMenuPrimitive.SubContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropdownMenuSubContent;
|
||||||
|
DropdownMenuSubContent.displayName = "DropdownMenuSubContent";
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
|
import { Button } from "../Button";
|
||||||
|
|
||||||
|
import MenuItemContent from "./DropdownMenuItemContent";
|
||||||
|
import {
|
||||||
|
getDropdownMenuItemClassName,
|
||||||
|
useHandleDropdownMenuItemClick,
|
||||||
|
} from "./common";
|
||||||
|
|
||||||
|
const DropdownMenuSubItem = ({
|
||||||
|
icon,
|
||||||
|
onSelect,
|
||||||
|
children,
|
||||||
|
shortcut,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
onSelect: (event: Event) => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
shortcut?: string;
|
||||||
|
className?: string;
|
||||||
|
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
||||||
|
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Item className="radix-menu-item">
|
||||||
|
<Button
|
||||||
|
{...rest}
|
||||||
|
onSelect={handleClick}
|
||||||
|
type="button"
|
||||||
|
className={getDropdownMenuItemClassName(className)}
|
||||||
|
title={rest.title ?? rest["aria-label"]}
|
||||||
|
>
|
||||||
|
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||||
|
{children}
|
||||||
|
</MenuItemContent>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuPrimitive.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropdownMenuSubItem;
|
||||||
|
DropdownMenuSubItem.displayName = "DropdownMenuSubItem";
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { ChevronRight } from "../icons";
|
||||||
|
|
||||||
|
import MenuItemContent from "./DropdownMenuItemContent";
|
||||||
|
import { getDropdownMenuItemClassName } from "./common";
|
||||||
|
|
||||||
|
import type { JSX } from "react";
|
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = ({
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
icon?: JSX.Element;
|
||||||
|
className?: string;
|
||||||
|
} & React.HTMLAttributes<HTMLDivElement>) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger className="radix-menu-item dropdown-menu__submenu-trigger">
|
||||||
|
<div
|
||||||
|
{...rest}
|
||||||
|
className={getDropdownMenuItemClassName(className)}
|
||||||
|
title={rest.title ?? rest["aria-label"]}
|
||||||
|
>
|
||||||
|
<MenuItemContent icon={icon}>{children}</MenuItemContent>
|
||||||
|
<div className="dropdown-menu__submenu-trigger-icon">
|
||||||
|
{ChevronRight}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropdownMenuSubTrigger;
|
||||||
|
DropdownMenuSubTrigger.displayName = "DropdownMenuSubTrigger";
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
import { useDevice } from "../App";
|
import { useDevice } from "../App";
|
||||||
|
|
||||||
const MenuTrigger = ({
|
const MenuTrigger = ({
|
||||||
@ -23,7 +25,8 @@ const MenuTrigger = ({
|
|||||||
},
|
},
|
||||||
).trim();
|
).trim();
|
||||||
return (
|
return (
|
||||||
<button
|
<DropdownMenuPrimitive.Trigger
|
||||||
|
data-dropdown-menu-trigger
|
||||||
data-prevent-outside-click
|
data-prevent-outside-click
|
||||||
className={classNames}
|
className={classNames}
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
@ -33,7 +36,7 @@ const MenuTrigger = ({
|
|||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</DropdownMenuPrimitive.Trigger>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export const getMenuTriggerComponent = (children: React.ReactNode) => {
|
const getMenuComponent = (component: string) => (children: React.ReactNode) => {
|
||||||
const comp = React.Children.toArray(children).find(
|
const comp = React.Children.toArray(children).find(
|
||||||
(child) =>
|
(child) =>
|
||||||
React.isValidElement(child) &&
|
React.isValidElement(child) &&
|
||||||
@ -8,7 +8,7 @@ export const getMenuTriggerComponent = (children: React.ReactNode) => {
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
child?.type.displayName &&
|
child?.type.displayName &&
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
child.type.displayName === "DropdownMenuTrigger",
|
child.type.displayName === component,
|
||||||
);
|
);
|
||||||
if (!comp) {
|
if (!comp) {
|
||||||
return null;
|
return null;
|
||||||
@ -17,19 +17,11 @@ export const getMenuTriggerComponent = (children: React.ReactNode) => {
|
|||||||
return comp;
|
return comp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMenuContentComponent = (children: React.ReactNode) => {
|
export const getMenuTriggerComponent = getMenuComponent("DropdownMenuTrigger");
|
||||||
const comp = React.Children.toArray(children).find(
|
export const getMenuContentComponent = getMenuComponent("DropdownMenuContent");
|
||||||
(child) =>
|
export const getSubMenuTriggerComponent = getMenuComponent(
|
||||||
React.isValidElement(child) &&
|
"DropdownMenuSubTrigger",
|
||||||
typeof child.type !== "string" &&
|
);
|
||||||
//@ts-ignore
|
export const getSubMenuContentComponent = getMenuComponent(
|
||||||
child?.type.displayName &&
|
"DropdownMenuSubContent",
|
||||||
//@ts-ignore
|
);
|
||||||
child.type.displayName === "DropdownMenuContent",
|
|
||||||
);
|
|
||||||
if (!comp) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
//@ts-ignore
|
|
||||||
return comp;
|
|
||||||
};
|
|
||||||
|
|||||||
@ -72,6 +72,15 @@ const modifiedTablerIconProps: Opts = {
|
|||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
//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,
|
||||||
|
);
|
||||||
|
|
||||||
// tabler-icons: present
|
// tabler-icons: present
|
||||||
export const PlusPromoIcon = createIcon(
|
export const PlusPromoIcon = createIcon(
|
||||||
<g strokeWidth="1.5">
|
<g strokeWidth="1.5">
|
||||||
@ -2269,3 +2278,21 @@ export const elementLinkIcon = createIcon(
|
|||||||
</g>,
|
</g>,
|
||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const settingsIcon = createIcon(
|
||||||
|
<g strokeWidth={1.25}>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M14 6m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||||
|
<path d="M4 6l8 0" />
|
||||||
|
<path d="M16 6l4 0" />
|
||||||
|
<path d="M8 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||||
|
<path d="M4 12l2 0" />
|
||||||
|
<path d="M10 12l10 0" />
|
||||||
|
<path d="M17 18m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||||
|
<path d="M4 18l11 0" />
|
||||||
|
<path d="M19 18l1 0" />
|
||||||
|
</g>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const emptyIcon = <div style={{ width: "1rem", height: "1rem" }} />;
|
||||||
|
|||||||
@ -9,8 +9,11 @@ import {
|
|||||||
actionLoadScene,
|
actionLoadScene,
|
||||||
actionSaveToActiveFile,
|
actionSaveToActiveFile,
|
||||||
actionShortcuts,
|
actionShortcuts,
|
||||||
|
actionToggleGridMode,
|
||||||
|
actionToggleObjectsSnapMode,
|
||||||
actionToggleSearchMenu,
|
actionToggleSearchMenu,
|
||||||
actionToggleTheme,
|
actionToggleTheme,
|
||||||
|
actionToggleZenMode,
|
||||||
} from "../../actions";
|
} from "../../actions";
|
||||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||||
import { trackEvent } from "../../analytics";
|
import { trackEvent } from "../../analytics";
|
||||||
@ -23,13 +26,23 @@ import {
|
|||||||
useExcalidrawActionManager,
|
useExcalidrawActionManager,
|
||||||
useExcalidrawElements,
|
useExcalidrawElements,
|
||||||
useAppProps,
|
useAppProps,
|
||||||
|
useApp,
|
||||||
} from "../App";
|
} from "../App";
|
||||||
import { openConfirmModal } from "../OverwriteConfirm/OverwriteConfirmState";
|
import { openConfirmModal } from "../OverwriteConfirm/OverwriteConfirmState";
|
||||||
import Trans from "../Trans";
|
import Trans from "../Trans";
|
||||||
import DropdownMenuItem from "../dropdownMenu/DropdownMenuItem";
|
import DropdownMenuItem from "../dropdownMenu/DropdownMenuItem";
|
||||||
import DropdownMenuItemContentRadio from "../dropdownMenu/DropdownMenuItemContentRadio";
|
import DropdownMenuItemContentRadio from "../dropdownMenu/DropdownMenuItemContentRadio";
|
||||||
import DropdownMenuItemLink from "../dropdownMenu/DropdownMenuItemLink";
|
import DropdownMenuItemLink from "../dropdownMenu/DropdownMenuItemLink";
|
||||||
import { GithubIcon, DiscordIcon, XBrandIcon } from "../icons";
|
import DropdownMenuSub from "../dropdownMenu/DropdownMenuSub";
|
||||||
|
import { actionToggleViewMode } from "../../actions/actionToggleViewMode";
|
||||||
|
import {
|
||||||
|
GithubIcon,
|
||||||
|
DiscordIcon,
|
||||||
|
XBrandIcon,
|
||||||
|
settingsIcon,
|
||||||
|
checkIcon,
|
||||||
|
emptyIcon,
|
||||||
|
} from "../icons";
|
||||||
import {
|
import {
|
||||||
boltIcon,
|
boltIcon,
|
||||||
DeviceDesktopIcon,
|
DeviceDesktopIcon,
|
||||||
@ -313,7 +326,10 @@ export const ChangeCanvasBackground = () => {
|
|||||||
>
|
>
|
||||||
{t("labels.canvasBackground")}
|
{t("labels.canvasBackground")}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ padding: "0 0.625rem" }}>
|
<div
|
||||||
|
style={{ padding: "0 0.625rem" }}
|
||||||
|
id="canvas-bg-color-picker-container"
|
||||||
|
>
|
||||||
{actionManager.renderAction("changeViewBackgroundColor")}
|
{actionManager.renderAction("changeViewBackgroundColor")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -393,3 +409,73 @@ export const LiveCollaborationTrigger = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
LiveCollaborationTrigger.displayName = "LiveCollaborationTrigger";
|
LiveCollaborationTrigger.displayName = "LiveCollaborationTrigger";
|
||||||
|
|
||||||
|
export const Preferences = ({ children }: { children?: React.ReactNode }) => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
const appState = useUIAppState();
|
||||||
|
const app = useApp();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSub.Trigger icon={settingsIcon}>
|
||||||
|
{t("labels.preferences")}
|
||||||
|
</DropdownMenuSub.Trigger>
|
||||||
|
<DropdownMenuSub.Content className="excalidraw-main-menu-preferences-submenu">
|
||||||
|
<DropdownMenuSub.Item
|
||||||
|
icon={appState.activeTool.locked ? checkIcon : emptyIcon}
|
||||||
|
shortcut={getShortcutFromShortcutName("toolLock")}
|
||||||
|
onSelect={(event) => {
|
||||||
|
app.toggleLock();
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("labels.preferences_toolLock")}
|
||||||
|
</DropdownMenuSub.Item>
|
||||||
|
<DropdownMenuSub.Item
|
||||||
|
icon={appState.objectsSnapModeEnabled ? checkIcon : emptyIcon}
|
||||||
|
shortcut={getShortcutFromShortcutName("objectsSnapMode")}
|
||||||
|
onSelect={(event) => {
|
||||||
|
actionManager.executeAction(actionToggleObjectsSnapMode);
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("buttons.objectsSnapMode")}
|
||||||
|
</DropdownMenuSub.Item>
|
||||||
|
<DropdownMenuSub.Item
|
||||||
|
icon={appState.gridModeEnabled ? checkIcon : emptyIcon}
|
||||||
|
shortcut={getShortcutFromShortcutName("gridMode")}
|
||||||
|
onSelect={(event) => {
|
||||||
|
actionManager.executeAction(actionToggleGridMode);
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("labels.toggleGrid")}
|
||||||
|
</DropdownMenuSub.Item>
|
||||||
|
<DropdownMenuSub.Item
|
||||||
|
icon={appState.zenModeEnabled ? checkIcon : emptyIcon}
|
||||||
|
shortcut={getShortcutFromShortcutName("zenMode")}
|
||||||
|
onSelect={(event) => {
|
||||||
|
actionManager.executeAction(actionToggleZenMode);
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("buttons.zenMode")}
|
||||||
|
</DropdownMenuSub.Item>
|
||||||
|
<DropdownMenuSub.Item
|
||||||
|
icon={appState.viewModeEnabled ? checkIcon : emptyIcon}
|
||||||
|
shortcut={getShortcutFromShortcutName("viewMode")}
|
||||||
|
onSelect={(event) => {
|
||||||
|
actionManager.executeAction(actionToggleViewMode);
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("labels.viewMode")}
|
||||||
|
</DropdownMenuSub.Item>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuSub.Content>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Preferences.displayName = "Preferences";
|
||||||
|
|||||||
@ -2,8 +2,12 @@ import React from "react";
|
|||||||
|
|
||||||
import { composeEventHandlers } from "@excalidraw/common";
|
import { composeEventHandlers } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import * as Portal from "@radix-ui/react-portal";
|
||||||
|
|
||||||
import { useTunnels } from "../../context/tunnels";
|
import { useTunnels } from "../../context/tunnels";
|
||||||
import { useUIAppState } from "../../context/ui-appState";
|
import { useUIAppState } from "../../context/ui-appState";
|
||||||
|
import DropdownMenuSub from "../dropdownMenu/DropdownMenuSub";
|
||||||
|
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import { useDevice, useExcalidrawSetAppState } from "../App";
|
import { useDevice, useExcalidrawSetAppState } from "../App";
|
||||||
import { UserList } from "../UserList";
|
import { UserList } from "../UserList";
|
||||||
@ -36,6 +40,17 @@ const MainMenu = Object.assign(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MainMenuTunnel.In>
|
<MainMenuTunnel.In>
|
||||||
|
{appState.openMenu === "canvas" && device.editor.isMobile && (
|
||||||
|
<Portal.Root
|
||||||
|
style={{
|
||||||
|
backgroundColor: "rgba(18, 18, 18, 0.2)",
|
||||||
|
position: "fixed",
|
||||||
|
inset: "0px",
|
||||||
|
// zIndex: "var(--zIndex-layerUI)",
|
||||||
|
}}
|
||||||
|
onClick={() => setAppState({ openMenu: null })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<DropdownMenu open={appState.openMenu === "canvas"}>
|
<DropdownMenu open={appState.openMenu === "canvas"}>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
onToggle={() => {
|
onToggle={() => {
|
||||||
@ -44,15 +59,27 @@ const MainMenu = Object.assign(
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
data-testid="main-menu-trigger"
|
data-testid="main-menu-trigger"
|
||||||
|
aria-label="Main menu"
|
||||||
className="main-menu-trigger"
|
className="main-menu-trigger"
|
||||||
>
|
>
|
||||||
{HamburgerMenuIcon}
|
{HamburgerMenuIcon}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
|
sideOffset={device.editor.isMobile ? 20 : undefined}
|
||||||
|
className="main-menu-content"
|
||||||
onClickOutside={onClickOutside}
|
onClickOutside={onClickOutside}
|
||||||
onSelect={composeEventHandlers(onSelect, () => {
|
onSelect={composeEventHandlers(onSelect, () => {
|
||||||
setAppState({ openMenu: null });
|
setAppState({ openMenu: null });
|
||||||
})}
|
})}
|
||||||
|
collisionPadding={
|
||||||
|
// accounting for
|
||||||
|
// - editor footer on desktop
|
||||||
|
// - toolbar on mobile
|
||||||
|
// we probably don't want the menu to overlay these elements
|
||||||
|
!device.editor.isMobile
|
||||||
|
? { bottom: 90, top: 10 }
|
||||||
|
: { top: 90, bottom: 10 }
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{device.editor.isMobile && appState.collaborators.size > 0 && (
|
{device.editor.isMobile && appState.collaborators.size > 0 && (
|
||||||
@ -78,6 +105,7 @@ const MainMenu = Object.assign(
|
|||||||
ItemCustom: DropdownMenu.ItemCustom,
|
ItemCustom: DropdownMenu.ItemCustom,
|
||||||
Group: DropdownMenu.Group,
|
Group: DropdownMenu.Group,
|
||||||
Separator: DropdownMenu.Separator,
|
Separator: DropdownMenu.Separator,
|
||||||
|
Sub: DropdownMenuSub,
|
||||||
DefaultItems,
|
DefaultItems,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -144,6 +144,7 @@
|
|||||||
--color-logo-icon: var(--color-primary);
|
--color-logo-icon: var(--color-primary);
|
||||||
--color-logo-text: #190064;
|
--color-logo-text: #190064;
|
||||||
|
|
||||||
|
--border-radius-sm: 0.25rem;
|
||||||
--border-radius-md: 0.375rem;
|
--border-radius-md: 0.375rem;
|
||||||
--border-radius-lg: 0.5rem;
|
--border-radius-lg: 0.5rem;
|
||||||
|
|
||||||
|
|||||||
@ -171,7 +171,9 @@
|
|||||||
"linkToElement": "Link to object",
|
"linkToElement": "Link to object",
|
||||||
"wrapSelectionInFrame": "Wrap selection in frame",
|
"wrapSelectionInFrame": "Wrap selection in frame",
|
||||||
"tab": "Tab",
|
"tab": "Tab",
|
||||||
"shapeSwitch": "Switch shape"
|
"shapeSwitch": "Switch shape",
|
||||||
|
"preferences": "Preferences",
|
||||||
|
"preferences_toolLock": "Tool lock"
|
||||||
},
|
},
|
||||||
"elementLink": {
|
"elementLink": {
|
||||||
"title": "Link to object",
|
"title": "Link to object",
|
||||||
|
|||||||
@ -81,11 +81,13 @@
|
|||||||
"@braintree/sanitize-url": "6.0.2",
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
"@excalidraw/common": "0.18.0",
|
"@excalidraw/common": "0.18.0",
|
||||||
"@excalidraw/element": "0.18.0",
|
"@excalidraw/element": "0.18.0",
|
||||||
"@excalidraw/math": "0.18.0",
|
|
||||||
"@excalidraw/laser-pointer": "1.3.1",
|
"@excalidraw/laser-pointer": "1.3.1",
|
||||||
|
"@excalidraw/math": "0.18.0",
|
||||||
"@excalidraw/mermaid-to-excalidraw": "1.1.3",
|
"@excalidraw/mermaid-to-excalidraw": "1.1.3",
|
||||||
"@excalidraw/random-username": "1.1.0",
|
"@excalidraw/random-username": "1.1.0",
|
||||||
|
"@radix-ui/react-dropdown-menu": "2.1.16",
|
||||||
"@radix-ui/react-popover": "1.1.6",
|
"@radix-ui/react-popover": "1.1.6",
|
||||||
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
"@radix-ui/react-tabs": "1.1.3",
|
"@radix-ui/react-tabs": "1.1.3",
|
||||||
"browser-fs-access": "0.29.1",
|
"browser-fs-access": "0.29.1",
|
||||||
"canvas-roundrect-polyfill": "0.0.1",
|
"canvas-roundrect-polyfill": "0.0.1",
|
||||||
@ -97,8 +99,8 @@
|
|||||||
"image-blob-reduce": "3.0.1",
|
"image-blob-reduce": "3.0.1",
|
||||||
"jotai": "2.11.0",
|
"jotai": "2.11.0",
|
||||||
"jotai-scope": "0.7.2",
|
"jotai-scope": "0.7.2",
|
||||||
"lodash.throttle": "4.1.1",
|
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
|
"lodash.throttle": "4.1.1",
|
||||||
"nanoid": "3.3.3",
|
"nanoid": "3.3.3",
|
||||||
"open-color": "1.9.1",
|
"open-color": "1.9.1",
|
||||||
"pako": "2.0.3",
|
"pako": "2.0.3",
|
||||||
|
|||||||
@ -1,13 +1,18 @@
|
|||||||
import { queryByText, queryByTestId } from "@testing-library/react";
|
import { queryByText, queryByTestId } from "@testing-library/react";
|
||||||
import React from "react";
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { THEME } from "@excalidraw/common";
|
import { THEME } from "@excalidraw/common";
|
||||||
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { Excalidraw, Footer, MainMenu } from "../index";
|
import { Excalidraw, Footer } from "..";
|
||||||
|
import MainMenu from "../components/main-menu/MainMenu";
|
||||||
|
|
||||||
import { fireEvent, GlobalTestState, toggleMenu, render } from "./test-utils";
|
import {
|
||||||
|
render,
|
||||||
|
togglePopover,
|
||||||
|
fireEvent,
|
||||||
|
GlobalTestState,
|
||||||
|
} from "./test-utils";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@ -15,7 +20,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
const menu = document.querySelector(".dropdown-menu");
|
const menu = document.querySelector(".dropdown-menu");
|
||||||
if (menu) {
|
if (menu) {
|
||||||
toggleMenu(document.querySelector(".excalidraw")!);
|
togglePopover("Main menu");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -136,7 +141,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
<Excalidraw UIOptions={undefined} />,
|
<Excalidraw UIOptions={undefined} />,
|
||||||
);
|
);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
togglePopover("Main menu");
|
||||||
expect(queryByTestId(container, "dropdown-menu")).toMatchSnapshot();
|
expect(queryByTestId(container, "dropdown-menu")).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -145,7 +150,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
<Excalidraw UIOptions={{ canvasActions: { clearCanvas: false } }} />,
|
<Excalidraw UIOptions={{ canvasActions: { clearCanvas: false } }} />,
|
||||||
);
|
);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
togglePopover("Main menu");
|
||||||
expect(queryByTestId(container, "clear-canvas-button")).toBeNull();
|
expect(queryByTestId(container, "clear-canvas-button")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -154,7 +159,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
<Excalidraw UIOptions={{ canvasActions: { export: false } }} />,
|
<Excalidraw UIOptions={{ canvasActions: { export: false } }} />,
|
||||||
);
|
);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
togglePopover("Main menu");
|
||||||
expect(queryByTestId(container, "json-export-button")).toBeNull();
|
expect(queryByTestId(container, "json-export-button")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -163,7 +168,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
<Excalidraw UIOptions={{ canvasActions: { saveAsImage: false } }} />,
|
<Excalidraw UIOptions={{ canvasActions: { saveAsImage: false } }} />,
|
||||||
);
|
);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
togglePopover("Main menu");
|
||||||
expect(queryByTestId(container, "image-export-button")).toBeNull();
|
expect(queryByTestId(container, "image-export-button")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -182,7 +187,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
togglePopover("Main menu");
|
||||||
expect(queryByTestId(container, "save-as-button")).toBeNull();
|
expect(queryByTestId(container, "save-as-button")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -193,7 +198,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
togglePopover("Main menu");
|
||||||
expect(queryByTestId(container, "save-button")).toBeNull();
|
expect(queryByTestId(container, "save-button")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -204,7 +209,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
togglePopover("Main menu");
|
||||||
expect(queryByTestId(container, "canvas-background-label")).toBeNull();
|
expect(queryByTestId(container, "canvas-background-label")).toBeNull();
|
||||||
expect(queryByTestId(container, "canvas-background-picker")).toBeNull();
|
expect(queryByTestId(container, "canvas-background-picker")).toBeNull();
|
||||||
});
|
});
|
||||||
@ -220,7 +225,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
</Excalidraw>,
|
</Excalidraw>,
|
||||||
);
|
);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
togglePopover("Main menu");
|
||||||
expect(queryByTestId(container, "canvas-background-label")).toBeNull();
|
expect(queryByTestId(container, "canvas-background-label")).toBeNull();
|
||||||
expect(queryByTestId(container, "canvas-background-picker")).toBeNull();
|
expect(queryByTestId(container, "canvas-background-picker")).toBeNull();
|
||||||
});
|
});
|
||||||
@ -230,7 +235,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
<Excalidraw UIOptions={{ canvasActions: { toggleTheme: false } }} />,
|
<Excalidraw UIOptions={{ canvasActions: { toggleTheme: false } }} />,
|
||||||
);
|
);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
togglePopover("Main menu");
|
||||||
expect(queryByTestId(container, "toggle-dark-mode")).toBeNull();
|
expect(queryByTestId(container, "toggle-dark-mode")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -251,8 +256,8 @@ describe("<Excalidraw/>", () => {
|
|||||||
</Excalidraw>,
|
</Excalidraw>,
|
||||||
);
|
);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
|
||||||
// load button shouldn't be rendered since `UIActions.canvasActions.loadScene` is `false`
|
// load button shouldn't be rendered since `UIActions.canvasActions.loadScene` is `false`
|
||||||
|
togglePopover("Main menu");
|
||||||
expect(queryByTestId(container, "load-button")).toBeNull();
|
expect(queryByTestId(container, "load-button")).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -263,7 +268,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
const { container } = await render(<Excalidraw />);
|
const { container } = await render(<Excalidraw />);
|
||||||
expect(h.state.theme).toBe(THEME.LIGHT);
|
expect(h.state.theme).toBe(THEME.LIGHT);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
togglePopover("Main menu");
|
||||||
const darkModeToggle = queryByTestId(container, "toggle-dark-mode");
|
const darkModeToggle = queryByTestId(container, "toggle-dark-mode");
|
||||||
expect(darkModeToggle).toBeTruthy();
|
expect(darkModeToggle).toBeTruthy();
|
||||||
});
|
});
|
||||||
@ -273,7 +278,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
|
|
||||||
expect(h.state.theme).toBe(THEME.DARK);
|
expect(h.state.theme).toBe(THEME.DARK);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
togglePopover("Main menu");
|
||||||
expect(queryByTestId(container, "toggle-dark-mode")).toBe(null);
|
expect(queryByTestId(container, "toggle-dark-mode")).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -286,7 +291,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
);
|
);
|
||||||
expect(h.state.theme).toBe(THEME.DARK);
|
expect(h.state.theme).toBe(THEME.DARK);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
togglePopover("Main menu");
|
||||||
const darkModeToggle = queryByTestId(container, "toggle-dark-mode");
|
const darkModeToggle = queryByTestId(container, "toggle-dark-mode");
|
||||||
expect(darkModeToggle).toBeTruthy();
|
expect(darkModeToggle).toBeTruthy();
|
||||||
});
|
});
|
||||||
@ -300,7 +305,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
);
|
);
|
||||||
expect(h.state.theme).toBe(THEME.DARK);
|
expect(h.state.theme).toBe(THEME.DARK);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
togglePopover("Main menu");
|
||||||
const darkModeToggle = queryByTestId(container, "toggle-dark-mode");
|
const darkModeToggle = queryByTestId(container, "toggle-dark-mode");
|
||||||
expect(darkModeToggle).toBe(null);
|
expect(darkModeToggle).toBe(null);
|
||||||
});
|
});
|
||||||
@ -310,7 +315,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
it("should allow editing name", async () => {
|
it("should allow editing name", async () => {
|
||||||
const { container } = await render(<Excalidraw />);
|
const { container } = await render(<Excalidraw />);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
togglePopover("Main menu");
|
||||||
fireEvent.click(queryByTestId(container, "image-export-button")!);
|
fireEvent.click(queryByTestId(container, "image-export-button")!);
|
||||||
const textInput: HTMLInputElement | null = document.querySelector(
|
const textInput: HTMLInputElement | null = document.querySelector(
|
||||||
".ImageExportModal .ImageExportModal__preview__filename .TextInput",
|
".ImageExportModal .ImageExportModal__preview__filename .TextInput",
|
||||||
@ -323,7 +328,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
const name = "test";
|
const name = "test";
|
||||||
const { container } = await render(<Excalidraw name={name} />);
|
const { container } = await render(<Excalidraw name={name} />);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
togglePopover("Main menu");
|
||||||
await fireEvent.click(queryByTestId(container, "image-export-button")!);
|
await fireEvent.click(queryByTestId(container, "image-export-button")!);
|
||||||
const textInput = document.querySelector(
|
const textInput = document.querySelector(
|
||||||
".ImageExportModal .ImageExportModal__preview__filename .TextInput",
|
".ImageExportModal .ImageExportModal__preview__filename .TextInput",
|
||||||
@ -375,7 +380,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
</Excalidraw>,
|
</Excalidraw>,
|
||||||
);
|
);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
togglePopover("Main menu");
|
||||||
expect(queryByTestId(container, "dropdown-menu")).toMatchSnapshot();
|
expect(queryByTestId(container, "dropdown-menu")).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -394,7 +399,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
|
|
||||||
const { container } = await render(<CustomExcalidraw />);
|
const { container } = await render(<CustomExcalidraw />);
|
||||||
//open menu
|
//open menu
|
||||||
toggleMenu(container);
|
togglePopover("Main menu");
|
||||||
|
|
||||||
expect(h.state.theme).toBe(THEME.LIGHT);
|
expect(h.state.theme).toBe(THEME.LIGHT);
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { act, queryByTestId } from "@testing-library/react";
|
import { queryByTestId } from "@testing-library/react";
|
||||||
import React from "react";
|
import { act } from "@testing-library/react";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
||||||
import { MIME_TYPES, ORIG_ID } from "@excalidraw/common";
|
import { MIME_TYPES, ORIG_ID } from "@excalidraw/common";
|
||||||
@ -13,9 +13,11 @@ import { serializeLibraryAsJSON } from "../data/json";
|
|||||||
import { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
import { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
|
|
||||||
|
import { fireEvent, render, togglePopover, waitFor } from "./test-utils";
|
||||||
|
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import { UI } from "./helpers/ui";
|
import { UI } from "./helpers/ui";
|
||||||
import { fireEvent, getCloneByOrigId, render, waitFor } from "./test-utils";
|
import { getCloneByOrigId } from "./test-utils";
|
||||||
|
|
||||||
import type { LibraryItem, LibraryItems } from "../types";
|
import type { LibraryItem, LibraryItems } from "../types";
|
||||||
|
|
||||||
@ -215,12 +217,13 @@ describe("library menu", () => {
|
|||||||
const libraryButton = container.querySelector(".sidebar-trigger");
|
const libraryButton = container.querySelector(".sidebar-trigger");
|
||||||
|
|
||||||
fireEvent.click(libraryButton!);
|
fireEvent.click(libraryButton!);
|
||||||
fireEvent.click(
|
togglePopover("Library menu");
|
||||||
queryByTestId(
|
// fireEvent.click(
|
||||||
container.querySelector(".layer-ui__library")!,
|
// queryByTestId(
|
||||||
"dropdown-menu-button",
|
// container.querySelector(".layer-ui__library")!,
|
||||||
)!,
|
// "dropdown-menu-button",
|
||||||
);
|
// )!,
|
||||||
|
// );
|
||||||
fireEvent.click(queryByTestId(container, "lib-dropdown--load")!);
|
fireEvent.click(queryByTestId(container, "lib-dropdown--load")!);
|
||||||
|
|
||||||
const libraryItems = parseLibraryJSON(await libraryJSONPromise);
|
const libraryItems = parseLibraryJSON(await libraryJSONPromise);
|
||||||
|
|||||||
@ -2,26 +2,46 @@
|
|||||||
|
|
||||||
exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu items if passed from host 1`] = `
|
exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu items if passed from host 1`] = `
|
||||||
<div
|
<div
|
||||||
class="dropdown-menu"
|
aria-labelledby="radix-:r65:"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
class="dropdown-menu main-menu-content"
|
||||||
|
data-align="start"
|
||||||
|
data-orientation="vertical"
|
||||||
|
data-radix-menu-content=""
|
||||||
|
data-side="bottom"
|
||||||
|
data-state="open"
|
||||||
data-testid="dropdown-menu"
|
data-testid="dropdown-menu"
|
||||||
|
dir="ltr"
|
||||||
|
id="radix-:r66:"
|
||||||
|
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
|
<div
|
||||||
class="Island dropdown-menu-container"
|
class="Island dropdown-menu-container"
|
||||||
style="--padding: 2; z-index: 2;"
|
style="--padding: 1; z-index: 2;"
|
||||||
>
|
>
|
||||||
<button
|
<div
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="radix-menu-item"
|
||||||
type="button"
|
data-orientation="vertical"
|
||||||
|
data-radix-collection-item=""
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<button
|
||||||
class="dropdown-menu-item__icon"
|
class="excalidraw-button dropdown-menu-item dropdown-menu-item-base"
|
||||||
/>
|
type="button"
|
||||||
<div
|
|
||||||
class="dropdown-menu-item__text"
|
|
||||||
>
|
>
|
||||||
Click me
|
<div
|
||||||
</div>
|
class="dropdown-menu-item__icon"
|
||||||
</button>
|
/>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu-item__text"
|
||||||
|
>
|
||||||
|
Click me
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<a
|
<a
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
href="https://plus.excalidraw.com/blog"
|
href="https://plus.excalidraw.com/blog"
|
||||||
@ -46,301 +66,361 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
|||||||
custom menu item
|
custom menu item
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div
|
||||||
aria-label="Help"
|
class="radix-menu-item"
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
data-orientation="vertical"
|
||||||
data-testid="help-menu-item"
|
data-radix-collection-item=""
|
||||||
title="Help"
|
role="menuitem"
|
||||||
type="button"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<button
|
||||||
class="dropdown-menu-item__icon"
|
aria-label="Help"
|
||||||
|
class="excalidraw-button dropdown-menu-item dropdown-menu-item-base"
|
||||||
|
data-testid="help-menu-item"
|
||||||
|
title="Help"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<svg
|
<div
|
||||||
aria-hidden="true"
|
class="dropdown-menu-item__icon"
|
||||||
class=""
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
role="img"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
>
|
||||||
<g
|
<svg
|
||||||
stroke-width="1.5"
|
aria-hidden="true"
|
||||||
|
class=""
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path
|
<g
|
||||||
d="M0 0h24v24H0z"
|
stroke-width="1.5"
|
||||||
fill="none"
|
>
|
||||||
stroke="none"
|
<path
|
||||||
/>
|
d="M0 0h24v24H0z"
|
||||||
<circle
|
fill="none"
|
||||||
cx="12"
|
stroke="none"
|
||||||
cy="12"
|
/>
|
||||||
r="9"
|
<circle
|
||||||
/>
|
cx="12"
|
||||||
<line
|
cy="12"
|
||||||
x1="12"
|
r="9"
|
||||||
x2="12"
|
/>
|
||||||
y1="17"
|
<line
|
||||||
y2="17.01"
|
x1="12"
|
||||||
/>
|
x2="12"
|
||||||
<path
|
y1="17"
|
||||||
d="M12 13.5a1.5 1.5 0 0 1 1 -1.5a2.6 2.6 0 1 0 -3 -4"
|
y2="17.01"
|
||||||
/>
|
/>
|
||||||
</g>
|
<path
|
||||||
</svg>
|
d="M12 13.5a1.5 1.5 0 0 1 1 -1.5a2.6 2.6 0 1 0 -3 -4"
|
||||||
</div>
|
/>
|
||||||
<div
|
</g>
|
||||||
class="dropdown-menu-item__text"
|
</svg>
|
||||||
>
|
</div>
|
||||||
Help
|
<div
|
||||||
</div>
|
class="dropdown-menu-item__text"
|
||||||
<div
|
>
|
||||||
class="dropdown-menu-item__shortcut"
|
Help
|
||||||
>
|
</div>
|
||||||
?
|
<div
|
||||||
</div>
|
class="dropdown-menu-item__shortcut"
|
||||||
</button>
|
>
|
||||||
|
?
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should render menu with default items when "UIOPtions" is "undefined" 1`] = `
|
exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should render menu with default items when "UIOPtions" is "undefined" 1`] = `
|
||||||
<div
|
<div
|
||||||
class="dropdown-menu"
|
aria-labelledby="radix-:rq:"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
class="dropdown-menu main-menu-content"
|
||||||
|
data-align="start"
|
||||||
|
data-orientation="vertical"
|
||||||
|
data-radix-menu-content=""
|
||||||
|
data-side="bottom"
|
||||||
|
data-state="open"
|
||||||
data-testid="dropdown-menu"
|
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
|
<div
|
||||||
class="Island dropdown-menu-container"
|
class="Island dropdown-menu-container"
|
||||||
style="--padding: 2; z-index: 2;"
|
style="--padding: 1; z-index: 2;"
|
||||||
>
|
>
|
||||||
<button
|
<div
|
||||||
aria-label="Open"
|
class="radix-menu-item"
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
data-orientation="vertical"
|
||||||
data-testid="load-button"
|
data-radix-collection-item=""
|
||||||
title="Open"
|
role="menuitem"
|
||||||
type="button"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<button
|
||||||
class="dropdown-menu-item__icon"
|
aria-label="Open"
|
||||||
|
class="excalidraw-button dropdown-menu-item dropdown-menu-item-base"
|
||||||
|
data-testid="load-button"
|
||||||
|
title="Open"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<svg
|
<div
|
||||||
aria-hidden="true"
|
class="dropdown-menu-item__icon"
|
||||||
class=""
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
role="img"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
d="m9.257 6.351.183.183H15.819c.34 0 .727.182 1.051.506.323.323.505.708.505 1.05v5.819c0 .316-.183.7-.52 1.035-.337.338-.723.522-1.037.522H4.182c-.352 0-.74-.181-1.058-.5-.318-.318-.499-.705-.499-1.057V5.182c0-.351.181-.736.5-1.054.32-.321.71-.503 1.057-.503H6.53l2.726 2.726Z"
|
aria-hidden="true"
|
||||||
stroke-width="1.25"
|
class=""
|
||||||
/>
|
fill="none"
|
||||||
</svg>
|
focusable="false"
|
||||||
</div>
|
role="img"
|
||||||
<div
|
stroke="currentColor"
|
||||||
class="dropdown-menu-item__text"
|
stroke-linecap="round"
|
||||||
>
|
stroke-linejoin="round"
|
||||||
Open
|
viewBox="0 0 20 20"
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="dropdown-menu-item__shortcut"
|
|
||||||
>
|
|
||||||
Ctrl+O
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
aria-label="Save to..."
|
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
|
||||||
data-testid="json-export-button"
|
|
||||||
title="Save to..."
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="dropdown-menu-item__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
class=""
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
role="img"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M3.333 14.167v1.666c0 .92.747 1.667 1.667 1.667h10c.92 0 1.667-.746 1.667-1.667v-1.666M5.833 9.167 10 13.333l4.167-4.166M10 3.333v10"
|
|
||||||
stroke-width="1.25"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="dropdown-menu-item__text"
|
|
||||||
>
|
|
||||||
Save to...
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
aria-label="Export image..."
|
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
|
||||||
data-testid="image-export-button"
|
|
||||||
title="Export image..."
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="dropdown-menu-item__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
class=""
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
role="img"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<g
|
|
||||||
stroke-width="1.25"
|
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="m9.257 6.351.183.183H15.819c.34 0 .727.182 1.051.506.323.323.505.708.505 1.05v5.819c0 .316-.183.7-.52 1.035-.337.338-.723.522-1.037.522H4.182c-.352 0-.74-.181-1.058-.5-.318-.318-.499-.705-.499-1.057V5.182c0-.351.181-.736.5-1.054.32-.321.71-.503 1.057-.503H6.53l2.726 2.726Z"
|
||||||
fill="none"
|
stroke-width="1.25"
|
||||||
stroke="none"
|
|
||||||
/>
|
/>
|
||||||
<path
|
</svg>
|
||||||
d="M15 8h.01"
|
</div>
|
||||||
/>
|
<div
|
||||||
<path
|
class="dropdown-menu-item__text"
|
||||||
d="M12 20h-5a3 3 0 0 1 -3 -3v-10a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M4 15l4 -4c.928 -.893 2.072 -.893 3 0l4 4"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M14 14l1 -1c.617 -.593 1.328 -.793 2.009 -.598"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M19 16v6"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M22 19l-3 3l-3 -3"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="dropdown-menu-item__text"
|
|
||||||
>
|
|
||||||
Export image...
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="dropdown-menu-item__shortcut"
|
|
||||||
>
|
|
||||||
Ctrl+Shift+E
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
aria-label="Help"
|
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
|
||||||
data-testid="help-menu-item"
|
|
||||||
title="Help"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="dropdown-menu-item__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
class=""
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
role="img"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
>
|
||||||
<g
|
Open
|
||||||
stroke-width="1.5"
|
</div>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu-item__shortcut"
|
||||||
|
>
|
||||||
|
Ctrl+O
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="radix-menu-item"
|
||||||
|
data-orientation="vertical"
|
||||||
|
data-radix-collection-item=""
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="Save to..."
|
||||||
|
class="excalidraw-button dropdown-menu-item dropdown-menu-item-base"
|
||||||
|
data-testid="json-export-button"
|
||||||
|
title="Save to..."
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu-item__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class=""
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M3.333 14.167v1.666c0 .92.747 1.667 1.667 1.667h10c.92 0 1.667-.746 1.667-1.667v-1.666M5.833 9.167 10 13.333l4.167-4.166M10 3.333v10"
|
||||||
fill="none"
|
stroke-width="1.25"
|
||||||
stroke="none"
|
|
||||||
/>
|
/>
|
||||||
<circle
|
</svg>
|
||||||
cx="12"
|
</div>
|
||||||
cy="12"
|
<div
|
||||||
r="9"
|
class="dropdown-menu-item__text"
|
||||||
/>
|
|
||||||
<line
|
|
||||||
x1="12"
|
|
||||||
x2="12"
|
|
||||||
y1="17"
|
|
||||||
y2="17.01"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M12 13.5a1.5 1.5 0 0 1 1 -1.5a2.6 2.6 0 1 0 -3 -4"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="dropdown-menu-item__text"
|
|
||||||
>
|
|
||||||
Help
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="dropdown-menu-item__shortcut"
|
|
||||||
>
|
|
||||||
?
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
aria-label="Reset the canvas"
|
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
|
||||||
data-testid="clear-canvas-button"
|
|
||||||
title="Reset the canvas"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="dropdown-menu-item__icon"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
class=""
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
role="img"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
>
|
>
|
||||||
<path
|
Save to...
|
||||||
d="M3.333 5.833h13.334M8.333 9.167v5M11.667 9.167v5M4.167 5.833l.833 10c0 .92.746 1.667 1.667 1.667h6.666c.92 0 1.667-.746 1.667-1.667l.833-10M7.5 5.833v-2.5c0-.46.373-.833.833-.833h3.334c.46 0 .833.373.833.833v2.5"
|
</div>
|
||||||
stroke-width="1.25"
|
</button>
|
||||||
/>
|
</div>
|
||||||
</svg>
|
<div
|
||||||
</div>
|
class="radix-menu-item"
|
||||||
<div
|
data-orientation="vertical"
|
||||||
class="dropdown-menu-item__text"
|
data-radix-collection-item=""
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="Export image..."
|
||||||
|
class="excalidraw-button dropdown-menu-item dropdown-menu-item-base"
|
||||||
|
data-testid="image-export-button"
|
||||||
|
title="Export image..."
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
Reset the canvas
|
<div
|
||||||
</div>
|
class="dropdown-menu-item__icon"
|
||||||
</button>
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class=""
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
stroke-width="1.25"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 0h24v24H0z"
|
||||||
|
fill="none"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M15 8h.01"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12 20h-5a3 3 0 0 1 -3 -3v-10a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M4 15l4 -4c.928 -.893 2.072 -.893 3 0l4 4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M14 14l1 -1c.617 -.593 1.328 -.793 2.009 -.598"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M19 16v6"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M22 19l-3 3l-3 -3"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu-item__text"
|
||||||
|
>
|
||||||
|
Export image...
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu-item__shortcut"
|
||||||
|
>
|
||||||
|
Ctrl+Shift+E
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="radix-menu-item"
|
||||||
|
data-orientation="vertical"
|
||||||
|
data-radix-collection-item=""
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="Help"
|
||||||
|
class="excalidraw-button dropdown-menu-item dropdown-menu-item-base"
|
||||||
|
data-testid="help-menu-item"
|
||||||
|
title="Help"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu-item__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class=""
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
stroke-width="1.5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 0h24v24H0z"
|
||||||
|
fill="none"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="9"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="12"
|
||||||
|
x2="12"
|
||||||
|
y1="17"
|
||||||
|
y2="17.01"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12 13.5a1.5 1.5 0 0 1 1 -1.5a2.6 2.6 0 1 0 -3 -4"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu-item__text"
|
||||||
|
>
|
||||||
|
Help
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu-item__shortcut"
|
||||||
|
>
|
||||||
|
?
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="radix-menu-item"
|
||||||
|
data-orientation="vertical"
|
||||||
|
data-radix-collection-item=""
|
||||||
|
role="menuitem"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="Reset the canvas"
|
||||||
|
class="excalidraw-button dropdown-menu-item dropdown-menu-item-base"
|
||||||
|
data-testid="clear-canvas-button"
|
||||||
|
title="Reset the canvas"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu-item__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class=""
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3.333 5.833h13.334M8.333 9.167v5M11.667 9.167v5M4.167 5.833l.833 10c0 .92.746 1.667 1.667 1.667h6.666c.92 0 1.667-.746 1.667-1.667l.833-10M7.5 5.833v-2.5c0-.46.373-.833.833-.833h3.334c.46 0 .833.373.833.833v2.5"
|
||||||
|
stroke-width="1.25"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu-item__text"
|
||||||
|
>
|
||||||
|
Reset the canvas
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
style="height: 1px; margin: .5rem 0px;"
|
style="height: 1px; margin: .5rem 0px;"
|
||||||
/>
|
/>
|
||||||
@ -473,45 +553,53 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
<div
|
<div
|
||||||
style="height: 1px; margin: .5rem 0px;"
|
style="height: 1px; margin: .5rem 0px;"
|
||||||
/>
|
/>
|
||||||
<button
|
<div
|
||||||
aria-label="Dark mode"
|
class="radix-menu-item"
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
data-orientation="vertical"
|
||||||
data-testid="toggle-dark-mode"
|
data-radix-collection-item=""
|
||||||
title="Dark mode"
|
role="menuitem"
|
||||||
type="button"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<button
|
||||||
class="dropdown-menu-item__icon"
|
aria-label="Dark mode"
|
||||||
|
class="excalidraw-button dropdown-menu-item dropdown-menu-item-base"
|
||||||
|
data-testid="toggle-dark-mode"
|
||||||
|
title="Dark mode"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<svg
|
<div
|
||||||
aria-hidden="true"
|
class="dropdown-menu-item__icon"
|
||||||
class=""
|
|
||||||
fill="none"
|
|
||||||
focusable="false"
|
|
||||||
role="img"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
clip-rule="evenodd"
|
aria-hidden="true"
|
||||||
d="M10 2.5h.328a6.25 6.25 0 0 0 6.6 10.372A7.5 7.5 0 1 1 10 2.493V2.5Z"
|
class=""
|
||||||
|
fill="none"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
/>
|
stroke-linecap="round"
|
||||||
</svg>
|
stroke-linejoin="round"
|
||||||
</div>
|
viewBox="0 0 20 20"
|
||||||
<div
|
>
|
||||||
class="dropdown-menu-item__text"
|
<path
|
||||||
>
|
clip-rule="evenodd"
|
||||||
Dark mode
|
d="M10 2.5h.328a6.25 6.25 0 0 0 6.6 10.372A7.5 7.5 0 1 1 10 2.493V2.5Z"
|
||||||
</div>
|
stroke="currentColor"
|
||||||
<div
|
/>
|
||||||
class="dropdown-menu-item__shortcut"
|
</svg>
|
||||||
>
|
</div>
|
||||||
Shift+Alt+D
|
<div
|
||||||
</div>
|
class="dropdown-menu-item__text"
|
||||||
</button>
|
>
|
||||||
|
Dark mode
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu-item__shortcut"
|
||||||
|
>
|
||||||
|
Shift+Alt+D
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
style="margin-top: 0.5rem;"
|
style="margin-top: 0.5rem;"
|
||||||
>
|
>
|
||||||
@ -522,6 +610,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
Canvas background
|
Canvas background
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
id="canvas-bg-color-picker-container"
|
||||||
style="padding: 0px 0.625rem;"
|
style="padding: 0px 0.625rem;"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@ -593,7 +682,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
style="width: 1px; height: 100%; margin: 0px auto;"
|
style="width: 1px; height: 100%; margin: 0px auto;"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
aria-controls="radix-:r0:"
|
aria-controls="radix-:r12:"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="dialog"
|
aria-haspopup="dialog"
|
||||||
aria-label="Canvas background"
|
aria-label="Canvas background"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user