fix: mobile view ui issues (#10284)

* hide zen mode when formFactor = phone

* tool bar fixes: icon and width

* view mode

* fix lint

* add exit-view-mode button

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Ryan Di
2025-11-04 22:20:55 +11:00
committed by Mark Tolmacs
parent ee2856325c
commit 62c932982a
10 changed files with 154 additions and 66 deletions

View File

@@ -852,7 +852,7 @@ const ExcalidrawWrapper = () => {
return null;
}
return (
<div className="top-right-ui">
<div className="excalidraw-ui-top-right">
{collabError.message && <CollabError collabError={collabError} />}
<LiveCollaborationTrigger
isCollaborating={isCollaborating}

View File

@@ -5,12 +5,6 @@
--color-primary-contrast-offset: #726dff; // to offset Chubb illusion
}
.top-right-ui {
display: flex;
justify-content: center;
align-items: flex-start;
}
.footer-center {
justify-content: flex-end;
margin-top: auto;

View File

@@ -25,8 +25,11 @@ export const actionToggleZenMode = register({
};
},
checked: (appState) => appState.zenModeEnabled,
predicate: (elements, appState, appProps) => {
return typeof appProps.zenModeEnabled === "undefined";
predicate: (elements, appState, appProps, app) => {
return (
app.editorInterface.formFactor !== "phone" &&
typeof appProps.zenModeEnabled === "undefined"
);
},
keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.Z,

View File

@@ -49,6 +49,8 @@ import { getFormValue } from "../actions/actionProperties";
import { useTextEditorFocus } from "../hooks/useTextEditorFocus";
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
import { getToolbarTools } from "./shapes";
import "./Actions.scss";
@@ -79,6 +81,7 @@ import {
adjustmentsIcon,
DotsHorizontalIcon,
SelectionIcon,
pencilIcon,
} from "./icons";
import { Island } from "./Island";
@@ -1304,7 +1307,7 @@ export const UndoRedoActions = ({
</div>
);
export const ExitZenModeAction = ({
export const ExitZenModeButton = ({
actionManager,
showExitZenModeBtn,
}: {
@@ -1321,3 +1324,17 @@ export const ExitZenModeAction = ({
{t("buttons.exitZenMode")}
</button>
);
export const ExitViewModeButton = ({
actionManager,
}: {
actionManager: ActionManager;
}) => (
<button
type="button"
className="disable-view-mode"
onClick={() => actionManager.executeAction(actionToggleViewMode)}
>
{pencilIcon}
</button>
);

View File

@@ -120,4 +120,53 @@
margin-bottom: auto;
}
}
.disable-view-mode {
display: flex;
justify-content: center;
cursor: pointer;
align-items: center;
border: 1px solid var(--color-primary);
padding: 0.5rem;
border-radius: var(--border-radius-lg);
background-color: var(--island-bg-color);
text-decoration: none !important;
font-family: var(--ui-font);
font-size: 0.8333rem;
box-sizing: border-box;
width: var(--mobile-action-button-size, var(--default-button-size));
height: var(--mobile-action-button-size, var(--default-button-size));
border: none;
box-shadow: 0 0 0 1px var(--color-surface-lowest);
background-color: var(--color-surface-low);
color: var(--button-color, var(--color-on-surface)) !important;
&:active {
box-shadow: 0 0 0 1px var(--color-brand-active);
}
&:hover {
background-color: var(--color-primary);
color: white !important;
}
&:active {
background-color: var(--color-primary-darker);
}
svg {
width: 20px;
height: 20px;
}
}
.theme--dark {
.plus-banner {
&:hover {
color: black !important;
}
}
}
}

View File

@@ -7,7 +7,7 @@ import { t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
import { MobileShapeActions } from "./Actions";
import { ExitViewModeButton, MobileShapeActions } from "./Actions";
import { MobileToolBar } from "./MobileToolBar";
import { FixedSideContainer } from "./FixedSideContainer";
@@ -65,8 +65,18 @@ export const MobileMenu = ({
DefaultSidebarTriggerTunnel,
} = useTunnels();
const renderAppTopBar = () => {
const topRightUI = renderTopRightUI?.(true, appState) ?? (
<DefaultSidebarTriggerTunnel.Out />
if (appState.openDialog?.name === "elementLinkSelector") {
return null;
}
const topRightUI = (
<div className="excalidraw-ui-top-right">
{renderTopRightUI?.(true, appState) ??
(!appState.viewModeEnabled && <DefaultSidebarTriggerTunnel.Out />)}
{appState.viewModeEnabled && (
<ExitViewModeButton actionManager={actionManager} />
)}
</div>
);
const topLeftUI = (
@@ -76,13 +86,6 @@ export const MobileMenu = ({
</div>
);
if (
appState.viewModeEnabled ||
appState.openDialog?.name === "elementLinkSelector"
) {
return <div className="App-toolbar-content">{topLeftUI}</div>;
}
return (
<div
className="App-toolbar-content"
@@ -117,41 +120,43 @@ export const MobileMenu = ({
{renderWelcomeScreen && <WelcomeScreenCenterTunnel.Out />}
</div>
<div
className="App-bottom-bar"
style={{
marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN,
}}
>
<MobileShapeActions
appState={appState}
elementsMap={app.scene.getNonDeletedElementsMap()}
renderAction={actionManager.renderAction}
app={app}
setAppState={setAppState}
/>
{!appState.viewModeEnabled && (
<div
className="App-bottom-bar"
style={{
marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN,
}}
>
<MobileShapeActions
appState={appState}
elementsMap={app.scene.getNonDeletedElementsMap()}
renderAction={actionManager.renderAction}
app={app}
setAppState={setAppState}
/>
<Island className="App-toolbar">
{!appState.viewModeEnabled &&
appState.openDialog?.name !== "elementLinkSelector" &&
renderToolbar()}
{appState.scrolledOutside &&
!appState.openMenu &&
!appState.openSidebar && (
<button
type="button"
className="scroll-back-to-content"
onClick={() => {
setAppState((appState) => ({
...calculateScrollCenter(elements, appState),
}));
}}
>
{t("buttons.scrollBackToContent")}
</button>
)}
</Island>
</div>
<Island className="App-toolbar">
{!appState.viewModeEnabled &&
appState.openDialog?.name !== "elementLinkSelector" &&
renderToolbar()}
{appState.scrolledOutside &&
!appState.openMenu &&
!appState.openSidebar && (
<button
type="button"
className="scroll-back-to-content"
onClick={() => {
setAppState((appState) => ({
...calculateScrollCenter(elements, appState),
}));
}}
>
{t("buttons.scrollBackToContent")}
</button>
)}
</Island>
</div>
)}
<FixedSideContainer side="top" className="App-top-bar">
{renderAppTopBar()}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from "react";
import { useState, useEffect } from "react";
import clsx from "clsx";
import { KEYS, capitalizeString } from "@excalidraw/common";
@@ -101,8 +101,6 @@ export const MobileToolBar = ({
"arrow" | "line"
>("arrow");
const toolbarRef = useRef<HTMLDivElement>(null);
// keep lastActiveGenericShape in sync with active tool if user switches via other UI
useEffect(() => {
if (
@@ -143,8 +141,8 @@ export const MobileToolBar = ({
}
};
const toolbarWidth =
toolbarRef.current?.getBoundingClientRect()?.width ?? 0 - 8;
const [toolbarWidth, setToolbarWidth] = useState(0);
const WIDTH = 36;
const GAP = 4;
@@ -164,6 +162,9 @@ export const MobileToolBar = ({
"laser",
"magicframe",
].filter((tool) => {
if (showTextToolOutside && tool === "text") {
return false;
}
if (showImageToolOutside && tool === "image") {
return false;
}
@@ -174,21 +175,30 @@ export const MobileToolBar = ({
});
const extraToolSelected = extraTools.includes(activeTool.type);
const extraIcon = extraToolSelected
? activeTool.type === "frame"
? activeTool.type === "text"
? TextIcon
: activeTool.type === "image"
? ImageIcon
: activeTool.type === "frame"
? frameToolIcon
: activeTool.type === "embeddable"
? EmbedIcon
: activeTool.type === "laser"
? laserPointerToolIcon
: activeTool.type === "text"
? TextIcon
: activeTool.type === "magicframe"
? MagicIcon
: extraToolsIcon
: extraToolsIcon;
return (
<div className="mobile-toolbar" ref={toolbarRef}>
<div
className="mobile-toolbar"
ref={(div) => {
if (div) {
setToolbarWidth(div.getBoundingClientRect().width);
}
}}
>
{/* Hand Tool */}
<HandButton
checked={isHandToolActive(app.state)}

View File

@@ -2,7 +2,7 @@ import clsx from "clsx";
import { actionShortcuts } from "../../actions";
import { useTunnels } from "../../context/tunnels";
import { ExitZenModeAction, UndoRedoActions, ZoomActions } from "../Actions";
import { ExitZenModeButton, UndoRedoActions, ZoomActions } from "../Actions";
import { HelpButton } from "../HelpButton";
import { Section } from "../Section";
import Stack from "../Stack";
@@ -66,7 +66,7 @@ const Footer = ({
/>
</div>
</div>
<ExitZenModeAction
<ExitZenModeButton
actionManager={actionManager}
showExitZenModeBtn={showExitZenModeBtn}
/>

View File

@@ -2336,3 +2336,12 @@ export const strokeIcon = createIcon(
</g>,
tablerIconProps,
);
export const pencilIcon = createIcon(
<g strokeWidth={1.25}>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4" />
<path d="M13.5 6.5l4 4" />
</g>,
tablerIconProps,
);

View File

@@ -293,7 +293,8 @@ body.excalidraw-cursor-resize * {
}
}
.excalidraw-ui-top-left {
.excalidraw-ui-top-left,
.excalidraw-ui-top-right {
display: flex;
align-items: center;
gap: 0.5rem;