mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-10-26 16:34:22 +01:00
fix: mobile UI and other fixes (#10177)
* remove legacy openMenu=shape state and unused actions * close menus/popups in applicable cases when opening a different one * split ui z-indexes to account prefer different overlap * make top canvas area clickable on mobile * make mobile main menu closable by clicking outside and reduce width * offset picker popups from viewport border on mobile * reduce items gap in mobile main menu * show top picks for canvas bg colors in all ui modes * fix menu separator visibility on mobile * fix command palette items not being filtered
This commit is contained in:
committed by
Mark Tolmacs
parent
9ea7ea5983
commit
b228d92e9b
@@ -1,65 +1,11 @@
|
|||||||
import { KEYS } from "@excalidraw/common";
|
import { KEYS } from "@excalidraw/common";
|
||||||
|
|
||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import { showSelectedShapeActions } from "@excalidraw/element";
|
|
||||||
|
|
||||||
import { CaptureUpdateAction } from "@excalidraw/element";
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { HelpIconThin } from "../components/icons";
|
||||||
import { HamburgerMenuIcon, HelpIconThin, palette } from "../components/icons";
|
|
||||||
import { t } from "../i18n";
|
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
export const actionToggleCanvasMenu = register({
|
|
||||||
name: "toggleCanvasMenu",
|
|
||||||
label: "buttons.menu",
|
|
||||||
trackEvent: { category: "menu" },
|
|
||||||
perform: (_, appState) => ({
|
|
||||||
appState: {
|
|
||||||
...appState,
|
|
||||||
openMenu: appState.openMenu === "canvas" ? null : "canvas",
|
|
||||||
},
|
|
||||||
captureUpdate: CaptureUpdateAction.EVENTUALLY,
|
|
||||||
}),
|
|
||||||
PanelComponent: ({ appState, updateData }) => (
|
|
||||||
<ToolButton
|
|
||||||
type="button"
|
|
||||||
icon={HamburgerMenuIcon}
|
|
||||||
aria-label={t("buttons.menu")}
|
|
||||||
onClick={updateData}
|
|
||||||
selected={appState.openMenu === "canvas"}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const actionToggleEditMenu = register({
|
|
||||||
name: "toggleEditMenu",
|
|
||||||
label: "buttons.edit",
|
|
||||||
trackEvent: { category: "menu" },
|
|
||||||
perform: (_elements, appState) => ({
|
|
||||||
appState: {
|
|
||||||
...appState,
|
|
||||||
openMenu: appState.openMenu === "shape" ? null : "shape",
|
|
||||||
},
|
|
||||||
captureUpdate: CaptureUpdateAction.EVENTUALLY,
|
|
||||||
}),
|
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
|
||||||
<ToolButton
|
|
||||||
visible={showSelectedShapeActions(
|
|
||||||
appState,
|
|
||||||
getNonDeletedElements(elements),
|
|
||||||
)}
|
|
||||||
type="button"
|
|
||||||
icon={palette}
|
|
||||||
aria-label={t("buttons.edit")}
|
|
||||||
onClick={updateData}
|
|
||||||
selected={appState.openMenu === "shape"}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const actionShortcuts = register({
|
export const actionShortcuts = register({
|
||||||
name: "toggleShortcuts",
|
name: "toggleShortcuts",
|
||||||
label: "welcomeScreen.defaults.helpHint",
|
label: "welcomeScreen.defaults.helpHint",
|
||||||
@@ -79,6 +25,8 @@ export const actionShortcuts = register({
|
|||||||
: {
|
: {
|
||||||
name: "help",
|
name: "help",
|
||||||
},
|
},
|
||||||
|
openMenu: null,
|
||||||
|
openPopup: null,
|
||||||
},
|
},
|
||||||
captureUpdate: CaptureUpdateAction.EVENTUALLY,
|
captureUpdate: CaptureUpdateAction.EVENTUALLY,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,11 +44,7 @@ export {
|
|||||||
} from "./actionExport";
|
} from "./actionExport";
|
||||||
|
|
||||||
export { actionCopyStyles, actionPasteStyles } from "./actionStyles";
|
export { actionCopyStyles, actionPasteStyles } from "./actionStyles";
|
||||||
export {
|
export { actionShortcuts } from "./actionMenu";
|
||||||
actionToggleCanvasMenu,
|
|
||||||
actionToggleEditMenu,
|
|
||||||
actionShortcuts,
|
|
||||||
} from "./actionMenu";
|
|
||||||
|
|
||||||
export { actionGroup, actionUngroup } from "./actionGroup";
|
export { actionGroup, actionUngroup } from "./actionGroup";
|
||||||
|
|
||||||
|
|||||||
@@ -72,8 +72,6 @@ export type ActionName =
|
|||||||
| "changeArrowProperties"
|
| "changeArrowProperties"
|
||||||
| "changeOpacity"
|
| "changeOpacity"
|
||||||
| "changeFontSize"
|
| "changeFontSize"
|
||||||
| "toggleCanvasMenu"
|
|
||||||
| "toggleEditMenu"
|
|
||||||
| "undo"
|
| "undo"
|
||||||
| "redo"
|
| "redo"
|
||||||
| "finalize"
|
| "finalize"
|
||||||
|
|||||||
@@ -1178,7 +1178,10 @@ export const ShapesSwitcher = ({
|
|||||||
// on top of it
|
// on top of it
|
||||||
(laserToolSelected && !app.props.isCollaborating),
|
(laserToolSelected && !app.props.isCollaborating),
|
||||||
})}
|
})}
|
||||||
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
|
onToggle={() => {
|
||||||
|
setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen);
|
||||||
|
setAppState({ openMenu: null, openPopup: null });
|
||||||
|
}}
|
||||||
title={t("toolBar.extraTools")}
|
title={t("toolBar.extraTools")}
|
||||||
>
|
>
|
||||||
{frameToolSelected
|
{frameToolSelected
|
||||||
|
|||||||
@@ -319,8 +319,9 @@ export const ColorPicker = ({
|
|||||||
openRef.current = appState.openPopup;
|
openRef.current = appState.openPopup;
|
||||||
}, [appState.openPopup]);
|
}, [appState.openPopup]);
|
||||||
const compactMode =
|
const compactMode =
|
||||||
appState.stylesPanelMode === "compact" ||
|
type !== "canvasBackground" &&
|
||||||
appState.stylesPanelMode === "mobile";
|
(appState.stylesPanelMode === "compact" ||
|
||||||
|
appState.stylesPanelMode === "mobile");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -476,7 +476,6 @@ function CommandPaletteInner({
|
|||||||
},
|
},
|
||||||
perform: () => {
|
perform: () => {
|
||||||
setAppState((prevState) => ({
|
setAppState((prevState) => ({
|
||||||
openMenu: prevState.openMenu === "shape" ? null : "shape",
|
|
||||||
openPopup: "elementStroke",
|
openPopup: "elementStroke",
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
@@ -496,7 +495,6 @@ function CommandPaletteInner({
|
|||||||
},
|
},
|
||||||
perform: () => {
|
perform: () => {
|
||||||
setAppState((prevState) => ({
|
setAppState((prevState) => ({
|
||||||
openMenu: prevState.openMenu === "shape" ? null : "shape",
|
|
||||||
openPopup: "elementBackground",
|
openPopup: "elementBackground",
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
@@ -838,7 +836,12 @@ function CommandPaletteInner({
|
|||||||
|
|
||||||
let matchingCommands =
|
let matchingCommands =
|
||||||
commandSearch?.length > 1
|
commandSearch?.length > 1
|
||||||
? [...allCommands, ...libraryCommands]
|
? [
|
||||||
|
...allCommands
|
||||||
|
.filter(isCommandAvailable)
|
||||||
|
.sort((a, b) => a.order - b.order),
|
||||||
|
...libraryCommands,
|
||||||
|
]
|
||||||
: allCommands
|
: allCommands
|
||||||
.filter(isCommandAvailable)
|
.filter(isCommandAvailable)
|
||||||
.sort((a, b) => a.order - b.order);
|
.sort((a, b) => a.order - b.order);
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ function Picker<T>({
|
|||||||
side={isMobile ? "right" : "bottom"}
|
side={isMobile ? "right" : "bottom"}
|
||||||
align="start"
|
align="start"
|
||||||
sideOffset={isMobile ? 8 : 12}
|
sideOffset={isMobile ? 8 : 12}
|
||||||
style={{ zIndex: "var(--zIndex-popup)" }}
|
style={{ zIndex: "var(--zIndex-ui-styles-popup)" }}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -133,6 +133,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.layer-ui__library .library-menu-dropdown-container {
|
.layer-ui__library .library-menu-dropdown-container {
|
||||||
|
z-index: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
&--in-heading {
|
&--in-heading {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|||||||
@@ -374,7 +374,10 @@ export const MobileToolBar = ({
|
|||||||
extraToolSelected || isOtherShapesMenuOpen,
|
extraToolSelected || isOtherShapesMenuOpen,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
onToggle={() => setIsOtherShapesMenuOpen(!isOtherShapesMenuOpen)}
|
onToggle={() => {
|
||||||
|
setIsOtherShapesMenuOpen(!isOtherShapesMenuOpen);
|
||||||
|
setAppState({ openMenu: null, openPopup: null });
|
||||||
|
}}
|
||||||
title={t("toolBar.extraTools")}
|
title={t("toolBar.extraTools")}
|
||||||
style={{
|
style={{
|
||||||
width: WIDTH,
|
width: WIDTH,
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ export const PropertiesPopover = React.forwardRef<
|
|||||||
alignOffset={-16}
|
alignOffset={-16}
|
||||||
sideOffset={20}
|
sideOffset={20}
|
||||||
style={{
|
style={{
|
||||||
zIndex: "var(--zIndex-popup)",
|
zIndex: "var(--zIndex-ui-styles-popup)",
|
||||||
|
marginLeft: device.editor.isMobile ? "0.5rem" : undefined,
|
||||||
}}
|
}}
|
||||||
onPointerLeave={onPointerLeave}
|
onPointerLeave={onPointerLeave}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 5;
|
z-index: var(--zIndex-ui-library);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
@@ -30,7 +30,11 @@ export const SidebarTrigger = ({
|
|||||||
.querySelector(".layer-ui__wrapper")
|
.querySelector(".layer-ui__wrapper")
|
||||||
?.classList.remove("animate");
|
?.classList.remove("animate");
|
||||||
const isOpen = event.target.checked;
|
const isOpen = event.target.checked;
|
||||||
setAppState({ openSidebar: isOpen ? { name, tab } : null });
|
setAppState({
|
||||||
|
openSidebar: isOpen ? { name, tab } : null,
|
||||||
|
openMenu: null,
|
||||||
|
openPopup: null,
|
||||||
|
});
|
||||||
onToggle?.(isOpen);
|
onToggle?.(isOpen);
|
||||||
}}
|
}}
|
||||||
checked={appState.openSidebar?.name === name}
|
checked={appState.openSidebar?.name === name}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2.5rem;
|
top: 2.5rem;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
|
max-width: 16rem;
|
||||||
|
|
||||||
&--placement-top {
|
&--placement-top {
|
||||||
top: auto;
|
top: auto;
|
||||||
@@ -20,10 +21,8 @@
|
|||||||
// When main menu is in the top toolbar, position relative to trigger
|
// When main menu is in the top toolbar, position relative to trigger
|
||||||
&.main-menu-dropdown {
|
&.main-menu-dropdown {
|
||||||
min-width: 232px;
|
min-width: 232px;
|
||||||
max-width: calc(100vw - var(--editor-container-padding) * 2);
|
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
z-index: var(--zIndex-layerUI);
|
|
||||||
|
|
||||||
@media screen and (orientation: landscape) {
|
@media screen and (orientation: landscape) {
|
||||||
max-width: 232px;
|
max-width: 232px;
|
||||||
|
|||||||
@@ -74,7 +74,12 @@ const MenuContent = ({
|
|||||||
{/* the zIndex ensures this menu has higher stacking order,
|
{/* the zIndex ensures this menu has higher stacking order,
|
||||||
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
||||||
{device.editor.isMobile ? (
|
{device.editor.isMobile ? (
|
||||||
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
<Stack.Col
|
||||||
|
className="dropdown-menu-container"
|
||||||
|
style={{ ["--gap" as any]: 1.25 }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Stack.Col>
|
||||||
) : (
|
) : (
|
||||||
<Island
|
<Island
|
||||||
className="dropdown-menu-container"
|
className="dropdown-menu-container"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const MenuSeparator = () => (
|
|||||||
height: "1px",
|
height: "1px",
|
||||||
backgroundColor: "var(--default-border-color)",
|
backgroundColor: "var(--default-border-color)",
|
||||||
margin: ".5rem 0",
|
margin: ".5rem 0",
|
||||||
|
flex: "0 0 auto",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ const MainMenu = Object.assign(
|
|||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const appState = useUIAppState();
|
const appState = useUIAppState();
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
const onClickOutside = device.editor.isMobile
|
|
||||||
? undefined
|
|
||||||
: () => setAppState({ openMenu: null });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainMenuTunnel.In>
|
<MainMenuTunnel.In>
|
||||||
@@ -41,6 +38,8 @@ const MainMenu = Object.assign(
|
|||||||
onToggle={() => {
|
onToggle={() => {
|
||||||
setAppState({
|
setAppState({
|
||||||
openMenu: appState.openMenu === "canvas" ? null : "canvas",
|
openMenu: appState.openMenu === "canvas" ? null : "canvas",
|
||||||
|
openPopup: null,
|
||||||
|
openDialog: null,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
data-testid="main-menu-trigger"
|
data-testid="main-menu-trigger"
|
||||||
@@ -49,7 +48,7 @@ const MainMenu = Object.assign(
|
|||||||
{HamburgerMenuIcon}
|
{HamburgerMenuIcon}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
onClickOutside={onClickOutside}
|
onClickOutside={() => setAppState({ openMenu: null })}
|
||||||
onSelect={composeEventHandlers(onSelect, () => {
|
onSelect={composeEventHandlers(onSelect, () => {
|
||||||
setAppState({ openMenu: null });
|
setAppState({ openMenu: null });
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -12,6 +12,11 @@
|
|||||||
--zIndex-eyeDropperPreview: 6;
|
--zIndex-eyeDropperPreview: 6;
|
||||||
--zIndex-hyperlinkContainer: 7;
|
--zIndex-hyperlinkContainer: 7;
|
||||||
|
|
||||||
|
--zIndex-ui-styles-popup: 40;
|
||||||
|
--zIndex-ui-bottom: 60;
|
||||||
|
--zIndex-ui-library: 80;
|
||||||
|
--zIndex-ui-top: 100;
|
||||||
|
|
||||||
--zIndex-modal: 1000;
|
--zIndex-modal: 1000;
|
||||||
--zIndex-popup: 1001;
|
--zIndex-popup: 1001;
|
||||||
--zIndex-toast: 999999;
|
--zIndex-toast: 999999;
|
||||||
@@ -237,7 +242,7 @@ body.excalidraw-cursor-resize * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.App-top-bar {
|
.App-top-bar {
|
||||||
z-index: var(--zIndex-layerUI);
|
z-index: var(--zIndex-ui-top);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -255,7 +260,7 @@ body.excalidraw-cursor-resize * {
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
--bar-padding: calc(4 * var(--space-factor));
|
--bar-padding: calc(4 * var(--space-factor));
|
||||||
z-index: var(--zIndex-layerUI);
|
z-index: var(--zIndex-ui-bottom);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
@@ -296,6 +301,12 @@ body.excalidraw-cursor-resize * {
|
|||||||
.App-toolbar-content {
|
.App-toolbar-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
pointer-events: var(--ui-pointerEvents);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-mobile-menu {
|
.App-mobile-menu {
|
||||||
|
|||||||
@@ -414,7 +414,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
style="height: 1px; margin: .5rem 0px;"
|
style="height: 1px; margin: .5rem 0px; flex: 0 0 auto;"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="dropdown-menu-group "
|
class="dropdown-menu-group "
|
||||||
@@ -558,7 +558,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style="height: 1px; margin: .5rem 0px;"
|
style="height: 1px; margin: .5rem 0px; flex: 0 0 auto;"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
aria-label="Dark mode"
|
aria-label="Dark mode"
|
||||||
|
|||||||
@@ -356,7 +356,7 @@ export interface AppState {
|
|||||||
isResizing: boolean;
|
isResizing: boolean;
|
||||||
isRotating: boolean;
|
isRotating: boolean;
|
||||||
zoom: Zoom;
|
zoom: Zoom;
|
||||||
openMenu: "canvas" | "shape" | null;
|
openMenu: "canvas" | null;
|
||||||
openPopup:
|
openPopup:
|
||||||
| "canvasBackground"
|
| "canvasBackground"
|
||||||
| "elementBackground"
|
| "elementBackground"
|
||||||
|
|||||||
Reference in New Issue
Block a user