Merge remote-tracking branch 'origin/release' into danieljgeiger-mathjax-maint-stage

This commit is contained in:
Daniel J. Geiger 2023-09-17 15:40:45 -05:00
commit 208285b7ba
18 changed files with 275 additions and 104 deletions

View File

@ -17,5 +17,6 @@ Below are the currently supported components:
- [MainMenu](/docs/@excalidraw/excalidraw/api/children-components/main-menu) - [MainMenu](/docs/@excalidraw/excalidraw/api/children-components/main-menu)
- [WelcomeScreen](/docs/@excalidraw/excalidraw/api/children-components/welcome-screen) - [WelcomeScreen](/docs/@excalidraw/excalidraw/api/children-components/welcome-screen)
- [Sidebar](/docs/@excalidraw/excalidraw/api/children-components/sidebar)
- [Footer](/docs/@excalidraw/excalidraw/api/children-components/footer) - [Footer](/docs/@excalidraw/excalidraw/api/children-components/footer)
- [LiveCollaborationTrigger](/docs/@excalidraw/excalidraw/api/children-components/live-collaboration-trigger) - [LiveCollaborationTrigger](/docs/@excalidraw/excalidraw/api/children-components/live-collaboration-trigger)

View File

@ -0,0 +1,129 @@
# Sidebar
The editor comes with a default sidebar on the right in LTR (Left to Right) mode which contains the library. You can also add your own custom sidebar(s) by rendering this component as a child of `<Excalidraw>`.
## Props
| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | `string` | Yes | Sidebar name that uniquely identifies it. |
| `children` | `React.ReactNode` | Yes | Content you want to render inside the sidebar. |
| `onStateChange` | `(state: AppState["openSidebar"]) => void` | No | Invoked on open/close or tab change. No need to act on this event, as the editor manages the sidebar open state on its own. |
| `onDock` | `(docked: boolean) => void` | No | Invoked when the user toggles the `dock` button. Passed the current docked state. |
| `docked` | `boolean` | No | Indicates whether the sidebar is `docked`. By default, the sidebar is `undocked`. If passed, the docking becomes controlled. |
| `className` | `string` | No | |
| `style` | `React.CSSProperties` | No | |
At minimum, each sidebar needs to have a unique `name` prop, and render some content inside it, which can be either composed from the exported sidebar sub-components, or custom elements.
Unless `docked={true}` is passed, the sidebar will close when the user clicks outside of it. It can also be closed using the close button in the header, if you render the `<Sidebar.Header>` component.
Further, if the sidebader doesn't comfortably fit in the editor, it won't be dockable. To decide the breakpoint for docking you can use [UIOptions.dockedSidebarBreakpoint](/docs/@excalidraw/excalidraw/api/props/ui-options#dockedsidebarbreakpoint).
To make your sidebar user-dockable, you need to supply `props.docked` (current docked state) alongside `props.onDock` callback (to listen for and handle docked state changes). The component doesn't track local state for the `docked` prop, so you need to manage it yourself.
## Sidebar.Header
| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | No | Content you want to render inside the sidebar header next to the `close` / `dock` buttons. |
| `className` | `string` | No | |
Renders a sidebar header which contains a close button, and a dock button (when applicable). You can also render custom content in addition.
Can be nested inside specific tabs, or rendered as direct child of `<Sidebar>` for the whole sidebar component.
## Sidebar.Tabs
| Prop | Type | Required | Description |
| ---------- | ----------------- | -------- | ------------------------------ |
| `children` | `React.ReactNode` | No | Container for individual tabs. |
Sidebar may contain inner tabs. Each `<Sidebar.Tab>` must be rendered inside this `<Sidebar.Tabs>` container component.
## Sidebar.Tab
| Prop | Type | Required | Description |
| ---------- | ----------------- | -------- | ---------------- |
| `tab` | `string` | Yes | Unique tab name. |
| `children` | `React.ReactNode` | No | Tab content. |
Content of a given sidebar tab. It must be rendered inside `<Sidebar.Tabs>`.
## Sidebar.TabTriggers
| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | No | Container for individual tab triggers. |
Container component for tab trigger buttons to switch between tabs.
## Sidebar.TabTrigger
| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `tab` | `string` | Yes | Tab name to toggle. |
| `children` | `React.ReactNode` | No | Tab trigger content, such as a label. |
A given tab trigger button that switches to a given sidebar tab. It must be rendered inside `<Sidebar.TabTriggers>`.
## Sidebar.Trigger
| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | `string` | Yes | Sidebar name the trigger will control. |
| `tab` | `string` | No | Optional tab to open. |
| `onToggle` | `(open: boolean) => void` | No | Callback invoked on toggle. |
| `title` | `string` | No | A11y title. |
| `children` | `React.ReactNode` | No | Content (usually label) you want to render inside the button. |
| `icon` | `JSX.Element` | No | Trigger icon if any. |
| `className` | `string` | No | |
| `style` | `React.CSSProperties` | No | |
You can use the [`ref.toggleSidebar({ name: "custom" })`](/docs/@excalidraw/excalidraw/api/props/ref#toggleSidebar) api to control the sidebar, but we export a trigger button to make UI use cases easier.
## Example
```tsx live
function App() {
const [docked, setDocked] = useState(false);
return (
<div style={{ height: "580px" }}>
<Excalidraw
UIOptions={{
// this effectively makes the sidebar dockable on any screen size,
// ignoring if it fits or not
dockedSidebarBreakpoint: 0,
}}
>
<Sidebar name="custom" docked={docked} onDock={setDocked}>
<Sidebar.Header />
<Sidebar.Tabs style={{ padding: "0.5rem" }}>
<Sidebar.Tab tab="one">Tab one!</Sidebar.Tab>
<Sidebar.Tab tab="two">Tab two!</Sidebar.Tab>
<Sidebar.TabTriggers>
<Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger>
<Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger>
</Sidebar.TabTriggers>
</Sidebar.Tabs>
</Sidebar>
<Footer>
<Sidebar.Trigger
name="custom"
tab="one"
style={{
marginLeft: "0.5rem",
background: "#70b1ec",
color: "white",
}}
>
Toggle Custom Sidebar
</Sidebar.Trigger>
</Footer>
</Excalidraw>
</div>
);
}
```

View File

@ -17,7 +17,6 @@ All `props` are *optional*.
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw | | [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | _ | Render function that renders custom UI in top right corner | | [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | _ | Render function that renders custom UI in top right corner |
| [`renderCustomStats`](/docs/@excalidraw/excalidraw/api/props/render-props#rendercustomstats) | `function` | _ | Render function that can be used to render custom stats on the stats dialog. | | [`renderCustomStats`](/docs/@excalidraw/excalidraw/api/props/render-props#rendercustomstats) | `function` | _ | Render function that can be used to render custom stats on the stats dialog. |
| [`renderSidebar`](/docs/@excalidraw/excalidraw/api/props/render-props#rendersidebar) | `function` | _ | Render function that renders custom sidebar. |
| [`viewModeEnabled`](#viewmodeenabled) | `boolean` | _ | This indicates if the app is in `view` mode. | | [`viewModeEnabled`](#viewmodeenabled) | `boolean` | _ | This indicates if the app is in `view` mode. |
| [`zenModeEnabled`](#zenmodeenabled) | `boolean` | _ | This indicates if the `zen` mode is enabled | | [`zenModeEnabled`](#zenmodeenabled) | `boolean` | _ | This indicates if the `zen` mode is enabled |
| [`gridModeEnabled`](#gridmodeenabled) | `boolean` | _ | This indicates if the `grid` mode is enabled | | [`gridModeEnabled`](#gridmodeenabled) | `boolean` | _ | This indicates if the `grid` mode is enabled |

View File

@ -404,15 +404,15 @@ This API can be used to customise the mouse cursor on the canvas and has the bel
(cursor: string) => void (cursor: string) => void
``` ```
## toggleMenu ## toggleSidebar
```tsx ```tsx
(type: "library" | "customSidebar", force?: boolean) => boolean; (opts: { name: string; tab?: string; force?: boolean }) => boolean;
``` ```
This API can be used to toggle a specific menu (currently only the sidebars), and returns whether the menu was toggled on or off. If the `force` flag passed, it will force the menu to be toggled either on/off based on the `boolean` passed. This API can be used to toggle sidebar, optionally opening a specific sidebar tab. It returns whether the sidebar was toggled on or off. If the `force` flag passed, it will force the sidebar to be toggled either on/off.
This API is especially useful when you render a custom sidebar using [`renderSidebar`](#rendersidebar) prop, and you want to toggle it from your app based on a user action. This API is especially useful when you render a custom [`<Sidebar/>`](/docs/@excalidraw/excalidraw/api/children-components/sidebar), and you want to toggle it from your app based on a user action.
## resetCursor ## resetCursor

View File

@ -6,8 +6,7 @@
(isMobile: boolean, appState: (isMobile: boolean, appState:
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95"> <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
AppState AppState
</a> </a>) => JSX | null
) => JSX | null
</pre> </pre>
A function returning `JSX` to render `custom` UI in the top right corner of the app. A function returning `JSX` to render `custom` UI in the top right corner of the app.
@ -63,69 +62,14 @@ function App() {
} }
``` ```
## renderSidebar
```tsx
() => JSX | null;
```
You can render `custom sidebar` using this prop. This sidebar is the same that the library menu sidebar is using, and can be used for any purposes your app needs.
You need to import the `Sidebar` component from `excalidraw` package and pass your content as its `children`. The function `renderSidebar` should return the `Sidebar` instance.
### Sidebar
The `<Sidebar>` component takes these props (all are optional except `children`):
| Prop | Type | Description |
| --- | --- | --- |
| `children` | `React.ReactNode` | Content you want to render inside the `sidebar`. |
| `onClose` | `function` | Invoked when the component is closed (by user, or the editor). No need to act on this event, as the editor manages the sidebar open state on its own. |
| `onDock` | `function` | Invoked when the user toggles the `dock` button. The callback receives a `boolean` parameter `isDocked` which indicates whether the sidebar is `docked` |
| `docked` | `boolean` | Indicates whether the sidebar is`docked`. By default, the sidebar is `undocked`. If passed, the docking becomes controlled, and you are responsible for updating the `docked` state by listening on `onDock` callback. To decide the breakpoint for docking you can use [UIOptions.dockedSidebarBreakpoint](/docs/@excalidraw/excalidraw/api/props/ui-options#dockedsidebarbreakpoint) for more info on docking. |
| `dockable` | `boolean` | Indicates whether to show the `dock` button so that user can `dock` the sidebar. If `false`, you can still dock programmatically by passing `docked` as `true`. |
The sidebar will always include a header with `close / dock` buttons (when applicable).
You can also add custom content to the header, by rendering `<Sidebar.Header>` as a child of the `<Sidebar>` component. Note that the custom header will still include the default buttons.
### Sidebar.Header
| name | type | description |
| --- | --- | --- |
| children | `React.ReactNode` | Content you want to render inside the sidebar header as a sibling of `close` / `dock` buttons. |
To control the visibility of the sidebar you can use [`toggleMenu("customSidebar")`](/docs/@excalidraw/excalidraw/api/props/ref#togglemenu) api available via `ref`.
```tsx live
function App() {
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
return (
<div style={{ height: "500px" }}>
<button className="custom-button" onClick={() => excalidrawAPI.toggleMenu("customSidebar")}>
Toggle Custom Sidebar
</button>
<Excalidraw
UIOptions={{ dockedSidebarBreakpoint: 100 }}
ref={(api) => setExcalidrawAPI(api)}
renderSidebar={() => {
return (
<Sidebar dockable={true}>
<Sidebar.Header>Custom Sidebar Header </Sidebar.Header>
<p style={{ padding: "1rem" }}> custom Sidebar Content </p>
</Sidebar>
);
}}
/>
</div>
);
}
```
## renderEmbeddable ## renderEmbeddable
<pre> <pre>
(element: NonDeleted&lt;ExcalidrawEmbeddableElement&gt;, appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>) => JSX.Element | null (element: NonDeleted&lt;ExcalidrawEmbeddableElement&gt;, appState:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
AppState
</a>
) => JSX.Element | null
</pre> </pre>
Allows you to replace the renderer for embeddable elements (which renders `<iframe>` elements). Allows you to replace the renderer for embeddable elements (which renders `<iframe>` elements).

View File

@ -18,7 +18,7 @@
"@docusaurus/core": "2.2.0", "@docusaurus/core": "2.2.0",
"@docusaurus/preset-classic": "2.2.0", "@docusaurus/preset-classic": "2.2.0",
"@docusaurus/theme-live-codeblock": "2.2.0", "@docusaurus/theme-live-codeblock": "2.2.0",
"@excalidraw/excalidraw": "0.15.3", "@excalidraw/excalidraw": "0.15.2-6546-3398d86",
"@mdx-js/react": "^1.6.22", "@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"docusaurus-plugin-sass": "0.2.3", "docusaurus-plugin-sass": "0.2.3",

View File

@ -64,6 +64,7 @@ const sidebars = {
items: [ items: [
"@excalidraw/excalidraw/api/children-components/main-menu", "@excalidraw/excalidraw/api/children-components/main-menu",
"@excalidraw/excalidraw/api/children-components/welcome-screen", "@excalidraw/excalidraw/api/children-components/welcome-screen",
"@excalidraw/excalidraw/api/children-components/sidebar",
"@excalidraw/excalidraw/api/children-components/footer", "@excalidraw/excalidraw/api/children-components/footer",
"@excalidraw/excalidraw/api/children-components/live-collaboration-trigger", "@excalidraw/excalidraw/api/children-components/live-collaboration-trigger",
], ],

View File

@ -1631,10 +1631,10 @@
url-loader "^4.1.1" url-loader "^4.1.1"
webpack "^5.73.0" webpack "^5.73.0"
"@excalidraw/excalidraw@0.15.3": "@excalidraw/excalidraw@0.15.2-6546-3398d86":
version "0.15.3" version "0.15.2-6546-3398d86"
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.3.tgz#5dea570f76451adf68bc24d4bfdd67a375cfeab1" resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2-6546-3398d86.tgz#e74d5ad944b8b414924d27ee91469a32b4f08dbf"
integrity sha512-/gpY7fgMO/AEaFLWnPqzbY8H7ly+/zocFf7D0Is5sWNMD2mhult5tana12lXKLSJ6EAz7ubo1A7LajXzvJXJDA== integrity sha512-Tzq6qighJUytXRA8iMzQ8onoGclo9CuvPSw7DMvPxME8nxAxn5CeK/gsxIs3zwooj9CC6XF42BSrx0+n+fPxaQ==
"@hapi/hoek@^9.0.0": "@hapi/hoek@^9.0.0":
version "9.3.0" version "9.3.0"

View File

@ -60,6 +60,7 @@ const flipSelectedElements = (
getNonDeletedElements(elements), getNonDeletedElements(elements),
appState, appState,
{ {
includeBoundTextElement: true,
includeElementsInFrames: true, includeElementsInFrames: true,
}, },
); );

View File

@ -15,9 +15,15 @@ interface ColorInputProps {
color: string; color: string;
onChange: (color: string) => void; onChange: (color: string) => void;
label: string; label: string;
eyeDropperType: "strokeColor" | "backgroundColor";
} }
export const ColorInput = ({ color, onChange, label }: ColorInputProps) => { export const ColorInput = ({
color,
onChange,
label,
eyeDropperType,
}: ColorInputProps) => {
const device = useDevice(); const device = useDevice();
const [innerValue, setInnerValue] = useState(color); const [innerValue, setInnerValue] = useState(color);
const [activeSection, setActiveColorPickerSection] = useAtom( const [activeSection, setActiveColorPickerSection] = useAtom(
@ -110,6 +116,7 @@ export const ColorInput = ({ color, onChange, label }: ColorInputProps) => {
: { : {
keepOpenOnAlt: false, keepOpenOnAlt: false,
onSelect: (color) => onChange(color), onSelect: (color) => onChange(color),
previewType: eyeDropperType,
}, },
) )
} }

View File

@ -82,7 +82,14 @@ const ColorPickerPopupContent = ({
const { container } = useExcalidrawContainer(); const { container } = useExcalidrawContainer();
const { isMobile, isLandscape } = useDevice(); const { isMobile, isLandscape } = useDevice();
const colorInputJSX = ( const eyeDropperType =
type === "canvasBackground"
? undefined
: type === "elementBackground"
? "backgroundColor"
: "strokeColor";
const colorInputJSX = eyeDropperType && (
<div> <div>
<PickerHeading>{t("colorPicker.hexCode")}</PickerHeading> <PickerHeading>{t("colorPicker.hexCode")}</PickerHeading>
<ColorInput <ColorInput
@ -91,6 +98,7 @@ const ColorPickerPopupContent = ({
onChange={(color) => { onChange={(color) => {
onChange(color); onChange(color);
}} }}
eyeDropperType={eyeDropperType}
/> />
</div> </div>
); );
@ -140,7 +148,7 @@ const ColorPickerPopupContent = ({
alignOffset={-16} alignOffset={-16}
sideOffset={20} sideOffset={20}
style={{ style={{
zIndex: 9999, zIndex: "var(--zIndex-layerUI)",
backgroundColor: "var(--popup-bg-color)", backgroundColor: "var(--popup-bg-color)",
maxWidth: "208px", maxWidth: "208px",
maxHeight: window.innerHeight, maxHeight: window.innerHeight,
@ -152,7 +160,7 @@ const ColorPickerPopupContent = ({
"0px 7px 14px rgba(0, 0, 0, 0.05), 0px 0px 3.12708px rgba(0, 0, 0, 0.0798), 0px 0px 0.931014px rgba(0, 0, 0, 0.1702)", "0px 7px 14px rgba(0, 0, 0, 0.05), 0px 0px 3.12708px rgba(0, 0, 0, 0.0798), 0px 0px 0.931014px rgba(0, 0, 0, 0.1702)",
}} }}
> >
{palette ? ( {palette && eyeDropperType ? (
<Picker <Picker
palette={palette} palette={palette}
color={color} color={color}
@ -165,6 +173,7 @@ const ColorPickerPopupContent = ({
state = state || { state = state || {
keepOpenOnAlt: true, keepOpenOnAlt: true,
onSelect: onChange, onSelect: onChange,
previewType: eyeDropperType,
}; };
state.keepOpenOnAlt = true; state.keepOpenOnAlt = true;
return state; return state;
@ -175,6 +184,7 @@ const ColorPickerPopupContent = ({
: { : {
keepOpenOnAlt: false, keepOpenOnAlt: false,
onSelect: onChange, onSelect: onChange,
previewType: eyeDropperType,
}; };
}); });
}} }}

View File

@ -4,7 +4,7 @@
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 2; z-index: var(--zIndex-eyeDropperBackdrop);
touch-action: none; touch-action: none;
} }
@ -21,7 +21,7 @@
width: 3rem; width: 3rem;
height: 3rem; height: 3rem;
position: fixed; position: fixed;
z-index: 999999; z-index: var(--zIndex-eyeDropperPreview);
border-radius: 1rem; border-radius: 1rem;
border: 1px solid var(--default-border-color); border: 1px solid var(--default-border-color);
filter: var(--theme-filter); filter: var(--theme-filter);

View File

@ -1,7 +1,7 @@
import { atom } from "jotai"; import { atom } from "jotai";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { COLOR_PALETTE, rgbToHex } from "../colors"; import { rgbToHex } from "../colors";
import { EVENT } from "../constants"; import { EVENT } from "../constants";
import { useUIAppState } from "../context/ui-appState"; import { useUIAppState } from "../context/ui-appState";
import { mutateElement } from "../element/mutateElement"; import { mutateElement } from "../element/mutateElement";
@ -18,8 +18,8 @@ import "./EyeDropper.scss";
type EyeDropperProperties = { type EyeDropperProperties = {
keepOpenOnAlt: boolean; keepOpenOnAlt: boolean;
swapPreviewOnAlt?: boolean; swapPreviewOnAlt?: boolean;
onSelect?: (color: string, event: PointerEvent) => void; onSelect: (color: string, event: PointerEvent) => void;
previewType?: "strokeColor" | "backgroundColor"; previewType: "strokeColor" | "backgroundColor";
}; };
export const activeEyeDropperAtom = atom<null | EyeDropperProperties>(null); export const activeEyeDropperAtom = atom<null | EyeDropperProperties>(null);
@ -28,13 +28,8 @@ export const EyeDropper: React.FC<{
onCancel: () => void; onCancel: () => void;
onSelect: Required<EyeDropperProperties>["onSelect"]; onSelect: Required<EyeDropperProperties>["onSelect"];
swapPreviewOnAlt?: EyeDropperProperties["swapPreviewOnAlt"]; swapPreviewOnAlt?: EyeDropperProperties["swapPreviewOnAlt"];
previewType?: EyeDropperProperties["previewType"]; previewType: EyeDropperProperties["previewType"];
}> = ({ }> = ({ onCancel, onSelect, swapPreviewOnAlt, previewType }) => {
onCancel,
onSelect,
swapPreviewOnAlt,
previewType = "backgroundColor",
}) => {
const eyeDropperContainer = useCreatePortalContainer({ const eyeDropperContainer = useCreatePortalContainer({
className: "excalidraw-eye-dropper-backdrop", className: "excalidraw-eye-dropper-backdrop",
parentSelector: ".excalidraw-eye-dropper-container", parentSelector: ".excalidraw-eye-dropper-container",
@ -58,11 +53,27 @@ export const EyeDropper: React.FC<{
return; return;
} }
let currentColor: string = COLOR_PALETTE.black;
let isHoldingPointerDown = false; let isHoldingPointerDown = false;
const ctx = app.canvas.getContext("2d")!; const ctx = app.canvas.getContext("2d")!;
const getCurrentColor = ({
clientX,
clientY,
}: {
clientX: number;
clientY: number;
}) => {
const pixel = ctx.getImageData(
(clientX - appState.offsetLeft) * window.devicePixelRatio,
(clientY - appState.offsetTop) * window.devicePixelRatio,
1,
1,
).data;
return rgbToHex(pixel[0], pixel[1], pixel[2]);
};
const mouseMoveListener = ({ const mouseMoveListener = ({
clientX, clientX,
clientY, clientY,
@ -76,14 +87,7 @@ export const EyeDropper: React.FC<{
colorPreviewDiv.style.top = `${clientY + 20}px`; colorPreviewDiv.style.top = `${clientY + 20}px`;
colorPreviewDiv.style.left = `${clientX + 20}px`; colorPreviewDiv.style.left = `${clientX + 20}px`;
const pixel = ctx.getImageData( const currentColor = getCurrentColor({ clientX, clientY });
(clientX - appState.offsetLeft) * window.devicePixelRatio,
(clientY - appState.offsetTop) * window.devicePixelRatio,
1,
1,
).data;
currentColor = rgbToHex(pixel[0], pixel[1], pixel[2]);
if (isHoldingPointerDown) { if (isHoldingPointerDown) {
for (const element of metaStuffRef.current.selectedElements) { for (const element of metaStuffRef.current.selectedElements) {
@ -125,7 +129,7 @@ export const EyeDropper: React.FC<{
event.stopImmediatePropagation(); event.stopImmediatePropagation();
event.preventDefault(); event.preventDefault();
onSelect(currentColor, event); onSelect(getCurrentColor(event), event);
}; };
const keyDownListener = (event: KeyboardEvent) => { const keyDownListener = (event: KeyboardEvent) => {

View File

@ -25,10 +25,10 @@
} }
.default-sidebar-trigger .sidebar-trigger__label { .default-sidebar-trigger .sidebar-trigger__label {
display: none; display: block;
}
@media screen and (min-width: 1024px) { &.excalidraw--mobile .default-sidebar-trigger .sidebar-trigger__label {
display: block; display: none;
}
} }
} }

View File

@ -6,6 +6,8 @@
--zIndex-interactiveCanvas: 2; --zIndex-interactiveCanvas: 2;
--zIndex-wysiwyg: 3; --zIndex-wysiwyg: 3;
--zIndex-layerUI: 4; --zIndex-layerUI: 4;
--zIndex-eyeDropperBackdrop: 5;
--zIndex-eyeDropperPreview: 6;
--zIndex-modal: 1000; --zIndex-modal: 1000;
--zIndex-popup: 1001; --zIndex-popup: 1001;

View File

@ -121,6 +121,7 @@ export const isBindableElement = (
element.type === "ellipse" || element.type === "ellipse" ||
element.type === "image" || element.type === "image" ||
element.type === "embeddable" || element.type === "embeddable" ||
element.type === "frame" ||
(element.type === "text" && !element.containerId)) (element.type === "text" && !element.containerId))
); );
}; };

View File

@ -1,11 +1,13 @@
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { import {
createPasteEvent, createPasteEvent,
fireEvent,
GlobalTestState, GlobalTestState,
render, render,
screen,
waitFor, waitFor,
} from "./test-utils"; } from "./test-utils";
import { UI, Pointer } from "./helpers/ui"; import { UI, Pointer, Keyboard } from "./helpers/ui";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { actionFlipHorizontal, actionFlipVertical } from "../actions"; import { actionFlipHorizontal, actionFlipVertical } from "../actions";
import { getElementAbsoluteCoords } from "../element"; import { getElementAbsoluteCoords } from "../element";
@ -13,6 +15,7 @@ import {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawImageElement, ExcalidrawImageElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
ExcalidrawTextElementWithContainer,
FileId, FileId,
} from "../element/types"; } from "../element/types";
import { newLinearElement } from "../element"; import { newLinearElement } from "../element";
@ -22,6 +25,8 @@ import { NormalizedZoomValue } from "../types";
import { ROUNDNESS } from "../constants"; import { ROUNDNESS } from "../constants";
import { vi } from "vitest"; import { vi } from "vitest";
import * as blob from "../data/blob"; import * as blob from "../data/blob";
import { KEYS } from "../keys";
import { getBoundTextElementPosition } from "../element/textElement";
const { h } = window; const { h } = window;
const mouse = new Pointer("mouse"); const mouse = new Pointer("mouse");
@ -812,3 +817,69 @@ describe("image", () => {
expect(h.elements[0].angle).toBeCloseTo(0); expect(h.elements[0].angle).toBeCloseTo(0);
}); });
}); });
describe("mutliple elements", () => {
it("with bound text flip correctly", async () => {
UI.clickTool("arrow");
fireEvent.click(screen.getByTitle("Architect"));
const arrow = UI.createElement("arrow", {
x: 0,
y: 0,
width: 180,
height: 80,
});
Keyboard.keyPress(KEYS.ENTER);
let editor = document.querySelector<HTMLTextAreaElement>(
".excalidraw-textEditorContainer > textarea",
)!;
fireEvent.input(editor, { target: { value: "arrow" } });
await new Promise((resolve) => setTimeout(resolve, 0));
Keyboard.keyPress(KEYS.ESCAPE);
const rectangle = UI.createElement("rectangle", {
x: 0,
y: 100,
width: 100,
height: 100,
});
Keyboard.keyPress(KEYS.ENTER);
editor = document.querySelector<HTMLTextAreaElement>(
".excalidraw-textEditorContainer > textarea",
)!;
fireEvent.input(editor, { target: { value: "rect\ntext" } });
await new Promise((resolve) => setTimeout(resolve, 0));
Keyboard.keyPress(KEYS.ESCAPE);
mouse.select([arrow, rectangle]);
h.app.actionManager.executeAction(actionFlipHorizontal);
h.app.actionManager.executeAction(actionFlipVertical);
const arrowText = h.elements[1] as ExcalidrawTextElementWithContainer;
const arrowTextPos = getBoundTextElementPosition(arrow.get(), arrowText)!;
const rectText = h.elements[3] as ExcalidrawTextElementWithContainer;
expect(arrow.x).toBeCloseTo(180);
expect(arrow.y).toBeCloseTo(200);
expect(arrow.points[1][0]).toBeCloseTo(-180);
expect(arrow.points[1][1]).toBeCloseTo(-80);
expect(arrowTextPos.x - (arrow.x - arrow.width)).toBeCloseTo(
arrow.x - (arrowTextPos.x + arrowText.width),
);
expect(arrowTextPos.y - (arrow.y - arrow.height)).toBeCloseTo(
arrow.y - (arrowTextPos.y + arrowText.height),
);
expect(rectangle.x).toBeCloseTo(80);
expect(rectangle.y).toBeCloseTo(0);
expect(rectText.x - rectangle.x).toBeCloseTo(
rectangle.x + rectangle.width - (rectText.x + rectText.width),
);
expect(rectText.y - rectangle.y).toBeCloseTo(
rectangle.y + rectangle.height - (rectText.y + rectText.height),
);
});
});

View File

@ -518,8 +518,9 @@ export type AppProps = Merge<
* in the app, eg Manager. Factored out into a separate type to keep DRY. */ * in the app, eg Manager. Factored out into a separate type to keep DRY. */
export type AppClassProperties = { export type AppClassProperties = {
props: AppProps; props: AppProps;
canvas: HTMLCanvasElement;
interactiveCanvas: HTMLCanvasElement | null; interactiveCanvas: HTMLCanvasElement | null;
/** static canvas */
canvas: HTMLCanvasElement;
focusContainer(): void; focusContainer(): void;
library: Library; library: Library;
imageCache: Map< imageCache: Map<