mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-11-08 06:44:23 +01:00
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:
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user