feat: use component dimensions to break to mobile (#3414)
Co-authored-by: Jed Fox <git@jedfox.com>
This commit is contained in:
parent
016e69b9f2
commit
09dfd16b17
@ -8,7 +8,7 @@ import { getCommonBounds, getNonDeletedElements } from "../element";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import { useIsMobile } from "../components/App";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { getNormalizedZoom, getSelectedElements } from "../scene";
|
||||
import { centerScrollOn } from "../scene/scroll";
|
||||
|
@ -8,7 +8,7 @@ import { Tooltip } from "../components/Tooltip";
|
||||
import { DarkModeToggle, Appearence } from "../components/DarkModeToggle";
|
||||
import { loadFromJSON, saveAsJSON } from "../data";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import { useIsMobile } from "../components/App";
|
||||
import { KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
import { supported } from "browser-fs-access";
|
||||
|
@ -3,7 +3,7 @@ import { ActionManager } from "../actions/manager";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import { useIsMobile } from "../components/App";
|
||||
import {
|
||||
canChangeSharpness,
|
||||
canHaveArrowheads,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Point, simplify } from "points-on-curve";
|
||||
import React from "react";
|
||||
import React, { useContext } from "react";
|
||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||
import rough from "roughjs/bin/rough";
|
||||
import clsx from "clsx";
|
||||
@ -54,6 +54,9 @@ import {
|
||||
GRID_SIZE,
|
||||
LINE_CONFIRM_THRESHOLD,
|
||||
MIME_TYPES,
|
||||
MQ_MAX_HEIGHT_LANDSCAPE,
|
||||
MQ_MAX_WIDTH_LANDSCAPE,
|
||||
MQ_MAX_WIDTH_PORTRAIT,
|
||||
POINTER_BUTTON,
|
||||
SCROLL_TIMEOUT,
|
||||
TAP_TWICE_TIMEOUT,
|
||||
@ -178,13 +181,15 @@ import {
|
||||
viewportCoordsToSceneCoords,
|
||||
withBatchedUpdates,
|
||||
} from "../utils";
|
||||
import { isMobile } from "../is-mobile";
|
||||
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
|
||||
import LayerUI from "./LayerUI";
|
||||
import { Stats } from "./Stats";
|
||||
import { Toast } from "./Toast";
|
||||
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
||||
|
||||
export const IsMobileContext = React.createContext(false);
|
||||
export const useIsMobile = () => useContext(IsMobileContext);
|
||||
|
||||
const { history } = createHistory();
|
||||
|
||||
let didTapTwice: boolean = false;
|
||||
@ -286,6 +291,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
rc: RoughCanvas | null = null;
|
||||
unmounted: boolean = false;
|
||||
actionManager: ActionManager;
|
||||
isMobile = false;
|
||||
detachIsMobileMqHandler?: () => void;
|
||||
|
||||
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
public static defaultProps: Partial<AppProps> = {
|
||||
@ -437,10 +445,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
<div
|
||||
className={clsx("excalidraw", {
|
||||
"excalidraw--view-mode": viewModeEnabled,
|
||||
"excalidraw--mobile": this.isMobile,
|
||||
})}
|
||||
ref={this.excalidrawContainerRef}
|
||||
onDrop={this.handleAppOnDrop}
|
||||
>
|
||||
<IsMobileContext.Provider value={this.isMobile}>
|
||||
<LayerUI
|
||||
canvas={this.canvas}
|
||||
appState={this.state}
|
||||
@ -464,7 +474,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
renderCustomFooter={renderFooter}
|
||||
viewModeEnabled={viewModeEnabled}
|
||||
showExitZenModeBtn={
|
||||
typeof this.props?.zenModeEnabled === "undefined" && zenModeEnabled
|
||||
typeof this.props?.zenModeEnabled === "undefined" &&
|
||||
zenModeEnabled
|
||||
}
|
||||
showThemeBtn={
|
||||
typeof this.props?.theme === "undefined" &&
|
||||
@ -491,6 +502,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
/>
|
||||
)}
|
||||
<main>{this.renderCanvas()}</main>
|
||||
</IsMobileContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -776,10 +788,29 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
// compute isMobile state
|
||||
// ---------------------------------------------------------------------
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
} = this.excalidrawContainerRef.current!.getBoundingClientRect();
|
||||
this.isMobile =
|
||||
width < MQ_MAX_WIDTH_PORTRAIT ||
|
||||
(height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE);
|
||||
// refresh offsets
|
||||
// ---------------------------------------------------------------------
|
||||
this.updateDOMRect();
|
||||
});
|
||||
this.resizeObserver?.observe(this.excalidrawContainerRef.current);
|
||||
} else if (window.matchMedia) {
|
||||
const mediaQuery = window.matchMedia(
|
||||
`(max-width: ${MQ_MAX_WIDTH_PORTRAIT}px), (max-height: ${MQ_MAX_HEIGHT_LANDSCAPE}px) and (max-width: ${MQ_MAX_WIDTH_LANDSCAPE}px)`,
|
||||
);
|
||||
const handler = () => (this.isMobile = mediaQuery.matches);
|
||||
mediaQuery.addListener(handler);
|
||||
this.detachIsMobileMqHandler = () => mediaQuery.removeListener(handler);
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search.slice(1));
|
||||
|
||||
if (searchParams.has("web-share-target")) {
|
||||
@ -839,6 +870,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.onGestureEnd as any,
|
||||
false,
|
||||
);
|
||||
|
||||
this.detachIsMobileMqHandler?.();
|
||||
}
|
||||
|
||||
private addEventListeners() {
|
||||
@ -1016,7 +1049,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
{
|
||||
renderOptimizations: true,
|
||||
renderScrollbars: !isMobile(),
|
||||
renderScrollbars: !this.isMobile,
|
||||
},
|
||||
);
|
||||
if (scrollBars) {
|
||||
@ -3811,8 +3844,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
const separator = "separator";
|
||||
|
||||
const _isMobile = isMobile();
|
||||
|
||||
const elements = this.scene.getElements();
|
||||
|
||||
const options: ContextMenuOption[] = [];
|
||||
@ -3849,7 +3880,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
ContextMenu.push({
|
||||
options: [
|
||||
_isMobile &&
|
||||
this.isMobile &&
|
||||
navigator.clipboard && {
|
||||
name: "paste",
|
||||
perform: (elements, appStates) => {
|
||||
@ -3860,7 +3891,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
contextItemLabel: "labels.paste",
|
||||
},
|
||||
_isMobile && navigator.clipboard && separator,
|
||||
this.isMobile && navigator.clipboard && separator,
|
||||
probablySupportsClipboardBlob &&
|
||||
elements.length > 0 &&
|
||||
actionCopyAsPng,
|
||||
@ -3903,9 +3934,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
ContextMenu.push({
|
||||
options: [
|
||||
_isMobile && actionCut,
|
||||
_isMobile && navigator.clipboard && actionCopy,
|
||||
_isMobile &&
|
||||
this.isMobile && actionCut,
|
||||
this.isMobile && navigator.clipboard && actionCopy,
|
||||
this.isMobile &&
|
||||
navigator.clipboard && {
|
||||
name: "paste",
|
||||
perform: (elements, appStates) => {
|
||||
@ -3916,7 +3947,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
contextItemLabel: "labels.paste",
|
||||
},
|
||||
_isMobile && separator,
|
||||
this.isMobile && separator,
|
||||
...options,
|
||||
separator,
|
||||
actionCopyStyles,
|
||||
|
@ -2,7 +2,7 @@ import React from "react";
|
||||
import clsx from "clsx";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import { useIsMobile } from "../components/App";
|
||||
import { users } from "./icons";
|
||||
|
||||
import "./CollabButton.scss";
|
||||
|
@ -218,7 +218,7 @@
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
@media #{$is-mobile-query} {
|
||||
@include isMobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -76,7 +76,7 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@media #{$is-mobile-query} {
|
||||
@include isMobile {
|
||||
.context-menu-option {
|
||||
display: block;
|
||||
|
||||
|
@ -31,7 +31,7 @@
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
@media #{$is-mobile-query} {
|
||||
@include isMobile {
|
||||
.Dialog {
|
||||
--metric: calc(var(--space-factor) * 4);
|
||||
--inset-left: #{"max(var(--metric), var(--sal))"};
|
||||
|
@ -2,7 +2,7 @@ import clsx from "clsx";
|
||||
import React, { useEffect } from "react";
|
||||
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import { useIsMobile } from "../components/App";
|
||||
import { KEYS } from "../keys";
|
||||
import "./Dialog.scss";
|
||||
import { back, close } from "./icons";
|
||||
|
@ -55,7 +55,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$is-mobile-query} {
|
||||
@include isMobile {
|
||||
.ExportDialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -6,7 +6,7 @@ import { canvasToBlob } from "../data/blob";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { CanvasError } from "../errors";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import { useIsMobile } from "../components/App";
|
||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||
import { exportToCanvas, getExportSize } from "../scene/export";
|
||||
import { AppState } from "../types";
|
||||
|
@ -19,7 +19,7 @@ $wide-viewport-width: 1000px;
|
||||
color: $oc-gray-6;
|
||||
font-size: 0.8rem;
|
||||
|
||||
@media #{$is-mobile-query} {
|
||||
@include isMobile {
|
||||
position: static;
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
@ -111,7 +111,7 @@
|
||||
:root[dir="rtl"] & {
|
||||
left: 2px;
|
||||
}
|
||||
@media #{$is-mobile-query} {
|
||||
@include isMobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ import { Library } from "../data/library";
|
||||
import { isTextElement, showSelectedShapeActions } from "../element";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { Language, t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import { useIsMobile } from "../components/App";
|
||||
import { calculateScrollCenter, getSelectedElements } from "../scene";
|
||||
import { ExportType } from "../scene/types";
|
||||
import {
|
||||
|
@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from "react";
|
||||
import { close } from "../components/icons";
|
||||
import { MIME_TYPES } from "../constants";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import { useIsMobile } from "../components/App";
|
||||
import { exportToSvg } from "../scene/export";
|
||||
import { LibraryItem } from "../types";
|
||||
import "./LibraryUnit.scss";
|
||||
|
@ -52,7 +52,7 @@
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@media #{$is-mobile-query} {
|
||||
@include isMobile {
|
||||
max-width: 100%;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
@ -82,7 +82,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$is-mobile-query} {
|
||||
@include isMobile {
|
||||
.Modal {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import "./Modal.scss";
|
||||
|
||||
import React, { useState, useLayoutEffect } from "react";
|
||||
import React, { useState, useLayoutEffect, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import clsx from "clsx";
|
||||
import { KEYS } from "../keys";
|
||||
import { useIsMobile } from "../components/App";
|
||||
|
||||
export const Modal = (props: {
|
||||
className?: string;
|
||||
@ -48,6 +49,16 @@ export const Modal = (props: {
|
||||
const useBodyRoot = () => {
|
||||
const [div, setDiv] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const isMobileRef = useRef(isMobile);
|
||||
isMobileRef.current = isMobile;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (div) {
|
||||
div.classList.toggle("excalidraw--mobile", isMobile);
|
||||
}
|
||||
}, [div, isMobile]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const isDarkTheme = !!document
|
||||
.querySelector(".excalidraw")
|
||||
@ -55,6 +66,7 @@ const useBodyRoot = () => {
|
||||
const div = document.createElement("div");
|
||||
|
||||
div.classList.add("excalidraw", "excalidraw-modal-container");
|
||||
div.classList.toggle("excalidraw--mobile", isMobileRef.current);
|
||||
|
||||
if (isDarkTheme) {
|
||||
div.classList.add("theme--dark");
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
.excalidraw {
|
||||
.PasteChartDialog {
|
||||
@media #{$is-mobile-query} {
|
||||
@include isMobile {
|
||||
.Island {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -13,7 +13,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
@media #{$is-mobile-query} {
|
||||
@include isMobile {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import React from "react";
|
||||
import { getCommonBounds } from "../element/bounds";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { useIsMobile } from "../is-mobile";
|
||||
import { useIsMobile } from "../components/App";
|
||||
import { getTargetElements } from "../scene";
|
||||
import { AppState, ExcalidrawProps } from "../types";
|
||||
import { close } from "./icons";
|
||||
|
@ -193,7 +193,7 @@
|
||||
margin-left: 5px;
|
||||
margin-top: 1px;
|
||||
|
||||
@media #{$is-mobile-query} {
|
||||
@include isMobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -137,3 +137,7 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
||||
theme: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const MQ_MAX_WIDTH_PORTRAIT = 730;
|
||||
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
|
||||
export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
|
||||
|
@ -480,7 +480,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$is-mobile-query} {
|
||||
@include isMobile {
|
||||
aside {
|
||||
display: none;
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
@import "open-color/open-color.scss";
|
||||
|
||||
// keep up to date with is-mobile.tsx
|
||||
$is-mobile-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)";
|
||||
@mixin isMobile() {
|
||||
@at-root .excalidraw--mobile#{&} {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
$theme-filter: "invert(93%) hue-rotate(180deg)";
|
||||
|
||||
:export {
|
||||
isMobileQuery: unquote($is-mobile-query);
|
||||
themeFilter: unquote($theme-filter);
|
||||
}
|
||||
|
@ -32,13 +32,13 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@media #{$is-mobile-query} {
|
||||
@include isMobile {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$is-mobile-query} {
|
||||
@include isMobile {
|
||||
.RoomDialog-usernameLabel {
|
||||
font-weight: bold;
|
||||
}
|
||||
@ -51,7 +51,7 @@
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
margin-inline-start: 1em;
|
||||
@media #{$is-mobile-query} {
|
||||
@include isMobile {
|
||||
margin-top: 0.5em;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
@ -1,37 +0,0 @@
|
||||
import React, { useState, useEffect, useRef, useContext } from "react";
|
||||
import variables from "./css/variables.module.scss";
|
||||
|
||||
const context = React.createContext(false);
|
||||
|
||||
const getIsMobileMatcher = () => {
|
||||
return window.matchMedia
|
||||
? window.matchMedia(variables.isMobileQuery)
|
||||
: (({
|
||||
matches: false,
|
||||
addListener: () => {},
|
||||
removeListener: () => {},
|
||||
} as any) as MediaQueryList);
|
||||
};
|
||||
|
||||
export const IsMobileProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const query = useRef<MediaQueryList>();
|
||||
if (!query.current) {
|
||||
query.current = getIsMobileMatcher();
|
||||
}
|
||||
const [isMobile, setMobile] = useState(query.current.matches);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => setMobile(query.current!.matches);
|
||||
query.current!.addListener(handler);
|
||||
return () => query.current!.removeListener(handler);
|
||||
}, []);
|
||||
|
||||
return <context.Provider value={isMobile}>{children}</context.Provider>;
|
||||
};
|
||||
|
||||
export const isMobile = () => getIsMobileMatcher().matches;
|
||||
export const useIsMobile = () => useContext(context);
|
@ -11,6 +11,14 @@ The change should be grouped under one of the below section and must contain PR
|
||||
Please add the latest change on the top under the correct section.
|
||||
-->
|
||||
|
||||
## Unreleased
|
||||
|
||||
## Excalidraw Library
|
||||
|
||||
### Features
|
||||
|
||||
- App now breaks into mobile view using the component dimensions, not viewport dimensions. This fixes a case where the app would break sooner than necessary when the component's size is smaller than viewport [#3414](https://github.com/excalidraw/excalidraw/pull/3414).
|
||||
|
||||
## 0.6.0 (2021-04-04)
|
||||
|
||||
## Excalidraw API
|
||||
|
@ -8,7 +8,6 @@ import "../../css/app.scss";
|
||||
import "../../css/styles.scss";
|
||||
|
||||
import { ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types";
|
||||
import { IsMobileProvider } from "../../is-mobile";
|
||||
import { defaultLang } from "../../i18n";
|
||||
import { DEFAULT_UI_OPTIONS } from "../../constants";
|
||||
|
||||
@ -61,7 +60,6 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
||||
|
||||
return (
|
||||
<InitializeApp langCode={langCode}>
|
||||
<IsMobileProvider>
|
||||
<App
|
||||
onChange={onChange}
|
||||
initialData={initialData}
|
||||
@ -81,7 +79,6 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
||||
renderCustomStats={renderCustomStats}
|
||||
UIOptions={UIOptions}
|
||||
/>
|
||||
</IsMobileProvider>
|
||||
</InitializeApp>
|
||||
);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user