From 416e8b3e421971bf2162cb78a2dee7bbc6dabb23 Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Fri, 10 Oct 2025 08:48:31 +1100 Subject: [PATCH] feat: new mobile layout (#9996) * compact bottom toolbar * put menu trigger to top left * add popup to switch between grouped tool types * add a dedicated mobile toolbar * update position for mobile * fix active tool type * add mobile mode as well * mobile actions * remove refactored popups * excali logo mobile * include mobile * update mobile menu layout * move selection and deletion back to right * do not fill eraser * fix styling * fix active styling * bigger buttons, smaller gaps * fix other tools not opened * fix: Style panel persistence and restore Signed-off-by: Mark Tolmacs * move hidden action btns to extra popover * fix dropdown overlapping with welcome screen * replace custom popup with popover * improve button styles * swapping redo and delete * always show undo & redo and improve styling * change background * toolbar styles * no any * persist perferred selection tool and align tablet as well * add a renderTopLeftUI to props * tweak border and bg * show combined properties only when using suitable tools * fix preferred tool * new stroke icon * hide color picker hot keys * init preferred tool based on device * fix main menu sizing * fix welcome screen offset * put text before image * disable call highlight on buttons * fix renderTopLeftUI --------- Signed-off-by: Mark Tolmacs Co-authored-by: Mark Tolmacs Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- packages/common/src/constants.ts | 5 + packages/element/src/comparisons.ts | 8 +- packages/excalidraw/actions/actionCanvas.tsx | 9 +- .../actions/actionDeleteSelected.tsx | 18 +- .../actions/actionDuplicateSelection.tsx | 11 +- .../excalidraw/actions/actionFinalize.tsx | 4 +- packages/excalidraw/actions/actionHistory.tsx | 22 +- .../excalidraw/actions/actionProperties.tsx | 47 +- packages/excalidraw/appState.ts | 7 +- packages/excalidraw/components/Actions.scss | 70 +- packages/excalidraw/components/Actions.tsx | 1067 +++++++++++------ packages/excalidraw/components/App.tsx | 57 +- .../components/ColorPicker/ColorPicker.scss | 15 + .../components/ColorPicker/ColorPicker.tsx | 44 +- .../components/ColorPicker/Picker.tsx | 22 +- .../ColorPicker/PickerColorList.tsx | 4 +- .../components/ColorPicker/ShadeList.tsx | 12 +- .../excalidraw/components/ExcalidrawLogo.scss | 14 + .../excalidraw/components/ExcalidrawLogo.tsx | 2 +- .../components/FontPicker/FontPicker.tsx | 1 + .../components/FontPicker/FontPickerList.tsx | 12 +- .../FontPicker/FontPickerTrigger.tsx | 13 + packages/excalidraw/components/HandButton.tsx | 2 +- packages/excalidraw/components/IconPicker.tsx | 10 +- packages/excalidraw/components/LayerUI.tsx | 8 +- packages/excalidraw/components/MobileMenu.tsx | 209 ++-- .../excalidraw/components/MobileToolBar.scss | 78 ++ .../excalidraw/components/MobileToolBar.tsx | 471 ++++++++ .../excalidraw/components/ToolPopover.scss | 18 + .../excalidraw/components/ToolPopover.tsx | 120 ++ packages/excalidraw/components/Toolbar.scss | 4 + .../components/dropdownMenu/DropdownMenu.scss | 30 +- .../components/dropdownMenu/DropdownMenu.tsx | 13 +- .../dropdownMenu/DropdownMenuContent.tsx | 3 + packages/excalidraw/components/icons.tsx | 14 +- .../components/main-menu/MainMenu.tsx | 2 + packages/excalidraw/components/shapes.tsx | 2 +- .../welcome-screen/WelcomeScreen.scss | 10 +- packages/excalidraw/css/styles.scss | 46 +- packages/excalidraw/css/theme.scss | 9 + packages/excalidraw/css/variables.module.scss | 16 + packages/excalidraw/index.tsx | 2 + .../__snapshots__/contextmenu.test.tsx.snap | 68 ++ .../tests/__snapshots__/history.test.tsx.snap | 258 +++- .../regressionTests.test.tsx.snap | 210 +++- packages/excalidraw/types.ts | 14 +- .../tests/__snapshots__/export.test.ts.snap | 4 + 47 files changed, 2407 insertions(+), 678 deletions(-) create mode 100644 packages/excalidraw/components/MobileToolBar.scss create mode 100644 packages/excalidraw/components/MobileToolBar.tsx create mode 100644 packages/excalidraw/components/ToolPopover.scss create mode 100644 packages/excalidraw/components/ToolPopover.tsx diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 3ac7a52b93..dfbb69aa97 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -543,3 +543,8 @@ export enum UserIdleState { export const LINE_POLYGON_POINT_MERGE_DISTANCE = 20; export const DOUBLE_TAP_POSITION_THRESHOLD = 35; + +// glass background for mobile action buttons +export const MOBILE_ACTION_BUTTON_BG = { + background: "var(--mobile-action-button-bg)", +} as const; diff --git a/packages/element/src/comparisons.ts b/packages/element/src/comparisons.ts index 75fac889dc..c15e1ca4bc 100644 --- a/packages/element/src/comparisons.ts +++ b/packages/element/src/comparisons.ts @@ -10,7 +10,13 @@ export const hasBackground = (type: ElementOrToolType) => type === "freedraw"; export const hasStrokeColor = (type: ElementOrToolType) => - type !== "image" && type !== "frame" && type !== "magicframe"; + type === "rectangle" || + type === "ellipse" || + type === "diamond" || + type === "freedraw" || + type === "arrow" || + type === "line" || + type === "text"; export const hasStrokeWidth = (type: ElementOrToolType) => type === "rectangle" || diff --git a/packages/excalidraw/actions/actionCanvas.tsx b/packages/excalidraw/actions/actionCanvas.tsx index d0039d1c29..b4aac19059 100644 --- a/packages/excalidraw/actions/actionCanvas.tsx +++ b/packages/excalidraw/actions/actionCanvas.tsx @@ -122,7 +122,10 @@ export const actionClearCanvas = register({ pasteDialog: appState.pasteDialog, activeTool: appState.activeTool.type === "image" - ? { ...appState.activeTool, type: app.defaultSelectionTool } + ? { + ...appState.activeTool, + type: app.state.preferredSelectionTool.type, + } : appState.activeTool, }, captureUpdate: CaptureUpdateAction.IMMEDIATELY, @@ -501,7 +504,7 @@ export const actionToggleEraserTool = register({ if (isEraserActive(appState)) { activeTool = updateActiveTool(appState, { ...(appState.activeTool.lastActiveTool || { - type: app.defaultSelectionTool, + type: app.state.preferredSelectionTool.type, }), lastActiveToolBeforeEraser: null, }); @@ -532,7 +535,7 @@ export const actionToggleLassoTool = register({ icon: LassoIcon, trackEvent: { category: "toolbar" }, predicate: (elements, appState, props, app) => { - return app.defaultSelectionTool !== "lasso"; + return app.state.preferredSelectionTool.type !== "lasso"; }, perform: (elements, appState, _, app) => { let activeTool: AppState["activeTool"]; diff --git a/packages/excalidraw/actions/actionDeleteSelected.tsx b/packages/excalidraw/actions/actionDeleteSelected.tsx index 78a3465689..694f02b90c 100644 --- a/packages/excalidraw/actions/actionDeleteSelected.tsx +++ b/packages/excalidraw/actions/actionDeleteSelected.tsx @@ -1,4 +1,8 @@ -import { KEYS, updateActiveTool } from "@excalidraw/common"; +import { + KEYS, + MOBILE_ACTION_BUTTON_BG, + updateActiveTool, +} from "@excalidraw/common"; import { getNonDeletedElements } from "@excalidraw/element"; import { fixBindingsAfterDeletion } from "@excalidraw/element"; @@ -299,7 +303,7 @@ export const actionDeleteSelected = register({ appState: { ...nextAppState, activeTool: updateActiveTool(appState, { - type: app.defaultSelectionTool, + type: app.state.preferredSelectionTool.type, }), multiElement: null, activeEmbeddable: null, @@ -323,7 +327,15 @@ export const actionDeleteSelected = register({ title={t("labels.delete")} aria-label={t("labels.delete")} onClick={() => updateData(null)} - visible={isSomeElementSelected(getNonDeletedElements(elements), appState)} + disabled={ + !isSomeElementSelected(getNonDeletedElements(elements), appState) + } + style={{ + ...(appState.stylesPanelMode === "mobile" && + appState.openPopup !== "compactOtherProperties" + ? MOBILE_ACTION_BUTTON_BG + : {}), + }} /> ), }); diff --git a/packages/excalidraw/actions/actionDuplicateSelection.tsx b/packages/excalidraw/actions/actionDuplicateSelection.tsx index c1b2a9da42..daf1dbb3c6 100644 --- a/packages/excalidraw/actions/actionDuplicateSelection.tsx +++ b/packages/excalidraw/actions/actionDuplicateSelection.tsx @@ -1,6 +1,7 @@ import { DEFAULT_GRID_SIZE, KEYS, + MOBILE_ACTION_BUTTON_BG, arrayToMap, getShortcutKey, } from "@excalidraw/common"; @@ -115,7 +116,15 @@ export const actionDuplicateSelection = register({ )}`} aria-label={t("labels.duplicateSelection")} onClick={() => updateData(null)} - visible={isSomeElementSelected(getNonDeletedElements(elements), appState)} + disabled={ + !isSomeElementSelected(getNonDeletedElements(elements), appState) + } + style={{ + ...(appState.stylesPanelMode === "mobile" && + appState.openPopup !== "compactOtherProperties" + ? MOBILE_ACTION_BUTTON_BG + : {}), + }} /> ), }); diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index 877c817ad4..4e7ae67919 100644 --- a/packages/excalidraw/actions/actionFinalize.tsx +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -261,13 +261,13 @@ export const actionFinalize = register({ if (appState.activeTool.type === "eraser") { activeTool = updateActiveTool(appState, { ...(appState.activeTool.lastActiveTool || { - type: app.defaultSelectionTool, + type: app.state.preferredSelectionTool.type, }), lastActiveToolBeforeEraser: null, }); } else { activeTool = updateActiveTool(appState, { - type: app.defaultSelectionTool, + type: app.state.preferredSelectionTool.type, }); } diff --git a/packages/excalidraw/actions/actionHistory.tsx b/packages/excalidraw/actions/actionHistory.tsx index b948fe7d49..a1971f527c 100644 --- a/packages/excalidraw/actions/actionHistory.tsx +++ b/packages/excalidraw/actions/actionHistory.tsx @@ -1,4 +1,10 @@ -import { isWindows, KEYS, matchKey, arrayToMap } from "@excalidraw/common"; +import { + isWindows, + KEYS, + matchKey, + arrayToMap, + MOBILE_ACTION_BUTTON_BG, +} from "@excalidraw/common"; import { CaptureUpdateAction } from "@excalidraw/element"; @@ -67,7 +73,7 @@ export const createUndoAction: ActionCreator = (history) => ({ ), keyTest: (event) => event[KEYS.CTRL_OR_CMD] && matchKey(event, KEYS.Z) && !event.shiftKey, - PanelComponent: ({ updateData, data }) => { + PanelComponent: ({ appState, updateData, data }) => { const { isUndoStackEmpty } = useEmitter( history.onHistoryChangedEmitter, new HistoryChangedEvent( @@ -85,6 +91,11 @@ export const createUndoAction: ActionCreator = (history) => ({ size={data?.size || "medium"} disabled={isUndoStackEmpty} data-testid="button-undo" + style={{ + ...(appState.stylesPanelMode === "mobile" + ? MOBILE_ACTION_BUTTON_BG + : {}), + }} /> ); }, @@ -103,7 +114,7 @@ export const createRedoAction: ActionCreator = (history) => ({ keyTest: (event) => (event[KEYS.CTRL_OR_CMD] && event.shiftKey && matchKey(event, KEYS.Z)) || (isWindows && event.ctrlKey && !event.shiftKey && matchKey(event, KEYS.Y)), - PanelComponent: ({ updateData, data }) => { + PanelComponent: ({ appState, updateData, data }) => { const { isRedoStackEmpty } = useEmitter( history.onHistoryChangedEmitter, new HistoryChangedEvent( @@ -121,6 +132,11 @@ export const createRedoAction: ActionCreator = (history) => ({ size={data?.size || "medium"} disabled={isRedoStackEmpty} data-testid="button-redo" + style={{ + ...(appState.stylesPanelMode === "mobile" + ? MOBILE_ACTION_BUTTON_BG + : {}), + }} /> ); }, diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index c03309e9cc..229b492533 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -348,7 +348,10 @@ export const actionChangeStrokeColor = register({ elements={elements} appState={appState} updateData={updateData} - compactMode={appState.stylesPanelMode === "compact"} + compactMode={ + appState.stylesPanelMode === "compact" || + appState.stylesPanelMode === "mobile" + } /> ), @@ -428,7 +431,10 @@ export const actionChangeBackgroundColor = register({ elements={elements} appState={appState} updateData={updateData} - compactMode={appState.stylesPanelMode === "compact"} + compactMode={ + appState.stylesPanelMode === "compact" || + appState.stylesPanelMode === "mobile" + } /> ), @@ -531,9 +537,7 @@ export const actionChangeStrokeWidth = register({ }, PanelComponent: ({ elements, appState, updateData, app, data }) => (
- {appState.stylesPanelMode === "full" && ( - {t("labels.strokeWidth")} - )} + {t("labels.strokeWidth")}
(
- {appState.stylesPanelMode === "full" && ( - {t("labels.sloppiness")} - )} + {t("labels.sloppiness")}
(
- {appState.stylesPanelMode === "full" && ( - {t("labels.strokeStyle")} - )} + {t("labels.strokeStyle")}
{ withCaretPositionPreservation( () => updateData(value), - appState.stylesPanelMode === "compact", + appState.stylesPanelMode === "compact" || + appState.stylesPanelMode === "mobile", !!appState.editingTextElement, data?.onPreventClose, ); @@ -1040,7 +1041,7 @@ export const actionChangeFontFamily = register({ return result; }, - PanelComponent: ({ elements, appState, app, updateData, data }) => { + PanelComponent: ({ elements, appState, app, updateData }) => { const cachedElementsRef = useRef(new Map()); const prevSelectedFontFamilyRef = useRef(null); // relying on state batching as multiple `FontPicker` handlers could be called in rapid succession and we want to combine them @@ -1117,7 +1118,7 @@ export const actionChangeFontFamily = register({ }, []); return ( -
+ <> {appState.stylesPanelMode === "full" && ( {t("labels.fontFamily")} )} @@ -1125,7 +1126,7 @@ export const actionChangeFontFamily = register({ isOpened={appState.openPopup === "fontFamily"} selectedFontFamily={selectedFontFamily} hoveredFontFamily={appState.currentHoveredFontFamily} - compactMode={appState.stylesPanelMode === "compact"} + compactMode={appState.stylesPanelMode !== "full"} onSelect={(fontFamily) => { withCaretPositionPreservation( () => { @@ -1137,7 +1138,8 @@ export const actionChangeFontFamily = register({ // defensive clear so immediate close won't abuse the cached elements cachedElementsRef.current.clear(); }, - appState.stylesPanelMode === "compact", + appState.stylesPanelMode === "compact" || + appState.stylesPanelMode === "mobile", !!appState.editingTextElement, ); }} @@ -1213,7 +1215,8 @@ export const actionChangeFontFamily = register({ // Refocus text editor when font picker closes if we were editing text if ( - appState.stylesPanelMode === "compact" && + (appState.stylesPanelMode === "compact" || + appState.stylesPanelMode === "mobile") && appState.editingTextElement ) { restoreCaretPosition(null); // Just refocus without saved position @@ -1221,7 +1224,7 @@ export const actionChangeFontFamily = register({ } }} /> -
+ ); }, }); @@ -1314,7 +1317,8 @@ export const actionChangeTextAlign = register({ onChange={(value) => { withCaretPositionPreservation( () => updateData(value), - appState.stylesPanelMode === "compact", + appState.stylesPanelMode === "compact" || + appState.stylesPanelMode === "mobile", !!appState.editingTextElement, data?.onPreventClose, ); @@ -1413,7 +1417,8 @@ export const actionChangeVerticalAlign = register({ onChange={(value) => { withCaretPositionPreservation( () => updateData(value), - appState.stylesPanelMode === "compact", + appState.stylesPanelMode === "compact" || + appState.stylesPanelMode === "mobile", !!appState.editingTextElement, data?.onPreventClose, ); @@ -1678,8 +1683,8 @@ export const actionChangeArrowProperties = register({ PanelComponent: ({ elements, appState, updateData, app, renderAction }) => { return (
- {renderAction("changeArrowType")} {renderAction("changeArrowhead")} + {renderAction("changeArrowType")}
); }, diff --git a/packages/excalidraw/appState.ts b/packages/excalidraw/appState.ts index 2a37b138d8..96876e5854 100644 --- a/packages/excalidraw/appState.ts +++ b/packages/excalidraw/appState.ts @@ -55,6 +55,10 @@ export const getDefaultAppState = (): Omit< fromSelection: false, lastActiveTool: null, }, + preferredSelectionTool: { + type: "selection", + initialized: false, + }, penMode: false, penDetected: false, errorMessage: null, @@ -176,6 +180,7 @@ const APP_STATE_STORAGE_CONF = (< editingTextElement: { browser: false, export: false, server: false }, editingGroupId: { browser: true, export: false, server: false }, activeTool: { browser: true, export: false, server: false }, + preferredSelectionTool: { browser: true, export: false, server: false }, penMode: { browser: true, export: false, server: false }, penDetected: { browser: true, export: false, server: false }, errorMessage: { browser: false, export: false, server: false }, @@ -248,7 +253,7 @@ const APP_STATE_STORAGE_CONF = (< searchMatches: { browser: false, export: false, server: false }, lockedMultiSelections: { browser: true, export: true, server: true }, activeLockedId: { browser: false, export: false, server: false }, - stylesPanelMode: { browser: true, export: false, server: false }, + stylesPanelMode: { browser: false, export: false, server: false }, }); const _clearAppStateForStorage = < diff --git a/packages/excalidraw/components/Actions.scss b/packages/excalidraw/components/Actions.scss index 93b5ef7c3e..f97f3c7b6f 100644 --- a/packages/excalidraw/components/Actions.scss +++ b/packages/excalidraw/components/Actions.scss @@ -106,15 +106,15 @@ justify-content: center; align-items: center; min-height: 2.5rem; + pointer-events: auto; --default-button-size: 2rem; .compact-action-button { - width: 2rem; - height: 2rem; + width: var(--mobile-action-button-size); + height: var(--mobile-action-button-size); border: none; border-radius: var(--border-radius-lg); - background: transparent; color: var(--color-on-surface); cursor: pointer; display: flex; @@ -122,24 +122,20 @@ justify-content: center; transition: all 0.2s ease; + background: var(--mobile-action-button-bg); + svg { width: 1rem; height: 1rem; flex: 0 0 auto; } - &:hover { - background: var(--button-hover-bg, var(--island-bg-color)); - border-color: var( - --button-hover-border, - var(--button-border, var(--default-border-color)) + &.active { + background: var( + --color-surface-primary-container, + var(--mobile-action-button-bg) ); } - - &:active { - background: var(--button-active-bg, var(--island-bg-color)); - border-color: var(--button-active-border, var(--color-primary-darkest)); - } } .compact-popover-content { @@ -167,6 +163,19 @@ } } } + + .ToolIcon { + .ToolIcon__icon { + width: var(--mobile-action-button-size); + height: var(--mobile-action-button-size); + + background: var(--mobile-action-button-bg); + + &:hover { + background-color: transparent; + } + } + } } .compact-shape-actions-island { @@ -174,29 +183,18 @@ overflow-x: hidden; } -.compact-popover-content { - .popover-section { - margin-bottom: 1rem; - - &:last-child { - margin-bottom: 0; - } - - .popover-section-title { - font-size: 0.75rem; - font-weight: 600; - color: var(--color-text-secondary); - margin-bottom: 0.5rem; - text-transform: uppercase; - letter-spacing: 0.5px; - } - - .buttonList { - display: flex; - flex-wrap: wrap; - gap: 0.25rem; - } - } +.mobile-shape-actions { + z-index: 999; + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; + background: transparent; + border-radius: var(--border-radius-lg); + box-shadow: none; + overflow: none; + scrollbar-width: none; + -ms-overflow-style: none; } .shape-actions-theme-scope { diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx index ae44dafd04..ec95d40c3e 100644 --- a/packages/excalidraw/components/Actions.tsx +++ b/packages/excalidraw/components/Actions.tsx @@ -1,5 +1,5 @@ import clsx from "clsx"; -import { useState } from "react"; +import { useRef, useState } from "react"; import * as Popover from "@radix-ui/react-popover"; import { @@ -56,6 +56,7 @@ import "./Actions.scss"; import { useDevice, useExcalidrawContainer } from "./App"; import Stack from "./Stack"; import { ToolButton } from "./ToolButton"; +import { ToolPopover } from "./ToolPopover"; import { Tooltip } from "./Tooltip"; import DropdownMenu from "./dropdownMenu/DropdownMenu"; import { PropertiesPopover } from "./PropertiesPopover"; @@ -73,8 +74,11 @@ import { TextSizeIcon, adjustmentsIcon, DotsHorizontalIcon, + SelectionIcon, } from "./icons"; +import { Island } from "./Island"; + import type { AppClassProperties, AppProps, @@ -302,6 +306,475 @@ export const SelectedShapeActions = ({ ); }; +const CombinedShapeProperties = ({ + appState, + renderAction, + setAppState, + targetElements, + container, +}: { + targetElements: ExcalidrawElement[]; + appState: UIAppState; + renderAction: ActionManager["renderAction"]; + setAppState: React.Component["setState"]; + container: HTMLDivElement | null; +}) => { + const showFillIcons = + (hasBackground(appState.activeTool.type) && + !isTransparent(appState.currentItemBackgroundColor)) || + targetElements.some( + (element) => + hasBackground(element.type) && !isTransparent(element.backgroundColor), + ); + + const shouldShowCombinedProperties = + targetElements.length > 0 || + (appState.activeTool.type !== "selection" && + appState.activeTool.type !== "eraser" && + appState.activeTool.type !== "hand" && + appState.activeTool.type !== "laser" && + appState.activeTool.type !== "lasso"); + const isOpen = appState.openPopup === "compactStrokeStyles"; + + if (!shouldShowCombinedProperties) { + return null; + } + + return ( +
+ { + if (open) { + setAppState({ openPopup: "compactStrokeStyles" }); + } else { + setAppState({ openPopup: null }); + } + }} + > + + + + {isOpen && ( + {}} + > +
+ {showFillIcons && renderAction("changeFillStyle")} + {(hasStrokeWidth(appState.activeTool.type) || + targetElements.some((element) => + hasStrokeWidth(element.type), + )) && + renderAction("changeStrokeWidth")} + {(hasStrokeStyle(appState.activeTool.type) || + targetElements.some((element) => + hasStrokeStyle(element.type), + )) && ( + <> + {renderAction("changeStrokeStyle")} + {renderAction("changeSloppiness")} + + )} + {(canChangeRoundness(appState.activeTool.type) || + targetElements.some((element) => + canChangeRoundness(element.type), + )) && + renderAction("changeRoundness")} + {renderAction("changeOpacity")} +
+
+ )} +
+
+ ); +}; + +const CombinedArrowProperties = ({ + appState, + renderAction, + setAppState, + targetElements, + container, + app, +}: { + targetElements: ExcalidrawElement[]; + appState: UIAppState; + renderAction: ActionManager["renderAction"]; + setAppState: React.Component["setState"]; + container: HTMLDivElement | null; + app: AppClassProperties; +}) => { + const showShowArrowProperties = + toolIsArrow(appState.activeTool.type) || + targetElements.some((element) => toolIsArrow(element.type)); + const isOpen = appState.openPopup === "compactArrowProperties"; + + if (!showShowArrowProperties) { + return null; + } + + return ( +
+ { + if (open) { + setAppState({ openPopup: "compactArrowProperties" }); + } else { + setAppState({ openPopup: null }); + } + }} + > + + + + {isOpen && ( + {}} + > + {renderAction("changeArrowProperties")} + + )} + +
+ ); +}; + +const CombinedTextProperties = ({ + appState, + renderAction, + setAppState, + targetElements, + container, + elementsMap, +}: { + appState: UIAppState; + renderAction: ActionManager["renderAction"]; + setAppState: React.Component["setState"]; + targetElements: ExcalidrawElement[]; + container: HTMLDivElement | null; + elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap; +}) => { + const { saveCaretPosition, restoreCaretPosition } = useTextEditorFocus(); + const isOpen = appState.openPopup === "compactTextProperties"; + + return ( +
+ { + if (open) { + if (appState.editingTextElement) { + saveCaretPosition(); + } + setAppState({ openPopup: "compactTextProperties" }); + } else { + setAppState({ openPopup: null }); + if (appState.editingTextElement) { + restoreCaretPosition(); + } + } + }} + > + + + + {appState.openPopup === "compactTextProperties" && ( + { + // Refocus text editor when popover closes with caret restoration + if (appState.editingTextElement) { + restoreCaretPosition(); + } + }} + > +
+ {(appState.activeTool.type === "text" || + targetElements.some(isTextElement)) && + renderAction("changeFontSize")} + {(appState.activeTool.type === "text" || + suppportsHorizontalAlign(targetElements, elementsMap)) && + renderAction("changeTextAlign")} + {shouldAllowVerticalAlign(targetElements, elementsMap) && + renderAction("changeVerticalAlign")} +
+
+ )} +
+
+ ); +}; + +const CombinedExtraActions = ({ + appState, + renderAction, + targetElements, + setAppState, + container, + app, + showDuplicate, + showDelete, +}: { + appState: UIAppState; + targetElements: ExcalidrawElement[]; + renderAction: ActionManager["renderAction"]; + setAppState: React.Component["setState"]; + container: HTMLDivElement | null; + app: AppClassProperties; + showDuplicate?: boolean; + showDelete?: boolean; +}) => { + const isEditingTextOrNewElement = Boolean( + appState.editingTextElement || appState.newElement, + ); + const showCropEditorAction = + !appState.croppingElementId && + targetElements.length === 1 && + isImageElement(targetElements[0]); + const showLinkIcon = targetElements.length === 1; + const showAlignActions = alignActionsPredicate(appState, app); + let isSingleElementBoundContainer = false; + if ( + targetElements.length === 2 && + (hasBoundTextElement(targetElements[0]) || + hasBoundTextElement(targetElements[1])) + ) { + isSingleElementBoundContainer = true; + } + + const isRTL = document.documentElement.getAttribute("dir") === "rtl"; + const isOpen = appState.openPopup === "compactOtherProperties"; + + if (isEditingTextOrNewElement || targetElements.length === 0) { + return null; + } + + return ( +
+ { + if (open) { + setAppState({ openPopup: "compactOtherProperties" }); + } else { + setAppState({ openPopup: null }); + } + }} + > + + + + {isOpen && ( + {}} + > +
+
+ {t("labels.layers")} +
+ {renderAction("sendToBack")} + {renderAction("sendBackward")} + {renderAction("bringForward")} + {renderAction("bringToFront")} +
+
+ + {showAlignActions && !isSingleElementBoundContainer && ( +
+ {t("labels.align")} +
+ {isRTL ? ( + <> + {renderAction("alignRight")} + {renderAction("alignHorizontallyCentered")} + {renderAction("alignLeft")} + + ) : ( + <> + {renderAction("alignLeft")} + {renderAction("alignHorizontallyCentered")} + {renderAction("alignRight")} + + )} + {targetElements.length > 2 && + renderAction("distributeHorizontally")} + {/* breaks the row ˇˇ */} +
+
+ {renderAction("alignTop")} + {renderAction("alignVerticallyCentered")} + {renderAction("alignBottom")} + {targetElements.length > 2 && + renderAction("distributeVertically")} +
+
+
+ )} +
+ {t("labels.actions")} +
+ {renderAction("group")} + {renderAction("ungroup")} + {showLinkIcon && renderAction("hyperlink")} + {showCropEditorAction && renderAction("cropEditor")} + {showDuplicate && renderAction("duplicateSelection")} + {showDelete && renderAction("deleteSelectedElements")} +
+
+
+
+ )} +
+
+ ); +}; + +const LinearEditorAction = ({ + appState, + renderAction, + targetElements, +}: { + appState: UIAppState; + targetElements: ExcalidrawElement[]; + renderAction: ActionManager["renderAction"]; +}) => { + const showLineEditorAction = + !appState.selectedLinearElement?.isEditing && + targetElements.length === 1 && + isLinearElement(targetElements[0]) && + !isElbowArrow(targetElements[0]); + + if (!showLineEditorAction) { + return null; + } + + return ( +
+ {renderAction("toggleLinearEditor")} +
+ ); +}; + export const CompactShapeActions = ({ appState, elementsMap, @@ -316,47 +789,18 @@ export const CompactShapeActions = ({ setAppState: React.Component["setState"]; }) => { const targetElements = getTargetElements(elementsMap, appState); - const { saveCaretPosition, restoreCaretPosition } = useTextEditorFocus(); const { container } = useExcalidrawContainer(); const isEditingTextOrNewElement = Boolean( appState.editingTextElement || appState.newElement, ); - const showFillIcons = - (hasBackground(appState.activeTool.type) && - !isTransparent(appState.currentItemBackgroundColor)) || - targetElements.some( - (element) => - hasBackground(element.type) && !isTransparent(element.backgroundColor), - ); - - const showLinkIcon = targetElements.length === 1; - const showLineEditorAction = !appState.selectedLinearElement?.isEditing && targetElements.length === 1 && isLinearElement(targetElements[0]) && !isElbowArrow(targetElements[0]); - const showCropEditorAction = - !appState.croppingElementId && - targetElements.length === 1 && - isImageElement(targetElements[0]); - - const showAlignActions = alignActionsPredicate(appState, app); - - let isSingleElementBoundContainer = false; - if ( - targetElements.length === 2 && - (hasBoundTextElement(targetElements[0]) || - hasBoundTextElement(targetElements[1])) - ) { - isSingleElementBoundContainer = true; - } - - const isRTL = document.documentElement.getAttribute("dir") === "rtl"; - return (
{/* Stroke Color */} @@ -373,156 +817,22 @@ export const CompactShapeActions = ({
)} - {/* Combined Properties (Fill, Stroke, Opacity) */} - {(showFillIcons || - hasStrokeWidth(appState.activeTool.type) || - targetElements.some((element) => hasStrokeWidth(element.type)) || - hasStrokeStyle(appState.activeTool.type) || - targetElements.some((element) => hasStrokeStyle(element.type)) || - canChangeRoundness(appState.activeTool.type) || - targetElements.some((element) => canChangeRoundness(element.type))) && ( -
- { - if (open) { - setAppState({ openPopup: "compactStrokeStyles" }); - } else { - setAppState({ openPopup: null }); - } - }} - > - - - - {appState.openPopup === "compactStrokeStyles" && ( - {}} - > -
- {showFillIcons && renderAction("changeFillStyle")} - {(hasStrokeWidth(appState.activeTool.type) || - targetElements.some((element) => - hasStrokeWidth(element.type), - )) && - renderAction("changeStrokeWidth")} - {(hasStrokeStyle(appState.activeTool.type) || - targetElements.some((element) => - hasStrokeStyle(element.type), - )) && ( - <> - {renderAction("changeStrokeStyle")} - {renderAction("changeSloppiness")} - - )} - {(canChangeRoundness(appState.activeTool.type) || - targetElements.some((element) => - canChangeRoundness(element.type), - )) && - renderAction("changeRoundness")} - {renderAction("changeOpacity")} -
-
- )} -
-
- )} - - {/* Combined Arrow Properties */} - {(toolIsArrow(appState.activeTool.type) || - targetElements.some((element) => toolIsArrow(element.type))) && ( -
- { - if (open) { - setAppState({ openPopup: "compactArrowProperties" }); - } else { - setAppState({ openPopup: null }); - } - }} - > - - - - {appState.openPopup === "compactArrowProperties" && ( - {}} - > - {renderAction("changeArrowProperties")} - - )} - -
- )} + + {/* Linear Editor */} {showLineEditorAction && (
@@ -537,73 +847,14 @@ export const CompactShapeActions = ({
{renderAction("changeFontFamily")}
-
- { - if (open) { - if (appState.editingTextElement) { - saveCaretPosition(); - } - setAppState({ openPopup: "compactTextProperties" }); - } else { - setAppState({ openPopup: null }); - if (appState.editingTextElement) { - restoreCaretPosition(); - } - } - }} - > - - - - {appState.openPopup === "compactTextProperties" && ( - { - // Refocus text editor when popover closes with caret restoration - if (appState.editingTextElement) { - restoreCaretPosition(); - } - }} - > -
- {(appState.activeTool.type === "text" || - targetElements.some(isTextElement)) && - renderAction("changeFontSize")} - {(appState.activeTool.type === "text" || - suppportsHorizontalAlign(targetElements, elementsMap)) && - renderAction("changeTextAlign")} - {shouldAllowVerticalAlign(targetElements, elementsMap) && - renderAction("changeVerticalAlign")} -
-
- )} -
-
+ )} @@ -621,135 +872,195 @@ export const CompactShapeActions = ({
)} - {/* Combined Other Actions */} - {!isEditingTextOrNewElement && targetElements.length > 0 && ( -
- { - if (open) { - setAppState({ openPopup: "compactOtherProperties" }); - } else { - setAppState({ openPopup: null }); - } - }} - > - - - - {appState.openPopup === "compactOtherProperties" && ( - {}} - > -
-
- {t("labels.layers")} -
- {renderAction("sendToBack")} - {renderAction("sendBackward")} - {renderAction("bringForward")} - {renderAction("bringToFront")} -
-
- - {showAlignActions && !isSingleElementBoundContainer && ( -
- {t("labels.align")} -
- {isRTL ? ( - <> - {renderAction("alignRight")} - {renderAction("alignHorizontallyCentered")} - {renderAction("alignLeft")} - - ) : ( - <> - {renderAction("alignLeft")} - {renderAction("alignHorizontallyCentered")} - {renderAction("alignRight")} - - )} - {targetElements.length > 2 && - renderAction("distributeHorizontally")} - {/* breaks the row ˇˇ */} -
-
- {renderAction("alignTop")} - {renderAction("alignVerticallyCentered")} - {renderAction("alignBottom")} - {targetElements.length > 2 && - renderAction("distributeVertically")} -
-
-
- )} -
- {t("labels.actions")} -
- {renderAction("group")} - {renderAction("ungroup")} - {showLinkIcon && renderAction("hyperlink")} - {showCropEditorAction && renderAction("cropEditor")} -
-
-
-
- )} -
-
- )} +
); }; +export const MobileShapeActions = ({ + appState, + elementsMap, + renderAction, + app, + setAppState, +}: { + appState: UIAppState; + elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap; + renderAction: ActionManager["renderAction"]; + app: AppClassProperties; + setAppState: React.Component["setState"]; +}) => { + const targetElements = getTargetElements(elementsMap, appState); + const { container } = useExcalidrawContainer(); + const mobileActionsRef = useRef(null); + + const ACTIONS_WIDTH = + mobileActionsRef.current?.getBoundingClientRect()?.width ?? 0; + + // 7 actions + 2 for undo/redo + const MIN_ACTIONS = 9; + + const GAP = 6; + const WIDTH = 32; + + const MIN_WIDTH = MIN_ACTIONS * WIDTH + (MIN_ACTIONS - 1) * GAP; + + const ADDITIONAL_WIDTH = WIDTH + GAP; + + const showDeleteOutside = ACTIONS_WIDTH >= MIN_WIDTH + ADDITIONAL_WIDTH; + const showDuplicateOutside = + ACTIONS_WIDTH >= MIN_WIDTH + 2 * ADDITIONAL_WIDTH; + + return ( + +
+ {canChangeStrokeColor(appState, targetElements) && ( +
+ {renderAction("changeStrokeColor")} +
+ )} + {canChangeBackgroundColor(appState, targetElements) && ( +
+ {renderAction("changeBackgroundColor")} +
+ )} + + {/* Combined Arrow Properties */} + + {/* Linear Editor */} + + {/* Text Properties */} + {(appState.activeTool.type === "text" || + targetElements.some(isTextElement)) && ( + <> +
+ {renderAction("changeFontFamily")} +
+ + + )} + + {/* Combined Other Actions */} + +
+
+
{renderAction("undo")}
+
{renderAction("redo")}
+ {showDuplicateOutside && ( +
+ {renderAction("duplicateSelection")} +
+ )} + {showDeleteOutside && ( +
+ {renderAction("deleteSelectedElements")} +
+ )} +
+
+ ); +}; + export const ShapesSwitcher = ({ activeTool, - appState, + setAppState, app, UIOptions, }: { activeTool: UIAppState["activeTool"]; - appState: UIAppState; + setAppState: React.Component["setState"]; app: AppClassProperties; UIOptions: AppProps["UIOptions"]; }) => { const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false); + const SELECTION_TOOLS = [ + { + type: "selection", + icon: SelectionIcon, + title: capitalizeString(t("toolBar.selection")), + }, + { + type: "lasso", + icon: LassoIcon, + title: capitalizeString(t("toolBar.lasso")), + }, + ] as const; + const frameToolSelected = activeTool.type === "frame"; const laserToolSelected = activeTool.type === "laser"; const lassoToolSelected = - activeTool.type === "lasso" && app.defaultSelectionTool !== "lasso"; + app.state.stylesPanelMode === "full" && + activeTool.type === "lasso" && + app.state.preferredSelectionTool.type !== "lasso"; const embeddableToolSelected = activeTool.type === "embeddable"; @@ -776,6 +1087,40 @@ export const ShapesSwitcher = ({ const shortcut = letter ? `${letter} ${t("helpDialog.or")} ${numericKey}` : `${numericKey}`; + // when in compact styles panel mode (tablet) + // use a ToolPopover for selection/lasso toggle as well + if ( + (value === "selection" || value === "lasso") && + app.state.stylesPanelMode === "compact" + ) { + return ( + { + if (type === "selection" || type === "lasso") { + app.setActiveTool({ type }); + setAppState({ + preferredSelectionTool: { type, initialized: true }, + }); + } + }} + displayedOption={ + SELECTION_TOOLS.find( + (tool) => + tool.type === app.state.preferredSelectionTool.type, + ) || SELECTION_TOOLS[0] + } + fillable={activeTool.type === "selection"} + /> + ); + } return ( { - if (!appState.penDetected && pointerType === "pen") { + if (!app.state.penDetected && pointerType === "pen") { app.togglePenMode(true); } if (value === "selection") { - if (appState.activeTool.type === "selection") { + if (app.state.activeTool.type === "selection") { app.setActiveTool({ type: "lasso" }); } else { app.setActiveTool({ type: "selection" }); @@ -804,7 +1149,7 @@ export const ShapesSwitcher = ({ } }} onChange={({ pointerType }) => { - if (appState.activeTool.type !== value) { + if (app.state.activeTool.type !== value) { trackEvent("toolbar", value, "ui"); } if (value === "image") { @@ -877,7 +1222,7 @@ export const ShapesSwitcher = ({ > {t("toolBar.laser")} - {app.defaultSelectionTool !== "lasso" && ( + {app.state.stylesPanelMode === "full" && ( app.setActiveTool({ type: "lasso" })} icon={LassoIcon} diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index af888b1921..c74ef73b52 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -666,14 +666,9 @@ class App extends React.Component { >(); onRemoveEventListenersEmitter = new Emitter<[]>(); - defaultSelectionTool: "selection" | "lasso" = "selection"; - constructor(props: AppProps) { super(props); const defaultAppState = getDefaultAppState(); - this.defaultSelectionTool = isMobileOrTablet() - ? ("lasso" as const) - : ("selection" as const); const { excalidrawAPI, viewModeEnabled = false, @@ -1527,7 +1522,7 @@ class App extends React.Component { public render() { const selectedElements = this.scene.getSelectedElements(this.state); - const { renderTopRightUI, renderCustomStats } = this.props; + const { renderTopRightUI, renderTopLeftUI, renderCustomStats } = this.props; const sceneNonce = this.scene.getSceneNonce(); const { elementsMap, visibleElements } = @@ -1613,6 +1608,7 @@ class App extends React.Component { onPenModeToggle={this.togglePenMode} onHandToolToggle={this.onHandToolToggle} langCode={getLanguage().code} + renderTopLeftUI={renderTopLeftUI} renderTopRightUI={renderTopRightUI} renderCustomStats={renderCustomStats} showExitZenModeBtn={ @@ -1625,7 +1621,7 @@ class App extends React.Component { !this.state.isLoading && this.state.showWelcomeScreen && this.state.activeTool.type === - this.defaultSelectionTool && + this.state.preferredSelectionTool.type && !this.state.zenModeEnabled && !this.scene.getElementsIncludingDeleted().length } @@ -2370,6 +2366,14 @@ class App extends React.Component { deleteInvisibleElements: true, }); const activeTool = scene.appState.activeTool; + + if (!scene.appState.preferredSelectionTool.initialized) { + scene.appState.preferredSelectionTool = { + type: this.device.editor.isMobile ? "lasso" : "selection", + initialized: true, + }; + } + scene.appState = { ...scene.appState, theme: this.props.theme || scene.appState.theme, @@ -2384,12 +2388,13 @@ class App extends React.Component { activeTool.type === "selection" ? { ...activeTool, - type: this.defaultSelectionTool, + type: scene.appState.preferredSelectionTool.type, } : scene.appState.activeTool, isLoading: false, toast: this.state.toast, }; + if (initialData?.scrollToContent) { scene.appState = { ...scene.appState, @@ -2490,6 +2495,8 @@ class App extends React.Component { // but not too narrow (> MQ_MAX_WIDTH_MOBILE) this.isTabletBreakpoint(editorWidth, editorHeight) && isMobileOrTablet() ? "compact" + : this.isMobileBreakpoint(editorWidth, editorHeight) + ? "mobile" : "full", }); @@ -3289,7 +3296,10 @@ class App extends React.Component { await this.insertClipboardContent(data, filesList, isPlainPaste); - this.setActiveTool({ type: this.defaultSelectionTool }, true); + this.setActiveTool( + { type: this.state.preferredSelectionTool.type }, + true, + ); event?.preventDefault(); }, ); @@ -3435,7 +3445,7 @@ class App extends React.Component { } }, ); - this.setActiveTool({ type: this.defaultSelectionTool }, true); + this.setActiveTool({ type: this.state.preferredSelectionTool.type }, true); if (opts.fitToContent) { this.scrollToContent(duplicatedElements, { @@ -3647,7 +3657,7 @@ class App extends React.Component { ...updateActiveTool( this.state, prevState.activeTool.locked - ? { type: this.defaultSelectionTool } + ? { type: this.state.preferredSelectionTool.type } : prevState.activeTool, ), locked: !prevState.activeTool.locked, @@ -3989,7 +3999,12 @@ class App extends React.Component { } if (appState) { - this.setState(appState); + this.setState({ + ...appState, + // keep existing stylesPanelMode as it needs to be preserved + // or set at startup + stylesPanelMode: this.state.stylesPanelMode, + } as Pick | null); } if (elements) { @@ -4653,7 +4668,7 @@ class App extends React.Component { if (event.key === KEYS.K && !event.altKey && !event[KEYS.CTRL_OR_CMD]) { if (this.state.activeTool.type === "laser") { - this.setActiveTool({ type: this.defaultSelectionTool }); + this.setActiveTool({ type: this.state.preferredSelectionTool.type }); } else { this.setActiveTool({ type: "laser" }); } @@ -5498,7 +5513,7 @@ class App extends React.Component { return; } // we should only be able to double click when mode is selection - if (this.state.activeTool.type !== this.defaultSelectionTool) { + if (this.state.activeTool.type !== this.state.preferredSelectionTool.type) { return; } @@ -6491,6 +6506,10 @@ class App extends React.Component { this.setAppState({ snapLines: [] }); } + if (this.state.openPopup) { + this.setState({ openPopup: null }); + } + this.updateGestureOnPointerDown(event); // if dragging element is freedraw and another pointerdown event occurs @@ -7695,7 +7714,7 @@ class App extends React.Component { if (!this.state.activeTool.locked) { this.setState({ activeTool: updateActiveTool(this.state, { - type: this.defaultSelectionTool, + type: this.state.preferredSelectionTool.type, }), }); } @@ -9409,7 +9428,7 @@ class App extends React.Component { this.setState((prevState) => ({ newElement: null, activeTool: updateActiveTool(this.state, { - type: this.defaultSelectionTool, + type: this.state.preferredSelectionTool.type, }), selectedElementIds: makeNextSelectedElementIds( { @@ -10026,7 +10045,7 @@ class App extends React.Component { newElement: null, suggestedBindings: [], activeTool: updateActiveTool(this.state, { - type: this.defaultSelectionTool, + type: this.state.preferredSelectionTool.type, }), }); } else { @@ -10256,7 +10275,7 @@ class App extends React.Component { { newElement: null, activeTool: updateActiveTool(this.state, { - type: this.defaultSelectionTool, + type: this.state.preferredSelectionTool.type, }), }, () => { @@ -10720,7 +10739,7 @@ class App extends React.Component { event.nativeEvent.pointerType === "pen" && // always allow if user uses a pen secondary button event.button !== POINTER_BUTTON.SECONDARY)) && - this.state.activeTool.type !== this.defaultSelectionTool + this.state.activeTool.type !== this.state.preferredSelectionTool.type ) { return; } diff --git a/packages/excalidraw/components/ColorPicker/ColorPicker.scss b/packages/excalidraw/components/ColorPicker/ColorPicker.scss index 0e3768dcc0..658a75dad7 100644 --- a/packages/excalidraw/components/ColorPicker/ColorPicker.scss +++ b/packages/excalidraw/components/ColorPicker/ColorPicker.scss @@ -7,6 +7,12 @@ } } + .color-picker__title { + padding: 0 0.5rem; + font-size: 0.875rem; + text-align: left; + } + .color-picker__heading { padding: 0 0.5rem; font-size: 0.75rem; @@ -157,6 +163,15 @@ width: 1.625rem; height: 1.625rem; } + + &.compact-sizing { + width: var(--mobile-action-button-size); + height: var(--mobile-action-button-size); + } + + &.mobile-border { + border: 1px solid var(--mobile-color-border); + } } .color-picker__button__hotkey-label { diff --git a/packages/excalidraw/components/ColorPicker/ColorPicker.tsx b/packages/excalidraw/components/ColorPicker/ColorPicker.tsx index ad0bea3610..759ab9cad2 100644 --- a/packages/excalidraw/components/ColorPicker/ColorPicker.tsx +++ b/packages/excalidraw/components/ColorPicker/ColorPicker.tsx @@ -19,7 +19,7 @@ import { useExcalidrawContainer } from "../App"; import { ButtonSeparator } from "../ButtonSeparator"; import { activeEyeDropperAtom } from "../EyeDropper"; import { PropertiesPopover } from "../PropertiesPopover"; -import { backgroundIcon, slashIcon, strokeIcon } from "../icons"; +import { slashIcon, strokeIcon } from "../icons"; import { saveCaretPosition, restoreCaretPosition, @@ -216,6 +216,11 @@ const ColorPickerPopupContent = ({ type={type} elements={elements} updateData={updateData} + showTitle={ + appState.stylesPanelMode === "compact" || + appState.stylesPanelMode === "mobile" + } + showHotKey={appState.stylesPanelMode !== "mobile"} > {colorInputJSX} @@ -230,7 +235,7 @@ const ColorPickerTrigger = ({ label, color, type, - compactMode = false, + stylesPanelMode, mode = "background", onToggle, editingTextElement, @@ -238,7 +243,7 @@ const ColorPickerTrigger = ({ color: string | null; label: string; type: ColorPickerType; - compactMode?: boolean; + stylesPanelMode?: AppState["stylesPanelMode"]; mode?: "background" | "stroke"; onToggle: () => void; editingTextElement?: boolean; @@ -263,6 +268,9 @@ const ColorPickerTrigger = ({ "is-transparent": !color || color === "transparent", "has-outline": !color || !isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD), + "compact-sizing": + stylesPanelMode === "compact" || stylesPanelMode === "mobile", + "mobile-border": stylesPanelMode === "mobile", })} aria-label={label} style={color ? { "--swatch-color": color } : undefined} @@ -275,20 +283,10 @@ const ColorPickerTrigger = ({ onClick={handleClick} >
{!color && slashIcon}
- {compactMode && color && ( -
- {mode === "background" ? ( - - {backgroundIcon} - - ) : ( + {(stylesPanelMode === "compact" || stylesPanelMode === "mobile") && + color && + mode === "stroke" && ( +
{strokeIcon} - )} -
- )} +
+ )} ); }; @@ -316,12 +313,15 @@ export const ColorPicker = ({ topPicks, updateData, appState, - compactMode = false, }: ColorPickerProps) => { const openRef = useRef(appState.openPopup); useEffect(() => { openRef.current = appState.openPopup; }, [appState.openPopup]); + const compactMode = + appState.stylesPanelMode === "compact" || + appState.stylesPanelMode === "mobile"; + return (
{ diff --git a/packages/excalidraw/components/ColorPicker/Picker.tsx b/packages/excalidraw/components/ColorPicker/Picker.tsx index f784912f4c..9c48c58075 100644 --- a/packages/excalidraw/components/ColorPicker/Picker.tsx +++ b/packages/excalidraw/components/ColorPicker/Picker.tsx @@ -37,8 +37,10 @@ interface PickerProps { palette: ColorPaletteCustom; updateData: (formData?: any) => void; children?: React.ReactNode; + showTitle?: boolean; onEyeDropperToggle: (force?: boolean) => void; onEscape: (event: React.KeyboardEvent | KeyboardEvent) => void; + showHotKey?: boolean; } export const Picker = React.forwardRef( @@ -51,11 +53,21 @@ export const Picker = React.forwardRef( palette, updateData, children, + showTitle, onEyeDropperToggle, onEscape, + showHotKey = true, }: PickerProps, ref, ) => { + const title = showTitle + ? type === "elementStroke" + ? t("labels.stroke") + : type === "elementBackground" + ? t("labels.background") + : null + : null; + const [customColors] = React.useState(() => { if (type === "canvasBackground") { return []; @@ -154,6 +166,8 @@ export const Picker = React.forwardRef( // to allow focusing by clicking but not by tabbing tabIndex={-1} > + {title &&
{title}
} + {!!customColors.length && (
@@ -175,12 +189,18 @@ export const Picker = React.forwardRef( palette={palette} onChange={onChange} activeShade={activeShade} + showHotKey={showHotKey} />
{t("colorPicker.shades")} - +
{children}
diff --git a/packages/excalidraw/components/ColorPicker/PickerColorList.tsx b/packages/excalidraw/components/ColorPicker/PickerColorList.tsx index 4fd6815e44..13928f0239 100644 --- a/packages/excalidraw/components/ColorPicker/PickerColorList.tsx +++ b/packages/excalidraw/components/ColorPicker/PickerColorList.tsx @@ -20,6 +20,7 @@ interface PickerColorListProps { color: string | null; onChange: (color: string) => void; activeShade: number; + showHotKey?: boolean; } const PickerColorList = ({ @@ -27,6 +28,7 @@ const PickerColorList = ({ color, onChange, activeShade, + showHotKey = true, }: PickerColorListProps) => { const colorObj = getColorNameAndShadeFromColor({ color, @@ -82,7 +84,7 @@ const PickerColorList = ({ key={key} >
- + {showHotKey && } ); })} diff --git a/packages/excalidraw/components/ColorPicker/ShadeList.tsx b/packages/excalidraw/components/ColorPicker/ShadeList.tsx index db33402b0c..2c17c57ede 100644 --- a/packages/excalidraw/components/ColorPicker/ShadeList.tsx +++ b/packages/excalidraw/components/ColorPicker/ShadeList.tsx @@ -16,9 +16,15 @@ interface ShadeListProps { color: string | null; onChange: (color: string) => void; palette: ColorPaletteCustom; + showHotKey?: boolean; } -export const ShadeList = ({ color, onChange, palette }: ShadeListProps) => { +export const ShadeList = ({ + color, + onChange, + palette, + showHotKey, +}: ShadeListProps) => { const colorObj = getColorNameAndShadeFromColor({ color: color || "transparent", palette, @@ -67,7 +73,9 @@ export const ShadeList = ({ color, onChange, palette }: ShadeListProps) => { }} >
- + {showHotKey && ( + + )} ))}
diff --git a/packages/excalidraw/components/ExcalidrawLogo.scss b/packages/excalidraw/components/ExcalidrawLogo.scss index e59e8a90c0..d42f98a325 100644 --- a/packages/excalidraw/components/ExcalidrawLogo.scss +++ b/packages/excalidraw/components/ExcalidrawLogo.scss @@ -1,5 +1,8 @@ .excalidraw { .ExcalidrawLogo { + --logo-icon--mobile: 1rem; + --logo-text--mobile: 0.75rem; + --logo-icon--xs: 2rem; --logo-text--xs: 1.5rem; @@ -30,6 +33,17 @@ color: var(--color-logo-text); } + &.is-mobile { + .ExcalidrawLogo-icon { + height: var(--logo-icon--mobile); + } + + .ExcalidrawLogo-text { + height: var(--logo-text--mobile); + margin-left: 0.5rem; + } + } + &.is-xs { .ExcalidrawLogo-icon { height: var(--logo-icon--xs); diff --git a/packages/excalidraw/components/ExcalidrawLogo.tsx b/packages/excalidraw/components/ExcalidrawLogo.tsx index 01d07fc505..8610249ba1 100644 --- a/packages/excalidraw/components/ExcalidrawLogo.tsx +++ b/packages/excalidraw/components/ExcalidrawLogo.tsx @@ -41,7 +41,7 @@ const LogoText = () => ( ); -type LogoSize = "xs" | "small" | "normal" | "large" | "custom"; +type LogoSize = "xs" | "small" | "normal" | "large" | "custom" | "mobile"; interface LogoProps { size?: LogoSize; diff --git a/packages/excalidraw/components/FontPicker/FontPicker.tsx b/packages/excalidraw/components/FontPicker/FontPicker.tsx index 891ae49efd..c52286a173 100644 --- a/packages/excalidraw/components/FontPicker/FontPicker.tsx +++ b/packages/excalidraw/components/FontPicker/FontPicker.tsx @@ -106,6 +106,7 @@ export const FontPicker = React.memo( {isOpened && ( - + {app.state.stylesPanelMode === "full" && ( + + )} { const setAppState = useExcalidrawSetAppState(); + const compactStyle = compactMode + ? { + ...MOBILE_ACTION_BUTTON_BG, + width: "2rem", + height: "2rem", + } + : {}; + return (
@@ -37,6 +49,7 @@ export const FontPickerTrigger = ({ }} style={{ border: "none", + ...compactStyle, }} />
diff --git a/packages/excalidraw/components/HandButton.tsx b/packages/excalidraw/components/HandButton.tsx index 5ebfdf9d3f..db653a8103 100644 --- a/packages/excalidraw/components/HandButton.tsx +++ b/packages/excalidraw/components/HandButton.tsx @@ -18,7 +18,7 @@ type LockIconProps = { export const HandButton = (props: LockIconProps) => { return ( ({ ); }; + const isMobile = device.editor.isMobile; + return ( diff --git a/packages/excalidraw/components/LayerUI.tsx b/packages/excalidraw/components/LayerUI.tsx index 6748095322..4fd6f6d269 100644 --- a/packages/excalidraw/components/LayerUI.tsx +++ b/packages/excalidraw/components/LayerUI.tsx @@ -91,6 +91,7 @@ interface LayerUIProps { onPenModeToggle: AppClassProperties["togglePenMode"]; showExitZenModeBtn: boolean; langCode: Language["code"]; + renderTopLeftUI?: ExcalidrawProps["renderTopLeftUI"]; renderTopRightUI?: ExcalidrawProps["renderTopRightUI"]; renderCustomStats?: ExcalidrawProps["renderCustomStats"]; UIOptions: AppProps["UIOptions"]; @@ -149,6 +150,7 @@ const LayerUI = ({ onHandToolToggle, onPenModeToggle, showExitZenModeBtn, + renderTopLeftUI, renderTopRightUI, renderCustomStats, UIOptions, @@ -366,7 +368,7 @@ const LayerUI = ({ /> diff --git a/packages/excalidraw/components/MobileMenu.tsx b/packages/excalidraw/components/MobileMenu.tsx index 454c0f64e5..8da02b30b3 100644 --- a/packages/excalidraw/components/MobileMenu.tsx +++ b/packages/excalidraw/components/MobileMenu.tsx @@ -1,32 +1,23 @@ import React from "react"; -import { showSelectedShapeActions } from "@excalidraw/element"; - import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types"; -import { isHandToolActive } from "../appState"; import { useTunnels } from "../context/tunnels"; import { t } from "../i18n"; import { calculateScrollCenter } from "../scene"; import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars"; -import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; +import { MobileShapeActions } from "./Actions"; +import { MobileToolBar } from "./MobileToolBar"; import { FixedSideContainer } from "./FixedSideContainer"; -import { HandButton } from "./HandButton"; -import { HintViewer } from "./HintViewer"; + import { Island } from "./Island"; -import { LockButton } from "./LockButton"; -import { PenModeButton } from "./PenModeButton"; -import { Section } from "./Section"; -import Stack from "./Stack"; import type { ActionManager } from "../actions/manager"; import type { AppClassProperties, AppProps, AppState, - Device, - ExcalidrawProps, UIAppState, } from "../types"; import type { JSX } from "react"; @@ -38,7 +29,6 @@ type MobileMenuProps = { renderImageExportDialog: () => React.ReactNode; setAppState: React.Component["setState"]; elements: readonly NonDeletedExcalidrawElement[]; - onLockToggle: () => void; onHandToolToggle: () => void; onPenModeToggle: AppClassProperties["togglePenMode"]; @@ -46,9 +36,11 @@ type MobileMenuProps = { isMobile: boolean, appState: UIAppState, ) => JSX.Element | null; - renderCustomStats?: ExcalidrawProps["renderCustomStats"]; + renderTopLeftUI?: ( + isMobile: boolean, + appState: UIAppState, + ) => JSX.Element | null; renderSidebars: () => JSX.Element | null; - device: Device; renderWelcomeScreen: boolean; UIOptions: AppProps["UIOptions"]; app: AppClassProperties; @@ -59,14 +51,10 @@ export const MobileMenu = ({ elements, actionManager, setAppState, - onLockToggle, onHandToolToggle, - onPenModeToggle, - + renderTopLeftUI, renderTopRightUI, - renderCustomStats, renderSidebars, - device, renderWelcomeScreen, UIOptions, app, @@ -76,141 +64,98 @@ export const MobileMenu = ({ MainMenuTunnel, DefaultSidebarTriggerTunnel, } = useTunnels(); - const renderToolbar = () => { - return ( - - {renderWelcomeScreen && } -
- {(heading: React.ReactNode) => ( - - - - {heading} - - - - - {renderTopRightUI && renderTopRightUI(true, appState)} -
- {!appState.viewModeEnabled && - appState.openDialog?.name !== "elementLinkSelector" && ( - - )} - onPenModeToggle(null)} - title={t("toolBar.penMode")} - isMobile - penDetected={appState.penDetected} - /> - - onHandToolToggle()} - title={t("toolBar.hand")} - isMobile - /> -
-
-
- )} -
- -
+ const renderAppTopBar = () => { + const topRightUI = renderTopRightUI?.(true, appState) ?? ( + + ); + + const topLeftUI = ( +
+ {renderTopLeftUI?.(true, appState)} + +
); - }; - const renderAppToolbar = () => { if ( appState.viewModeEnabled || appState.openDialog?.name === "elementLinkSelector" ) { - return ( -
- -
- ); + return
{topLeftUI}
; } return ( -
- - {actionManager.renderAction("toggleEditMenu")} - {actionManager.renderAction( - appState.multiElement ? "finalize" : "duplicateSelection", - )} - {actionManager.renderAction("deleteSelectedElements")} -
- {actionManager.renderAction("undo")} - {actionManager.renderAction("redo")} -
+
+ {topLeftUI} + {topRightUI}
); }; + const renderToolbar = () => { + return ( + + ); + }; + return ( <> {renderSidebars()} - {!appState.viewModeEnabled && - appState.openDialog?.name !== "elementLinkSelector" && - renderToolbar()} + {/* welcome screen, bottom bar, and top bar all have the same z-index */} + {/* ordered in this reverse order so that top bar is on top */} +
+ {renderWelcomeScreen && } +
+
- - {appState.openMenu === "shape" && - !appState.viewModeEnabled && - appState.openDialog?.name !== "elementLinkSelector" && - showSelectedShapeActions(appState, elements) ? ( -
- -
- ) : null} -
- {renderAppToolbar()} - {appState.scrolledOutside && - !appState.openMenu && - !appState.openSidebar && ( - - )} -
+ + + + {!appState.viewModeEnabled && + appState.openDialog?.name !== "elementLinkSelector" && + renderToolbar()} + {appState.scrolledOutside && + !appState.openMenu && + !appState.openSidebar && ( + + )}
+ + + {renderAppTopBar()} + ); }; diff --git a/packages/excalidraw/components/MobileToolBar.scss b/packages/excalidraw/components/MobileToolBar.scss new file mode 100644 index 0000000000..b936c70ebd --- /dev/null +++ b/packages/excalidraw/components/MobileToolBar.scss @@ -0,0 +1,78 @@ +@import "open-color/open-color.scss"; +@import "../css/variables.module.scss"; + +.excalidraw { + .mobile-toolbar { + display: flex; + flex: 1; + align-items: center; + padding: 0px; + gap: 4px; + border-radius: var(--space-factor); + overflow-x: auto; + scrollbar-width: none; + -ms-overflow-style: none; + justify-content: space-between; + } + + .mobile-toolbar::-webkit-scrollbar { + display: none; + } + + .mobile-toolbar .ToolIcon { + min-width: 2rem; + min-height: 2rem; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + .ToolIcon__icon { + width: 2.25rem; + height: 2.25rem; + + &:hover { + background-color: transparent; + } + } + + &.active { + background: var( + --color-surface-primary-container, + var(--island-bg-color) + ); + border-color: var(--button-active-border, var(--color-primary-darkest)); + } + + svg { + width: 1rem; + height: 1rem; + } + } + + .mobile-toolbar .App-toolbar__extra-tools-dropdown { + min-width: 160px; + z-index: var(--zIndex-layerUI); + } + + .mobile-toolbar-separator { + width: 1px; + height: 24px; + background: var(--default-border-color); + margin: 0 2px; + flex-shrink: 0; + } + + .mobile-toolbar-undo { + display: flex; + align-items: center; + } + + .mobile-toolbar-undo .ToolIcon { + min-width: 32px; + min-height: 32px; + width: 32px; + height: 32px; + } +} diff --git a/packages/excalidraw/components/MobileToolBar.tsx b/packages/excalidraw/components/MobileToolBar.tsx new file mode 100644 index 0000000000..093cbd2630 --- /dev/null +++ b/packages/excalidraw/components/MobileToolBar.tsx @@ -0,0 +1,471 @@ +import { useState, useEffect, useRef } from "react"; +import clsx from "clsx"; + +import { KEYS, capitalizeString } from "@excalidraw/common"; + +import { trackEvent } from "../analytics"; + +import { t } from "../i18n"; + +import { isHandToolActive } from "../appState"; + +import { useTunnels } from "../context/tunnels"; + +import { HandButton } from "./HandButton"; +import { ToolButton } from "./ToolButton"; +import DropdownMenu from "./dropdownMenu/DropdownMenu"; +import { ToolPopover } from "./ToolPopover"; + +import { + SelectionIcon, + FreedrawIcon, + EraserIcon, + RectangleIcon, + ArrowIcon, + extraToolsIcon, + DiamondIcon, + EllipseIcon, + LineIcon, + TextIcon, + ImageIcon, + frameToolIcon, + EmbedIcon, + laserPointerToolIcon, + LassoIcon, + mermaidLogoIcon, + MagicIcon, +} from "./icons"; + +import "./ToolIcon.scss"; +import "./MobileToolBar.scss"; + +import type { AppClassProperties, ToolType, UIAppState } from "../types"; + +const SHAPE_TOOLS = [ + { + type: "rectangle", + icon: RectangleIcon, + title: capitalizeString(t("toolBar.rectangle")), + }, + { + type: "diamond", + icon: DiamondIcon, + title: capitalizeString(t("toolBar.diamond")), + }, + { + type: "ellipse", + icon: EllipseIcon, + title: capitalizeString(t("toolBar.ellipse")), + }, +] as const; + +const SELECTION_TOOLS = [ + { + type: "selection", + icon: SelectionIcon, + title: capitalizeString(t("toolBar.selection")), + }, + { + type: "lasso", + icon: LassoIcon, + title: capitalizeString(t("toolBar.lasso")), + }, +] as const; + +const LINEAR_ELEMENT_TOOLS = [ + { + type: "arrow", + icon: ArrowIcon, + title: capitalizeString(t("toolBar.arrow")), + }, + { type: "line", icon: LineIcon, title: capitalizeString(t("toolBar.line")) }, +] as const; + +type MobileToolBarProps = { + app: AppClassProperties; + onHandToolToggle: () => void; + setAppState: React.Component["setState"]; +}; + +export const MobileToolBar = ({ + app, + onHandToolToggle, + setAppState, +}: MobileToolBarProps) => { + const activeTool = app.state.activeTool; + const [isOtherShapesMenuOpen, setIsOtherShapesMenuOpen] = useState(false); + const [lastActiveGenericShape, setLastActiveGenericShape] = useState< + "rectangle" | "diamond" | "ellipse" + >("rectangle"); + const [lastActiveLinearElement, setLastActiveLinearElement] = useState< + "arrow" | "line" + >("arrow"); + + const toolbarRef = useRef(null); + + // keep lastActiveGenericShape in sync with active tool if user switches via other UI + useEffect(() => { + if ( + activeTool.type === "rectangle" || + activeTool.type === "diamond" || + activeTool.type === "ellipse" + ) { + setLastActiveGenericShape(activeTool.type); + } + }, [activeTool.type]); + + // keep lastActiveLinearElement in sync with active tool if user switches via other UI + useEffect(() => { + if (activeTool.type === "arrow" || activeTool.type === "line") { + setLastActiveLinearElement(activeTool.type); + } + }, [activeTool.type]); + + const frameToolSelected = activeTool.type === "frame"; + const laserToolSelected = activeTool.type === "laser"; + const embeddableToolSelected = activeTool.type === "embeddable"; + + const { TTDDialogTriggerTunnel } = useTunnels(); + + const handleToolChange = (toolType: string, pointerType?: string) => { + if (app.state.activeTool.type !== toolType) { + trackEvent("toolbar", toolType, "ui"); + } + + if (toolType === "selection") { + if (app.state.activeTool.type === "selection") { + // Toggle selection tool behavior if needed + } else { + app.setActiveTool({ type: "selection" }); + } + } else { + app.setActiveTool({ type: toolType as ToolType }); + } + }; + + const toolbarWidth = + toolbarRef.current?.getBoundingClientRect()?.width ?? 0 - 8; + const WIDTH = 36; + const GAP = 4; + + // hand, selection, freedraw, eraser, rectangle, arrow, others + const MIN_TOOLS = 7; + const MIN_WIDTH = MIN_TOOLS * WIDTH + (MIN_TOOLS - 1) * GAP; + const ADDITIONAL_WIDTH = WIDTH + GAP; + + const showTextToolOutside = toolbarWidth >= MIN_WIDTH + 1 * ADDITIONAL_WIDTH; + const showImageToolOutside = toolbarWidth >= MIN_WIDTH + 2 * ADDITIONAL_WIDTH; + const showFrameToolOutside = toolbarWidth >= MIN_WIDTH + 3 * ADDITIONAL_WIDTH; + + const extraTools = [ + "text", + "frame", + "embeddable", + "laser", + "magicframe", + ].filter((tool) => { + if (showImageToolOutside && tool === "image") { + return false; + } + if (showFrameToolOutside && tool === "frame") { + return false; + } + return true; + }); + const extraToolSelected = extraTools.includes(activeTool.type); + const extraIcon = extraToolSelected + ? activeTool.type === "frame" + ? frameToolIcon + : activeTool.type === "embeddable" + ? EmbedIcon + : activeTool.type === "laser" + ? laserPointerToolIcon + : activeTool.type === "text" + ? TextIcon + : activeTool.type === "magicframe" + ? MagicIcon + : extraToolsIcon + : extraToolsIcon; + + return ( +
+ {/* Hand Tool */} + + + {/* Selection Tool */} + { + if (type === "selection" || type === "lasso") { + app.setActiveTool({ type }); + setAppState({ + preferredSelectionTool: { type, initialized: true }, + }); + } + }} + displayedOption={ + SELECTION_TOOLS.find( + (tool) => tool.type === app.state.preferredSelectionTool.type, + ) || SELECTION_TOOLS[0] + } + /> + + {/* Free Draw */} + handleToolChange("freedraw")} + /> + + {/* Eraser */} + handleToolChange("eraser")} + /> + + {/* Rectangle */} + { + if ( + type === "rectangle" || + type === "diamond" || + type === "ellipse" + ) { + setLastActiveGenericShape(type); + app.setActiveTool({ type }); + } + }} + displayedOption={ + SHAPE_TOOLS.find((tool) => tool.type === lastActiveGenericShape) || + SHAPE_TOOLS[0] + } + /> + + {/* Arrow/Line */} + { + if (type === "arrow" || type === "line") { + setLastActiveLinearElement(type); + app.setActiveTool({ type }); + } + }} + displayedOption={ + LINEAR_ELEMENT_TOOLS.find( + (tool) => tool.type === lastActiveLinearElement, + ) || LINEAR_ELEMENT_TOOLS[0] + } + /> + + {/* Text Tool */} + {showTextToolOutside && ( + handleToolChange("text")} + /> + )} + + {/* Image */} + {showImageToolOutside && ( + handleToolChange("image")} + /> + )} + + {/* Frame Tool */} + {showFrameToolOutside && ( + handleToolChange("frame")} + /> + )} + + {/* Other Shapes */} + + setIsOtherShapesMenuOpen(!isOtherShapesMenuOpen)} + title={t("toolBar.extraTools")} + style={{ + width: WIDTH, + height: WIDTH, + display: "flex", + alignItems: "center", + justifyContent: "center", + }} + > + {extraIcon} + + setIsOtherShapesMenuOpen(false)} + onSelect={() => setIsOtherShapesMenuOpen(false)} + className="App-toolbar__extra-tools-dropdown" + > + {!showTextToolOutside && ( + app.setActiveTool({ type: "text" })} + icon={TextIcon} + shortcut={KEYS.T.toLocaleUpperCase()} + data-testid="toolbar-text" + selected={activeTool.type === "text"} + > + {t("toolBar.text")} + + )} + + {!showImageToolOutside && ( + app.setActiveTool({ type: "image" })} + icon={ImageIcon} + data-testid="toolbar-image" + selected={activeTool.type === "image"} + > + {t("toolBar.image")} + + )} + {!showFrameToolOutside && ( + app.setActiveTool({ type: "frame" })} + icon={frameToolIcon} + shortcut={KEYS.F.toLocaleUpperCase()} + data-testid="toolbar-frame" + selected={frameToolSelected} + > + {t("toolBar.frame")} + + )} + app.setActiveTool({ type: "embeddable" })} + icon={EmbedIcon} + data-testid="toolbar-embeddable" + selected={embeddableToolSelected} + > + {t("toolBar.embeddable")} + + app.setActiveTool({ type: "laser" })} + icon={laserPointerToolIcon} + data-testid="toolbar-laser" + selected={laserToolSelected} + shortcut={KEYS.K.toLocaleUpperCase()} + > + {t("toolBar.laser")} + +
+ Generate +
+ {app.props.aiEnabled !== false && } + app.setOpenDialog({ name: "ttd", tab: "mermaid" })} + icon={mermaidLogoIcon} + data-testid="toolbar-embeddable" + > + {t("toolBar.mermaidToExcalidraw")} + + {app.props.aiEnabled !== false && app.plugins.diagramToCode && ( + <> + app.onMagicframeToolSelect()} + icon={MagicIcon} + data-testid="toolbar-magicframe" + > + {t("toolBar.magicframe")} + AI + + + )} +
+
+
+ ); +}; diff --git a/packages/excalidraw/components/ToolPopover.scss b/packages/excalidraw/components/ToolPopover.scss new file mode 100644 index 0000000000..d049704bb7 --- /dev/null +++ b/packages/excalidraw/components/ToolPopover.scss @@ -0,0 +1,18 @@ +@import "../css/variables.module.scss"; + +.excalidraw { + .tool-popover-content { + display: flex; + flex-direction: row; + gap: 0.25rem; + border-radius: 0.5rem; + background: var(--island-bg-color); + box-shadow: var(--shadow-island); + padding: 0.5rem; + z-index: var(--zIndex-layerUI); + } + + &:focus { + outline: none; + } +} diff --git a/packages/excalidraw/components/ToolPopover.tsx b/packages/excalidraw/components/ToolPopover.tsx new file mode 100644 index 0000000000..81d5726d5a --- /dev/null +++ b/packages/excalidraw/components/ToolPopover.tsx @@ -0,0 +1,120 @@ +import React, { useEffect, useState } from "react"; +import clsx from "clsx"; + +import { capitalizeString } from "@excalidraw/common"; + +import * as Popover from "@radix-ui/react-popover"; + +import { trackEvent } from "../analytics"; + +import { ToolButton } from "./ToolButton"; + +import "./ToolPopover.scss"; + +import type { AppClassProperties } from "../types"; + +type ToolOption = { + type: string; + icon: React.ReactNode; + title?: string; +}; + +type ToolPopoverProps = { + app: AppClassProperties; + options: readonly ToolOption[]; + activeTool: { type: string }; + defaultOption: string; + className?: string; + namePrefix: string; + title: string; + "data-testid": string; + onToolChange: (type: string) => void; + displayedOption: ToolOption; + fillable?: boolean; +}; + +export const ToolPopover = ({ + app, + options, + activeTool, + defaultOption, + className = "Shape", + namePrefix, + title, + "data-testid": dataTestId, + onToolChange, + displayedOption, + fillable = false, +}: ToolPopoverProps) => { + const [isPopupOpen, setIsPopupOpen] = useState(false); + const currentType = activeTool.type; + const isActive = displayedOption.type === currentType; + const SIDE_OFFSET = 32 / 2 + 10; + + // if currentType is not in options, close popup + if (!options.some((o) => o.type === currentType) && isPopupOpen) { + setIsPopupOpen(false); + } + + // Close popover when user starts interacting with the canvas (pointer down) + useEffect(() => { + // app.onPointerDownEmitter emits when pointer down happens on canvas area + const unsubscribe = app.onPointerDownEmitter.on(() => { + setIsPopupOpen(false); + }); + return () => unsubscribe?.(); + }, [app]); + + return ( + + + o.type === activeTool.type), + })} + type="radio" + icon={displayedOption.icon} + checked={isActive} + name="editor-current-shape" + title={title} + aria-label={title} + data-testid={dataTestId} + onPointerDown={() => { + setIsPopupOpen((v) => !v); + onToolChange(defaultOption); + }} + /> + + + + {options.map(({ type, icon, title }) => ( + { + if (app.state.activeTool.type !== type) { + trackEvent("toolbar", type, "ui"); + } + app.setActiveTool({ type: type as any }); + onToolChange?.(type); + }} + /> + ))} + + + ); +}; diff --git a/packages/excalidraw/components/Toolbar.scss b/packages/excalidraw/components/Toolbar.scss index 14c4cc174b..3919176bbb 100644 --- a/packages/excalidraw/components/Toolbar.scss +++ b/packages/excalidraw/components/Toolbar.scss @@ -44,6 +44,10 @@ var(--button-active-border, var(--color-primary-darkest)) inset; } + &:hover { + background-color: transparent; + } + &--selected, &--selected:hover { background: var(--color-primary-light); diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenu.scss b/packages/excalidraw/components/dropdownMenu/DropdownMenu.scss index 95d258c46b..a0a230941d 100644 --- a/packages/excalidraw/components/dropdownMenu/DropdownMenu.scss +++ b/packages/excalidraw/components/dropdownMenu/DropdownMenu.scss @@ -3,24 +3,46 @@ .excalidraw { .dropdown-menu { position: absolute; - top: 100%; + top: 2.5rem; margin-top: 0.5rem; + &--placement-top { + top: auto; + bottom: 100%; + margin-top: 0; + margin-bottom: 0.5rem; + } + &--mobile { - left: 0; width: 100%; row-gap: 0.75rem; + // When main menu is in the top toolbar, position relative to trigger + &.main-menu-dropdown { + min-width: 232px; + max-width: calc(100vw - var(--editor-container-padding) * 2); + margin-top: 0; + margin-bottom: 0; + z-index: var(--zIndex-layerUI); + + @media screen and (orientation: landscape) { + max-width: 232px; + } + } + .dropdown-menu-container { padding: 8px 8px; box-sizing: border-box; - // background-color: var(--island-bg-color); + max-height: calc( + 100svh - var(--editor-container-padding) * 2 - 2.25rem + ); box-shadow: var(--shadow-island); border-radius: var(--border-radius-lg); position: relative; transition: box-shadow 0.5s ease-in-out; display: flex; flex-direction: column; + overflow-y: auto; &.zen-mode { box-shadow: none; @@ -30,7 +52,7 @@ .dropdown-menu-container { background-color: var(--island-bg-color); - max-height: calc(100vh - 150px); + overflow-y: auto; --gap: 2; } diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenu.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenu.tsx index e1412e20b1..761d09b3f9 100644 --- a/packages/excalidraw/components/dropdownMenu/DropdownMenu.tsx +++ b/packages/excalidraw/components/dropdownMenu/DropdownMenu.tsx @@ -17,16 +17,27 @@ import "./DropdownMenu.scss"; const DropdownMenu = ({ children, open, + placement, }: { children?: React.ReactNode; open: boolean; + placement?: "top" | "bottom"; }) => { const MenuTriggerComp = getMenuTriggerComponent(children); const MenuContentComp = getMenuContentComponent(children); + + // clone the MenuContentComp to pass the placement prop + const MenuContentCompWithPlacement = + MenuContentComp && React.isValidElement(MenuContentComp) + ? React.cloneElement(MenuContentComp as React.ReactElement, { + placement, + }) + : MenuContentComp; + return ( <> {MenuTriggerComp} - {open && MenuContentComp} + {open && MenuContentCompWithPlacement} ); }; diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx index de6fc31c18..291f857e80 100644 --- a/packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx +++ b/packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx @@ -17,6 +17,7 @@ const MenuContent = ({ className = "", onSelect, style, + placement = "bottom", }: { children?: React.ReactNode; onClickOutside?: () => void; @@ -26,6 +27,7 @@ const MenuContent = ({ */ onSelect?: (event: Event) => void; style?: React.CSSProperties; + placement?: "top" | "bottom"; }) => { const device = useDevice(); const menuRef = useRef(null); @@ -58,6 +60,7 @@ const MenuContent = ({ const classNames = clsx(`dropdown-menu ${className}`, { "dropdown-menu--mobile": device.editor.isMobile, + "dropdown-menu--placement-top": placement === "top", }).trim(); return ( diff --git a/packages/excalidraw/components/icons.tsx b/packages/excalidraw/components/icons.tsx index 33e59380c7..3f6c4d1bb1 100644 --- a/packages/excalidraw/components/icons.tsx +++ b/packages/excalidraw/components/icons.tsx @@ -2319,22 +2319,10 @@ export const adjustmentsIcon = createIcon( tablerIconProps, ); -export const backgroundIcon = createIcon( - - - - - - - - , - tablerIconProps, -); - export const strokeIcon = createIcon( - + , tablerIconProps, ); diff --git a/packages/excalidraw/components/main-menu/MainMenu.tsx b/packages/excalidraw/components/main-menu/MainMenu.tsx index 7c2b5fb4a1..8ce2a5d69b 100644 --- a/packages/excalidraw/components/main-menu/MainMenu.tsx +++ b/packages/excalidraw/components/main-menu/MainMenu.tsx @@ -53,6 +53,8 @@ const MainMenu = Object.assign( onSelect={composeEventHandlers(onSelect, () => { setAppState({ openMenu: null }); })} + placement="bottom" + className={device.editor.isMobile ? "main-menu-dropdown" : ""} > {children} {device.editor.isMobile && appState.collaborators.size > 0 && ( diff --git a/packages/excalidraw/components/shapes.tsx b/packages/excalidraw/components/shapes.tsx index 56c85bcd42..d46f08a311 100644 --- a/packages/excalidraw/components/shapes.tsx +++ b/packages/excalidraw/components/shapes.tsx @@ -89,7 +89,7 @@ export const SHAPES = [ ] as const; export const getToolbarTools = (app: AppClassProperties) => { - return app.defaultSelectionTool === "lasso" + return app.state.preferredSelectionTool.type === "lasso" ? ([ { value: "lasso", diff --git a/packages/excalidraw/components/welcome-screen/WelcomeScreen.scss b/packages/excalidraw/components/welcome-screen/WelcomeScreen.scss index 8e3a010309..96f1ca2df3 100644 --- a/packages/excalidraw/components/welcome-screen/WelcomeScreen.scss +++ b/packages/excalidraw/components/welcome-screen/WelcomeScreen.scss @@ -252,16 +252,12 @@ } } - @media (max-height: 599px) { + &.excalidraw--mobile { .welcome-screen-center { - margin-top: 4rem; - } - } - @media (min-height: 600px) and (max-height: 900px) { - .welcome-screen-center { - margin-top: 8rem; + margin-bottom: 2rem; } } + @media (max-height: 500px), (max-width: 320px) { .welcome-screen-center { display: none; diff --git a/packages/excalidraw/css/styles.scss b/packages/excalidraw/css/styles.scss index 2169696ae0..679a5c4cd1 100644 --- a/packages/excalidraw/css/styles.scss +++ b/packages/excalidraw/css/styles.scss @@ -44,6 +44,11 @@ body.excalidraw-cursor-resize * { height: 100%; width: 100%; + button, + label { + @include buttonNoHighlight; + } + button { cursor: pointer; user-select: none; @@ -235,27 +240,32 @@ body.excalidraw-cursor-resize * { z-index: var(--zIndex-layerUI); display: flex; flex-direction: column; - align-items: center; + } + + .App-welcome-screen { + z-index: var(--zIndex-layerUI); } .App-bottom-bar { position: absolute; - top: 0; + // account for margins + width: calc(100% - 28px); + max-width: 450px; bottom: 0; - left: 0; - right: 0; + left: 50%; + transform: translateX(-50%); --bar-padding: calc(4 * var(--space-factor)); - z-index: 4; + z-index: var(--zIndex-layerUI); display: flex; - align-items: flex-end; + flex-direction: column; + pointer-events: none; + justify-content: center; > .Island { - width: 100%; - max-width: 100%; - min-width: 100%; box-sizing: border-box; max-height: 100%; + padding: 4px; display: flex; flex-direction: column; pointer-events: var(--ui-pointerEvents); @@ -263,7 +273,8 @@ body.excalidraw-cursor-resize * { } .App-toolbar { - width: 100%; + display: flex; + justify-content: center; .eraser { &.ToolIcon:hover { @@ -276,16 +287,15 @@ body.excalidraw-cursor-resize * { } } - .App-toolbar-content { + .excalidraw-ui-top-left { display: flex; align-items: center; - justify-content: space-between; - padding: 8px; + gap: 0.5rem; + } - .dropdown-menu--mobile { - bottom: 55px; - top: auto; - } + .App-toolbar-content { + display: flex; + flex-direction: column; } .App-mobile-menu { @@ -506,7 +516,7 @@ body.excalidraw-cursor-resize * { display: none; } .scroll-back-to-content { - bottom: calc(80px + var(--sab, 0)); + bottom: calc(100px + var(--sab, 0)); z-index: -1; } } diff --git a/packages/excalidraw/css/theme.scss b/packages/excalidraw/css/theme.scss index 1d6a569665..223cd8eb6e 100644 --- a/packages/excalidraw/css/theme.scss +++ b/packages/excalidraw/css/theme.scss @@ -8,6 +8,8 @@ --button-gray-1: #{$oc-gray-2}; --button-gray-2: #{$oc-gray-4}; --button-gray-3: #{$oc-gray-5}; + --mobile-action-button-bg: rgba(255, 255, 255, 0.35); + --mobile-color-border: var(--default-border-color); --button-special-active-bg-color: #{$oc-green-0}; --dialog-border-color: var(--color-gray-20); --dropdown-icon: url('data:image/svg+xml,'); @@ -42,6 +44,11 @@ --lg-button-size: 2.25rem; --lg-icon-size: 1rem; --editor-container-padding: 1rem; + --mobile-action-button-size: 2rem; + + @include isMobile { + --editor-container-padding: 0.75rem; + } @media screen and (min-device-width: 1921px) { --lg-button-size: 2.5rem; @@ -177,6 +184,8 @@ --button-gray-1: #363636; --button-gray-2: #272727; --button-gray-3: #222; + --mobile-action-button-bg: var(--island-bg-color); + --mobile-color-border: rgba(255, 255, 255, 0.85); --button-special-active-bg-color: #204624; --dialog-border-color: var(--color-gray-80); --dropdown-icon: url('data:image/svg+xml,'); diff --git a/packages/excalidraw/css/variables.module.scss b/packages/excalidraw/css/variables.module.scss index c360c0dc6b..15d0768adb 100644 --- a/packages/excalidraw/css/variables.module.scss +++ b/packages/excalidraw/css/variables.module.scss @@ -122,6 +122,17 @@ color: var(--button-color, var(--color-on-primary-container)); } } + + @include isMobile() { + width: var(--mobile-action-button-size, var(--default-button-size)); + height: var(--mobile-action-button-size, var(--default-button-size)); + } +} + +@mixin buttonNoHighlight { + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + user-select: none; } @mixin outlineButtonIconStyles { @@ -187,4 +198,9 @@ &:active { box-shadow: 0 0 0 1px var(--color-brand-active); } + + @include isMobile() { + width: var(--mobile-action-button-size, 2rem); + height: var(--mobile-action-button-size, 2rem); + } } diff --git a/packages/excalidraw/index.tsx b/packages/excalidraw/index.tsx index 1b1f830439..1d599a98ec 100644 --- a/packages/excalidraw/index.tsx +++ b/packages/excalidraw/index.tsx @@ -28,6 +28,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { excalidrawAPI, isCollaborating = false, onPointerUpdate, + renderTopLeftUI, renderTopRightUI, langCode = defaultLang.code, viewModeEnabled, @@ -120,6 +121,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { excalidrawAPI={excalidrawAPI} isCollaborating={isCollaborating} onPointerUpdate={onPointerUpdate} + renderTopLeftUI={renderTopLeftUI} renderTopRightUI={renderTopRightUI} langCode={langCode} viewModeEnabled={viewModeEnabled} diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap index da94b4731f..6f4f6fd559 100644 --- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap @@ -956,6 +956,10 @@ exports[`contextMenu element > right-clicking on a group should select whole gro }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -1151,6 +1155,10 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -1364,6 +1372,10 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -1694,6 +1706,10 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2024,6 +2040,10 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2237,6 +2257,10 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2477,6 +2501,10 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2774,6 +2802,10 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id3": true, }, @@ -3145,6 +3177,10 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -3637,6 +3673,10 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -3959,6 +3999,10 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -4281,6 +4325,10 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id3": true, }, @@ -5565,6 +5613,10 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -6781,6 +6833,10 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -7718,6 +7774,10 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -8714,6 +8774,10 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9707,6 +9771,10 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index 8e0b5dabe0..d436af1375 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -78,6 +78,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id4": true, }, @@ -693,6 +697,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id4": true, }, @@ -1181,6 +1189,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -1544,6 +1556,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -1910,6 +1926,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2169,6 +2189,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2613,6 +2637,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2915,6 +2943,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -3233,6 +3265,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -3526,6 +3562,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -3811,6 +3851,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -4045,6 +4089,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -4301,6 +4349,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -4571,6 +4623,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -4799,6 +4855,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -5027,6 +5087,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -5273,6 +5337,10 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -5528,6 +5596,10 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -5782,6 +5854,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id1": true, }, @@ -6101,7 +6177,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "offsetTop": 0, "openDialog": null, "openMenu": null, - "openPopup": "elementBackground", + "openPopup": null, "openSidebar": null, "originSnapOffset": null, "pasteDialog": { @@ -6110,6 +6186,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id8": true, }, @@ -6536,6 +6616,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id1": true, }, @@ -6912,6 +6996,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -7220,6 +7308,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -7535,6 +7627,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -7764,6 +7860,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -8115,6 +8215,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -8466,6 +8570,10 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -8871,6 +8979,10 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9157,6 +9269,10 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9420,6 +9536,10 @@ exports[`history > multiplayer undo/redo > should not override remote changes on }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9684,6 +9804,10 @@ exports[`history > multiplayer undo/redo > should not override remote changes on }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9918,6 +10042,10 @@ exports[`history > multiplayer undo/redo > should override remotely added groups }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -10211,6 +10339,10 @@ exports[`history > multiplayer undo/redo > should override remotely added points }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -10559,6 +10691,10 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -10797,6 +10933,10 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -11241,6 +11381,10 @@ exports[`history > multiplayer undo/redo > should update history entries after r }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -11500,6 +11644,10 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -11734,6 +11882,10 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -11961,7 +12113,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "offsetTop": 0, "openDialog": null, "openMenu": null, - "openPopup": "elementStroke", + "openPopup": null, "openSidebar": null, "originSnapOffset": null, "pasteDialog": { @@ -11970,6 +12122,10 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -12375,6 +12531,10 @@ exports[`history > singleplayer undo/redo > should create new history entry on e }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -12581,6 +12741,10 @@ exports[`history > singleplayer undo/redo > should create new history entry on e }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -12790,6 +12954,10 @@ exports[`history > singleplayer undo/redo > should create new history entry on i }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -13087,6 +13255,10 @@ exports[`history > singleplayer undo/redo > should create new history entry on i }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -13387,6 +13559,10 @@ exports[`history > singleplayer undo/redo > should create new history entry on s }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": -50, @@ -13628,6 +13804,10 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -13864,6 +14044,10 @@ exports[`history > singleplayer undo/redo > should end up with no history entry }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -14100,6 +14284,10 @@ exports[`history > singleplayer undo/redo > should iterate through the history w }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -14346,6 +14534,10 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -14679,6 +14871,10 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -14845,6 +15041,10 @@ exports[`history > singleplayer undo/redo > should not end up with history entry }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -15131,6 +15331,10 @@ exports[`history > singleplayer undo/redo > should not end up with history entry }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -15393,6 +15597,10 @@ exports[`history > singleplayer undo/redo > should not modify anything on unrela }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -15533,7 +15741,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "offsetTop": 0, "openDialog": null, "openMenu": null, - "openPopup": "elementBackground", + "openPopup": null, "openSidebar": null, "originSnapOffset": null, "pasteDialog": { @@ -15542,6 +15750,10 @@ exports[`history > singleplayer undo/redo > should not override appstate changes }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -15826,6 +16038,10 @@ exports[`history > singleplayer undo/redo > should support appstate name or view }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -15984,6 +16200,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -16688,6 +16908,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -17322,6 +17546,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -17956,6 +18184,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -18674,6 +18906,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -19424,6 +19660,10 @@ exports[`history > singleplayer undo/redo > should support changes in elements' }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -19903,6 +20143,10 @@ exports[`history > singleplayer undo/redo > should support duplication of groups }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id1": true, }, @@ -20413,6 +20657,10 @@ exports[`history > singleplayer undo/redo > should support element creation, del }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id3": true, }, @@ -20871,6 +21119,10 @@ exports[`history > singleplayer undo/redo > should support linear element creati }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap index 761fbc54d9..a33ca9c963 100644 --- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap @@ -79,6 +79,10 @@ exports[`given element A and group of elements B and given both are selected whe }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -504,6 +508,10 @@ exports[`given element A and group of elements B and given both are selected whe }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -919,6 +927,10 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -1484,6 +1496,10 @@ exports[`regression tests > Drags selected element when hitting only bounding bo }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -1690,6 +1706,10 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -2073,6 +2093,10 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -2317,6 +2341,10 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = ` }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2496,6 +2524,10 @@ exports[`regression tests > can drag element that covers another element, while }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id6": true, }, @@ -2820,6 +2852,10 @@ exports[`regression tests > change the properties of a shape > [end of test] app }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -3074,6 +3110,10 @@ exports[`regression tests > click on an element and drag it > [dragged] appState }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -3314,6 +3354,10 @@ exports[`regression tests > click on an element and drag it > [end of test] appS }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -3549,6 +3593,10 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`] }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id3": true, }, @@ -3806,6 +3854,10 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id6": true, }, @@ -4119,6 +4171,10 @@ exports[`regression tests > deleting last but one element in editing group shoul }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -4554,6 +4610,10 @@ exports[`regression tests > deselects group of selected elements on pointer down }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -4836,6 +4896,10 @@ exports[`regression tests > deselects group of selected elements on pointer up w }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -5111,6 +5175,10 @@ exports[`regression tests > deselects selected element on pointer down when poin }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -5318,6 +5386,10 @@ exports[`regression tests > deselects selected element, on pointer up, when clic }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -5517,6 +5589,10 @@ exports[`regression tests > double click to edit a group > [end of test] appStat }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -5909,6 +5985,10 @@ exports[`regression tests > drags selected elements from point inside common bou }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -6205,6 +6285,10 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1` }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -7060,6 +7144,10 @@ exports[`regression tests > given a group of selected elements with an element t }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, "id6": true, @@ -7384,7 +7472,7 @@ exports[`regression tests > given a selected element A and a not selected elemen "offsetTop": 0, "openDialog": null, "openMenu": null, - "openPopup": "elementBackground", + "openPopup": null, "openSidebar": null, "originSnapOffset": null, "pasteDialog": { @@ -7393,6 +7481,10 @@ exports[`regression tests > given a selected element A and a not selected elemen }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -7671,6 +7763,10 @@ exports[`regression tests > given selected element A with lower z-index than uns }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -7905,6 +8001,10 @@ exports[`regression tests > given selected element A with lower z-index than uns }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -8144,6 +8244,10 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -8323,6 +8427,10 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -8502,6 +8610,10 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -8681,6 +8793,10 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1` }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -8910,6 +9026,10 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`] }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9137,6 +9257,10 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9332,6 +9456,10 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1` }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9561,6 +9689,10 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9740,6 +9872,10 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`] }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9967,6 +10103,10 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -10146,6 +10286,10 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -10341,6 +10485,10 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -10520,6 +10668,10 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -11050,6 +11202,10 @@ exports[`regression tests > noop interaction after undo shouldn't create history }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -11329,6 +11485,10 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = ` }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": "-6.25000", @@ -11451,6 +11611,10 @@ exports[`regression tests > shift click on selected element should deselect it o }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -11650,6 +11814,10 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -11968,6 +12136,10 @@ exports[`regression tests > should group elements and ungroup them > [end of tes }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -12396,6 +12568,10 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, "id15": true, @@ -13038,6 +13214,10 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 60, @@ -13160,6 +13340,10 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`] }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id0": true, }, @@ -13790,6 +13974,10 @@ exports[`regression tests > switches from group of selected elements to another }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id3": true, "id6": true, @@ -14128,6 +14316,10 @@ exports[`regression tests > switches selected element on pointer down > [end of }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": { "id3": true, }, @@ -14391,6 +14583,10 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`] }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 20, @@ -14513,6 +14709,10 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -14904,6 +15104,10 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -15029,6 +15233,10 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = ` }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": true, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index 65f330ae24..3b4d2eb478 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -316,6 +316,10 @@ export interface AppState { // indicates if the current tool is temporarily switched on from the selection tool fromSelection: boolean; } & ActiveTool; + preferredSelectionTool: { + type: "selection" | "lasso"; + initialized: boolean; + }; penMode: boolean; penDetected: boolean; exportBackground: boolean; @@ -364,7 +368,6 @@ export interface AppState { | { name: "ttd"; tab: "text-to-diagram" | "mermaid" } | { name: "commandPalette" } | { name: "elementLinkSelector"; sourceElementId: ExcalidrawElement["id"] }; - /** * Reflects user preference for whether the default sidebar should be docked. * @@ -448,7 +451,7 @@ export interface AppState { lockedMultiSelections: { [groupId: string]: true }; /** properties sidebar mode - determines whether to show compact or complete sidebar */ - stylesPanelMode: "compact" | "full"; + stylesPanelMode: "compact" | "full" | "mobile"; } export type SearchMatch = { @@ -571,6 +574,10 @@ export interface ExcalidrawProps { /** excludes the duplicated elements */ prevElements: readonly ExcalidrawElement[], ) => ExcalidrawElement[] | void; + renderTopLeftUI?: ( + isMobile: boolean, + appState: UIAppState, + ) => JSX.Element | null; renderTopRightUI?: ( isMobile: boolean, appState: UIAppState, @@ -738,8 +745,7 @@ export type AppClassProperties = { onPointerUpEmitter: App["onPointerUpEmitter"]; updateEditorAtom: App["updateEditorAtom"]; - - defaultSelectionTool: "selection" | "lasso"; + onPointerDownEmitter: App["onPointerDownEmitter"]; }; export type PointerDownState = Readonly<{ diff --git a/packages/utils/tests/__snapshots__/export.test.ts.snap b/packages/utils/tests/__snapshots__/export.test.ts.snap index 20f3ee28d9..1f799501c9 100644 --- a/packages/utils/tests/__snapshots__/export.test.ts.snap +++ b/packages/utils/tests/__snapshots__/export.test.ts.snap @@ -80,6 +80,10 @@ exports[`exportToSvg > with default arguments 1`] = ` }, "penDetected": false, "penMode": false, + "preferredSelectionTool": { + "initialized": false, + "type": "selection", + }, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0,