feat: compact layout for tablets (#9910)

* feat: allow the hiding of top picks

* feat: allow the hiding of default fonts

* refactor: rename to compactMode

* feat: introduce layout (incomplete)

* tweak icons

* do not show border

* lint

* add isTouchMobile to device

* add isTouchMobile to device

* refactor to use showCompactSidebar instead

* hide library label in compact

* fix icon color in dark theme

* fix library and share btns getting hidden in smaller tablet widths

* update tests

* use a smaller gap between shapes

* proper fix of range

* quicker switching between different popovers

* to not show properties panel at all when editing text

* fix switching between different popovers for texts

* fix popover not closable and font search auto focus

* change properties for a new or editing text

* change icon for more style settings

* use bolt icon for extra actions

* fix breakpoints

* use rem for icon sizes

* fix tests

* improve switching between triggers (incomplete)

* improve trigger switching (complete)

* clean up code

* put compact into app state

* fix button size

* remove redundant PanelComponentProps["compactMode"]

* move fontSize UI on top

* mobile detection (breakpoints incomplete)

* tweak compact mode detection

* rename appState prop & values

* update snapshots

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Ryan Di
2025-09-12 10:18:31 +10:00
committed by GitHub
parent 414182f599
commit 204e06b77b
32 changed files with 1527 additions and 147 deletions

View File

@@ -41,9 +41,6 @@ import {
LINE_CONFIRM_THRESHOLD,
MAX_ALLOWED_FILE_BYTES,
MIME_TYPES,
MQ_MAX_HEIGHT_LANDSCAPE,
MQ_MAX_WIDTH_LANDSCAPE,
MQ_MAX_WIDTH_PORTRAIT,
MQ_RIGHT_SIDEBAR_MIN_WIDTH,
POINTER_BUTTON,
ROUNDNESS,
@@ -100,9 +97,14 @@ import {
randomInteger,
CLASSES,
Emitter,
isMobile,
MINIMUM_ARROW_SIZE,
DOUBLE_TAP_POSITION_THRESHOLD,
isMobileOrTablet,
MQ_MAX_WIDTH_MOBILE,
MQ_MAX_HEIGHT_LANDSCAPE,
MQ_MAX_WIDTH_LANDSCAPE,
MQ_MIN_TABLET,
MQ_MAX_TABLET,
} from "@excalidraw/common";
import {
@@ -667,7 +669,7 @@ class App extends React.Component<AppProps, AppState> {
constructor(props: AppProps) {
super(props);
const defaultAppState = getDefaultAppState();
this.defaultSelectionTool = this.isMobileOrTablet()
this.defaultSelectionTool = isMobileOrTablet()
? ("lasso" as const)
: ("selection" as const);
const {
@@ -2420,23 +2422,20 @@ class App extends React.Component<AppProps, AppState> {
}
};
private isMobileOrTablet = (): boolean => {
const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
const hasCoarsePointer =
"matchMedia" in window &&
window?.matchMedia("(pointer: coarse)")?.matches;
const isTouchMobile = hasTouch && hasCoarsePointer;
return isMobile || isTouchMobile;
};
private isMobileBreakpoint = (width: number, height: number) => {
return (
width < MQ_MAX_WIDTH_PORTRAIT ||
width <= MQ_MAX_WIDTH_MOBILE ||
(height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE)
);
};
private isTabletBreakpoint = (editorWidth: number, editorHeight: number) => {
const minSide = Math.min(editorWidth, editorHeight);
const maxSide = Math.max(editorWidth, editorHeight);
return minSide >= MQ_MIN_TABLET && maxSide <= MQ_MAX_TABLET;
};
private refreshViewportBreakpoints = () => {
const container = this.excalidrawContainerRef.current;
if (!container) {
@@ -2481,6 +2480,17 @@ class App extends React.Component<AppProps, AppState> {
canFitSidebar: editorWidth > sidebarBreakpoint,
});
// also check if we need to update the app state
this.setState({
stylesPanelMode:
// NOTE: we could also remove the isMobileOrTablet check here and
// always switch to compact mode when the editor is narrow (e.g. < MQ_MIN_WIDTH_DESKTOP)
// but not too narrow (> MQ_MAX_WIDTH_MOBILE)
this.isTabletBreakpoint(editorWidth, editorHeight) && isMobileOrTablet()
? "compact"
: "full",
});
if (prevEditorState !== nextEditorState) {
this.device = { ...this.device, editor: nextEditorState };
return true;
@@ -3147,7 +3157,7 @@ class App extends React.Component<AppProps, AppState> {
this.addElementsFromPasteOrLibrary({
elements,
files: data.files || null,
position: this.isMobileOrTablet() ? "center" : "cursor",
position: isMobileOrTablet() ? "center" : "cursor",
retainSeed: isPlainPaste,
});
return;
@@ -3172,7 +3182,7 @@ class App extends React.Component<AppProps, AppState> {
this.addElementsFromPasteOrLibrary({
elements,
files,
position: this.isMobileOrTablet() ? "center" : "cursor",
position: isMobileOrTablet() ? "center" : "cursor",
});
return;
@@ -6668,8 +6678,6 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.hit.element &&
this.isASelectedElement(pointerDownState.hit.element);
const isMobileOrTablet = this.isMobileOrTablet();
if (
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements &&
!pointerDownState.resize.handleType &&
@@ -6683,12 +6691,12 @@ class App extends React.Component<AppProps, AppState> {
// block dragging after lasso selection on PCs until the next pointer down
// (on mobile or tablet, we want to allow user to drag immediately)
pointerDownState.drag.blockDragging = !isMobileOrTablet;
pointerDownState.drag.blockDragging = !isMobileOrTablet();
}
// only for mobile or tablet, if we hit an element, select it immediately like normal selection
if (
isMobileOrTablet &&
isMobileOrTablet() &&
pointerDownState.hit.element &&
!hitSelectedElement
) {
@@ -8489,7 +8497,7 @@ class App extends React.Component<AppProps, AppState> {
if (
this.state.activeTool.type === "lasso" &&
this.lassoTrail.hasCurrentTrail &&
!(this.isMobileOrTablet() && pointerDownState.hit.element) &&
!(isMobileOrTablet() && pointerDownState.hit.element) &&
!this.state.activeTool.fromSelection
) {
return;