feat: make device breakpoints more specific (#7243)

This commit is contained in:
David Luzar 2023-11-06 16:29:00 +01:00 committed by GitHub
parent 18a7b97515
commit b1037b342d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 164 additions and 121 deletions

View File

@ -34,7 +34,7 @@ Open the `Menu` in the below playground and you will see the `custom footer` ren
```jsx live noInline ```jsx live noInline
const MobileFooter = ({}) => { const MobileFooter = ({}) => {
const device = useDevice(); const device = useDevice();
if (device.isMobile) { if (device.editor.isMobile) {
return ( return (
<Footer> <Footer>
<button <button

View File

@ -299,7 +299,7 @@ Open the `main menu` in the below example to view the footer.
```jsx live noInline ```jsx live noInline
const MobileFooter = ({}) => { const MobileFooter = ({}) => {
const device = useDevice(); const device = useDevice();
if (device.isMobile) { if (device.editor.isMobile) {
return ( return (
<Footer> <Footer>
<button <button
@ -335,7 +335,6 @@ The `device` has the following `attributes`
| Name | Type | Description | | Name | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| `isSmScreen` | `boolean` | Set to `true` when the device small screen is small (Width < `640px` ) |
| `isMobile` | `boolean` | Set to `true` when the device is `mobile` | | `isMobile` | `boolean` | Set to `true` when the device is `mobile` |
| `isTouchScreen` | `boolean` | Set to `true` for `touch` devices | | `isTouchScreen` | `boolean` | Set to `true` for `touch` devices |
| `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` | | `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` |

View File

@ -17,8 +17,10 @@ describe("Test MobileMenu", () => {
beforeEach(async () => { beforeEach(async () => {
await render(<ExcalidrawApp />); await render(<ExcalidrawApp />);
//@ts-ignore // @ts-ignore
h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!); h.app.refreshViewportBreakpoints();
// @ts-ignore
h.app.refreshEditorBreakpoints();
}); });
afterAll(() => { afterAll(() => {
@ -28,11 +30,15 @@ describe("Test MobileMenu", () => {
it("should set device correctly", () => { it("should set device correctly", () => {
expect(h.app.device).toMatchInlineSnapshot(` expect(h.app.device).toMatchInlineSnapshot(`
{ {
"canDeviceFitSidebar": false, "editor": {
"isLandscape": true, "canFitSidebar": false,
"isMobile": true, "isMobile": true,
"isSmScreen": false, },
"isTouchScreen": false, "isTouchScreen": false,
"viewport": {
"isLandscape": false,
"isMobile": true,
},
} }
`); `);
}); });

View File

@ -217,7 +217,7 @@ export const actionSaveFileToDisk = register({
icon={saveAs} icon={saveAs}
title={t("buttons.saveAs")} title={t("buttons.saveAs")}
aria-label={t("buttons.saveAs")} aria-label={t("buttons.saveAs")}
showAriaLabel={useDevice().isMobile} showAriaLabel={useDevice().editor.isMobile}
hidden={!nativeFileSystemSupported} hidden={!nativeFileSystemSupported}
onClick={() => updateData(null)} onClick={() => updateData(null)}
data-testid="save-as-button" data-testid="save-as-button"

View File

@ -328,7 +328,7 @@ export const actionChangeFillStyle = register({
trackEvent( trackEvent(
"element", "element",
"changeFillStyle", "changeFillStyle",
`${value} (${app.device.isMobile ? "mobile" : "desktop"})`, `${value} (${app.device.editor.isMobile ? "mobile" : "desktop"})`,
); );
return { return {
elements: changeProperty(elements, appState, (el) => elements: changeProperty(elements, appState, (el) =>

View File

@ -29,7 +29,7 @@ const trackAction = (
trackEvent( trackEvent(
action.trackEvent.category, action.trackEvent.category,
action.trackEvent.action || action.name, action.trackEvent.action || action.name,
`${source} (${app.device.isMobile ? "mobile" : "desktop"})`, `${source} (${app.device.editor.isMobile ? "mobile" : "desktop"})`,
); );
} }
} }

View File

@ -202,8 +202,8 @@ export const SelectedShapeActions = ({
<fieldset> <fieldset>
<legend>{t("labels.actions")}</legend> <legend>{t("labels.actions")}</legend>
<div className="buttonList"> <div className="buttonList">
{!device.isMobile && renderAction("duplicateSelection")} {!device.editor.isMobile && renderAction("duplicateSelection")}
{!device.isMobile && renderAction("deleteSelectedElements")} {!device.editor.isMobile && renderAction("deleteSelectedElements")}
{renderAction("group")} {renderAction("group")}
{renderAction("ungroup")} {renderAction("ungroup")}
{showLinkIcon && renderAction("hyperlink")} {showLinkIcon && renderAction("hyperlink")}

View File

@ -74,7 +74,6 @@ import {
MQ_MAX_WIDTH_LANDSCAPE, MQ_MAX_WIDTH_LANDSCAPE,
MQ_MAX_WIDTH_PORTRAIT, MQ_MAX_WIDTH_PORTRAIT,
MQ_RIGHT_SIDEBAR_MIN_WIDTH, MQ_RIGHT_SIDEBAR_MIN_WIDTH,
MQ_SM_MAX_WIDTH,
POINTER_BUTTON, POINTER_BUTTON,
ROUNDNESS, ROUNDNESS,
SCROLL_TIMEOUT, SCROLL_TIMEOUT,
@ -381,11 +380,15 @@ const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!); const AppPropsContext = React.createContext<AppProps>(null!);
const deviceContextInitialValue = { const deviceContextInitialValue = {
isSmScreen: false, viewport: {
isMobile: false, isMobile: false,
isLandscape: false,
},
editor: {
isMobile: false,
canFitSidebar: false,
},
isTouchScreen: false, isTouchScreen: false,
canDeviceFitSidebar: false,
isLandscape: false,
}; };
const DeviceContext = React.createContext<Device>(deviceContextInitialValue); const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
DeviceContext.displayName = "DeviceContext"; DeviceContext.displayName = "DeviceContext";
@ -436,6 +439,9 @@ export const useExcalidrawSetAppState = () =>
export const useExcalidrawActionManager = () => export const useExcalidrawActionManager = () =>
useContext(ExcalidrawActionManagerContext); useContext(ExcalidrawActionManagerContext);
const supportsResizeObserver =
typeof window !== "undefined" && "ResizeObserver" in window;
let didTapTwice: boolean = false; let didTapTwice: boolean = false;
let tappedTwiceTimer = 0; let tappedTwiceTimer = 0;
let isHoldingSpace: boolean = false; let isHoldingSpace: boolean = false;
@ -472,7 +478,6 @@ class App extends React.Component<AppProps, AppState> {
unmounted: boolean = false; unmounted: boolean = false;
actionManager: ActionManager; actionManager: ActionManager;
device: Device = deviceContextInitialValue; device: Device = deviceContextInitialValue;
detachIsMobileMqHandler?: () => void;
private excalidrawContainerRef = React.createRef<HTMLDivElement>(); private excalidrawContainerRef = React.createRef<HTMLDivElement>();
@ -1180,7 +1185,7 @@ class App extends React.Component<AppProps, AppState> {
<div <div
className={clsx("excalidraw excalidraw-container", { className={clsx("excalidraw excalidraw-container", {
"excalidraw--view-mode": this.state.viewModeEnabled, "excalidraw--view-mode": this.state.viewModeEnabled,
"excalidraw--mobile": this.device.isMobile, "excalidraw--mobile": this.device.editor.isMobile,
})} })}
style={{ style={{
["--ui-pointerEvents" as any]: ["--ui-pointerEvents" as any]:
@ -1657,20 +1662,62 @@ class App extends React.Component<AppProps, AppState> {
}); });
}; };
private refreshDeviceState = (container: HTMLDivElement) => { private isMobileBreakpoint = (width: number, height: number) => {
const { width, height } = container.getBoundingClientRect(); return (
width < MQ_MAX_WIDTH_PORTRAIT ||
(height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE)
);
};
private refreshViewportBreakpoints = () => {
const container = this.excalidrawContainerRef.current;
if (!container) {
return;
}
const { clientWidth: viewportWidth, clientHeight: viewportHeight } =
document.body;
const prevViewportState = this.device.viewport;
const nextViewportState = updateObject(prevViewportState, {
isLandscape: viewportWidth > viewportHeight,
isMobile: this.isMobileBreakpoint(viewportWidth, viewportHeight),
});
if (prevViewportState !== nextViewportState) {
this.device = { ...this.device, viewport: nextViewportState };
return true;
}
return false;
};
private refreshEditorBreakpoints = () => {
const container = this.excalidrawContainerRef.current;
if (!container) {
return;
}
const { width: editorWidth, height: editorHeight } =
container.getBoundingClientRect();
const sidebarBreakpoint = const sidebarBreakpoint =
this.props.UIOptions.dockedSidebarBreakpoint != null this.props.UIOptions.dockedSidebarBreakpoint != null
? this.props.UIOptions.dockedSidebarBreakpoint ? this.props.UIOptions.dockedSidebarBreakpoint
: MQ_RIGHT_SIDEBAR_MIN_WIDTH; : MQ_RIGHT_SIDEBAR_MIN_WIDTH;
this.device = updateObject(this.device, {
isLandscape: width > height, const prevEditorState = this.device.editor;
isSmScreen: width < MQ_SM_MAX_WIDTH,
isMobile: const nextEditorState = updateObject(prevEditorState, {
width < MQ_MAX_WIDTH_PORTRAIT || isMobile: this.isMobileBreakpoint(editorWidth, editorHeight),
(height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE), canFitSidebar: editorWidth > sidebarBreakpoint,
canDeviceFitSidebar: width > sidebarBreakpoint,
}); });
if (prevEditorState !== nextEditorState) {
this.device = { ...this.device, editor: nextEditorState };
return true;
}
return false;
}; };
public async componentDidMount() { public async componentDidMount() {
@ -1712,52 +1759,21 @@ class App extends React.Component<AppProps, AppState> {
} }
if ( if (
this.excalidrawContainerRef.current &&
// bounding rects don't work in tests so updating // bounding rects don't work in tests so updating
// the state on init would result in making the test enviro run // the state on init would result in making the test enviro run
// in mobile breakpoint (0 width/height), making everything fail // in mobile breakpoint (0 width/height), making everything fail
!isTestEnv() !isTestEnv()
) { ) {
this.refreshDeviceState(this.excalidrawContainerRef.current); this.refreshViewportBreakpoints();
this.refreshEditorBreakpoints();
} }
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) { if (supportsResizeObserver && this.excalidrawContainerRef.current) {
this.resizeObserver = new ResizeObserver(() => { this.resizeObserver = new ResizeObserver(() => {
// recompute device dimensions state this.refreshEditorBreakpoints();
// ---------------------------------------------------------------------
this.refreshDeviceState(this.excalidrawContainerRef.current!);
// refresh offsets
// ---------------------------------------------------------------------
this.updateDOMRect(); this.updateDOMRect();
}); });
this.resizeObserver?.observe(this.excalidrawContainerRef.current); this.resizeObserver?.observe(this.excalidrawContainerRef.current);
} else if (window.matchMedia) {
const mdScreenQuery = 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 smScreenQuery = window.matchMedia(
`(max-width: ${MQ_SM_MAX_WIDTH}px)`,
);
const canDeviceFitSidebarMediaQuery = window.matchMedia(
`(min-width: ${
// NOTE this won't update if a different breakpoint is supplied
// after mount
this.props.UIOptions.dockedSidebarBreakpoint != null
? this.props.UIOptions.dockedSidebarBreakpoint
: MQ_RIGHT_SIDEBAR_MIN_WIDTH
}px)`,
);
const handler = () => {
this.excalidrawContainerRef.current!.getBoundingClientRect();
this.device = updateObject(this.device, {
isSmScreen: smScreenQuery.matches,
isMobile: mdScreenQuery.matches,
canDeviceFitSidebar: canDeviceFitSidebarMediaQuery.matches,
});
};
mdScreenQuery.addListener(handler);
this.detachIsMobileMqHandler = () =>
mdScreenQuery.removeListener(handler);
} }
const searchParams = new URLSearchParams(window.location.search.slice(1)); const searchParams = new URLSearchParams(window.location.search.slice(1));
@ -1802,6 +1818,11 @@ class App extends React.Component<AppProps, AppState> {
this.scene this.scene
.getElementsIncludingDeleted() .getElementsIncludingDeleted()
.forEach((element) => ShapeCache.delete(element)); .forEach((element) => ShapeCache.delete(element));
this.refreshViewportBreakpoints();
this.updateDOMRect();
if (!supportsResizeObserver) {
this.refreshEditorBreakpoints();
}
this.setState({}); this.setState({});
}); });
@ -1855,7 +1876,6 @@ class App extends React.Component<AppProps, AppState> {
false, false,
); );
this.detachIsMobileMqHandler?.();
window.removeEventListener(EVENT.MESSAGE, this.onWindowMessage, false); window.removeEventListener(EVENT.MESSAGE, this.onWindowMessage, false);
} }
@ -1940,11 +1960,10 @@ class App extends React.Component<AppProps, AppState> {
} }
if ( if (
this.excalidrawContainerRef.current &&
prevProps.UIOptions.dockedSidebarBreakpoint !== prevProps.UIOptions.dockedSidebarBreakpoint !==
this.props.UIOptions.dockedSidebarBreakpoint this.props.UIOptions.dockedSidebarBreakpoint
) { ) {
this.refreshDeviceState(this.excalidrawContainerRef.current); this.refreshEditorBreakpoints();
} }
if ( if (
@ -2410,7 +2429,7 @@ class App extends React.Component<AppProps, AppState> {
// from library, not when pasting from clipboard. Alas. // from library, not when pasting from clipboard. Alas.
openSidebar: openSidebar:
this.state.openSidebar && this.state.openSidebar &&
this.device.canDeviceFitSidebar && this.device.editor.canFitSidebar &&
jotaiStore.get(isSidebarDockedAtom) jotaiStore.get(isSidebarDockedAtom)
? this.state.openSidebar ? this.state.openSidebar
: null, : null,
@ -2624,7 +2643,7 @@ class App extends React.Component<AppProps, AppState> {
!isPlainPaste && !isPlainPaste &&
textElements.length > 1 && textElements.length > 1 &&
PLAIN_PASTE_TOAST_SHOWN === false && PLAIN_PASTE_TOAST_SHOWN === false &&
!this.device.isMobile !this.device.editor.isMobile
) { ) {
this.setToast({ this.setToast({
message: t("toast.pasteAsSingleElement", { message: t("toast.pasteAsSingleElement", {
@ -2658,7 +2677,7 @@ class App extends React.Component<AppProps, AppState> {
trackEvent( trackEvent(
"toolbar", "toolbar",
"toggleLock", "toggleLock",
`${source} (${this.device.isMobile ? "mobile" : "desktop"})`, `${source} (${this.device.editor.isMobile ? "mobile" : "desktop"})`,
); );
} }
this.setState((prevState) => { this.setState((prevState) => {
@ -3153,7 +3172,9 @@ class App extends React.Component<AppProps, AppState> {
trackEvent( trackEvent(
"toolbar", "toolbar",
shape, shape,
`keyboard (${this.device.isMobile ? "mobile" : "desktop"})`, `keyboard (${
this.device.editor.isMobile ? "mobile" : "desktop"
})`,
); );
} }
this.setActiveTool({ type: shape }); this.setActiveTool({ type: shape });
@ -3887,7 +3908,7 @@ class App extends React.Component<AppProps, AppState> {
element, element,
this.state, this.state,
[scenePointer.x, scenePointer.y], [scenePointer.x, scenePointer.y],
this.device.isMobile, this.device.editor.isMobile,
) )
); );
}); });
@ -3919,7 +3940,7 @@ class App extends React.Component<AppProps, AppState> {
this.hitLinkElement, this.hitLinkElement,
this.state, this.state,
[lastPointerDownCoords.x, lastPointerDownCoords.y], [lastPointerDownCoords.x, lastPointerDownCoords.y],
this.device.isMobile, this.device.editor.isMobile,
); );
const lastPointerUpCoords = viewportCoordsToSceneCoords( const lastPointerUpCoords = viewportCoordsToSceneCoords(
this.lastPointerUpEvent!, this.lastPointerUpEvent!,
@ -3929,7 +3950,7 @@ class App extends React.Component<AppProps, AppState> {
this.hitLinkElement, this.hitLinkElement,
this.state, this.state,
[lastPointerUpCoords.x, lastPointerUpCoords.y], [lastPointerUpCoords.x, lastPointerUpCoords.y],
this.device.isMobile, this.device.editor.isMobile,
); );
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) { if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
let url = this.hitLinkElement.link; let url = this.hitLinkElement.link;
@ -4791,7 +4812,7 @@ class App extends React.Component<AppProps, AppState> {
); );
const clicklength = const clicklength =
event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0); event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0);
if (this.device.isMobile && clicklength < 300) { if (this.device.editor.isMobile && clicklength < 300) {
const hitElement = this.getElementAtPosition( const hitElement = this.getElementAtPosition(
scenePointer.x, scenePointer.x,
scenePointer.y, scenePointer.y,

View File

@ -98,7 +98,7 @@ export const ColorInput = ({
}} }}
/> />
{/* TODO reenable on mobile with a better UX */} {/* TODO reenable on mobile with a better UX */}
{!device.isMobile && ( {!device.editor.isMobile && (
<> <>
<div <div
style={{ style={{

View File

@ -80,7 +80,7 @@ const ColorPickerPopupContent = ({
); );
const { container } = useExcalidrawContainer(); const { container } = useExcalidrawContainer();
const { isMobile, isLandscape } = useDevice(); const device = useDevice();
const colorInputJSX = ( const colorInputJSX = (
<div> <div>
@ -136,8 +136,16 @@ const ColorPickerPopupContent = ({
updateData({ openPopup: null }); updateData({ openPopup: null });
setActiveColorPickerSection(null); setActiveColorPickerSection(null);
}} }}
side={isMobile && !isLandscape ? "bottom" : "right"} side={
align={isMobile && !isLandscape ? "center" : "start"} device.editor.isMobile && !device.viewport.isLandscape
? "bottom"
: "right"
}
align={
device.editor.isMobile && !device.viewport.isLandscape
? "center"
: "start"
}
alignOffset={-16} alignOffset={-16}
sideOffset={20} sideOffset={20}
style={{ style={{

View File

@ -119,7 +119,7 @@ export const Dialog = (props: DialogProps) => {
title={t("buttons.close")} title={t("buttons.close")}
aria-label={t("buttons.close")} aria-label={t("buttons.close")}
> >
{device.isMobile ? back : CloseIcon} {device.editor.isMobile ? back : CloseIcon}
</button> </button>
<div className="Dialog__content">{props.children}</div> <div className="Dialog__content">{props.children}</div>
</Island> </Island>

View File

@ -22,7 +22,7 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState; const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
const multiMode = appState.multiElement !== null; const multiMode = appState.multiElement !== null;
if (appState.openSidebar && !device.canDeviceFitSidebar) { if (appState.openSidebar && !device.editor.canFitSidebar) {
return null; return null;
} }

View File

@ -246,7 +246,7 @@ const LayerUI = ({
> >
<HintViewer <HintViewer
appState={appState} appState={appState}
isMobile={device.isMobile} isMobile={device.editor.isMobile}
device={device} device={device}
app={app} app={app}
/> />
@ -314,7 +314,7 @@ const LayerUI = ({
)} )}
> >
<UserList collaborators={appState.collaborators} /> <UserList collaborators={appState.collaborators} />
{renderTopRightUI?.(device.isMobile, appState)} {renderTopRightUI?.(device.editor.isMobile, appState)}
{!appState.viewModeEnabled && {!appState.viewModeEnabled &&
// hide button when sidebar docked // hide button when sidebar docked
(!isSidebarDocked || (!isSidebarDocked ||
@ -335,7 +335,7 @@ const LayerUI = ({
trackEvent( trackEvent(
"sidebar", "sidebar",
`toggleDock (${docked ? "dock" : "undock"})`, `toggleDock (${docked ? "dock" : "undock"})`,
`(${device.isMobile ? "mobile" : "desktop"})`, `(${device.editor.isMobile ? "mobile" : "desktop"})`,
); );
}} }}
/> />
@ -363,7 +363,7 @@ const LayerUI = ({
trackEvent( trackEvent(
"sidebar", "sidebar",
`${DEFAULT_SIDEBAR.name} (open)`, `${DEFAULT_SIDEBAR.name} (open)`,
`button (${device.isMobile ? "mobile" : "desktop"})`, `button (${device.editor.isMobile ? "mobile" : "desktop"})`,
); );
} }
}} }}
@ -380,7 +380,7 @@ const LayerUI = ({
{appState.errorMessage} {appState.errorMessage}
</ErrorDialog> </ErrorDialog>
)} )}
{eyeDropperState && !device.isMobile && ( {eyeDropperState && !device.editor.isMobile && (
<EyeDropper <EyeDropper
colorPickerType={eyeDropperState.colorPickerType} colorPickerType={eyeDropperState.colorPickerType}
onCancel={() => { onCancel={() => {
@ -450,7 +450,7 @@ const LayerUI = ({
} }
/> />
)} )}
{device.isMobile && ( {device.editor.isMobile && (
<MobileMenu <MobileMenu
app={app} app={app}
appState={appState} appState={appState}
@ -469,14 +469,14 @@ const LayerUI = ({
renderWelcomeScreen={renderWelcomeScreen} renderWelcomeScreen={renderWelcomeScreen}
/> />
)} )}
{!device.isMobile && ( {!device.editor.isMobile && (
<> <>
<div <div
className="layer-ui__wrapper" className="layer-ui__wrapper"
style={ style={
appState.openSidebar && appState.openSidebar &&
isSidebarDocked && isSidebarDocked &&
device.canDeviceFitSidebar device.editor.canFitSidebar
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` } ? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
: {} : {}
} }

View File

@ -47,7 +47,7 @@ export const LibraryUnit = memo(
}, [svg]); }, [svg]);
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const isMobile = useDevice().isMobile; const isMobile = useDevice().editor.isMobile;
const adder = isPending && ( const adder = isPending && (
<div className="library-unit__adder">{PlusIcon}</div> <div className="library-unit__adder">{PlusIcon}</div>
); );

View File

@ -113,11 +113,11 @@ export const SidebarInner = forwardRef(
if ((event.target as Element).closest(".sidebar-trigger")) { if ((event.target as Element).closest(".sidebar-trigger")) {
return; return;
} }
if (!docked || !device.canDeviceFitSidebar) { if (!docked || !device.editor.canFitSidebar) {
closeLibrary(); closeLibrary();
} }
}, },
[closeLibrary, docked, device.canDeviceFitSidebar], [closeLibrary, docked, device.editor.canFitSidebar],
), ),
); );
@ -125,7 +125,7 @@ export const SidebarInner = forwardRef(
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if ( if (
event.key === KEYS.ESCAPE && event.key === KEYS.ESCAPE &&
(!docked || !device.canDeviceFitSidebar) (!docked || !device.editor.canFitSidebar)
) { ) {
closeLibrary(); closeLibrary();
} }
@ -134,7 +134,7 @@ export const SidebarInner = forwardRef(
return () => { return () => {
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown); document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
}; };
}, [closeLibrary, docked, device.canDeviceFitSidebar]); }, [closeLibrary, docked, device.editor.canFitSidebar]);
return ( return (
<Island <Island

View File

@ -18,7 +18,7 @@ export const SidebarHeader = ({
const props = useContext(SidebarPropsContext); const props = useContext(SidebarPropsContext);
const renderDockButton = !!( const renderDockButton = !!(
device.canDeviceFitSidebar && props.shouldRenderDockButton device.editor.canFitSidebar && props.shouldRenderDockButton
); );
return ( return (

View File

@ -30,7 +30,7 @@ const MenuContent = ({
}); });
const classNames = clsx(`dropdown-menu ${className}`, { const classNames = clsx(`dropdown-menu ${className}`, {
"dropdown-menu--mobile": device.isMobile, "dropdown-menu--mobile": device.editor.isMobile,
}).trim(); }).trim();
return ( return (
@ -43,7 +43,7 @@ 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.isMobile ? ( {device.editor.isMobile ? (
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col> <Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
) : ( ) : (
<Island <Island

View File

@ -14,7 +14,7 @@ const MenuItemContent = ({
<> <>
<div className="dropdown-menu-item__icon">{icon}</div> <div className="dropdown-menu-item__icon">{icon}</div>
<div className="dropdown-menu-item__text">{children}</div> <div className="dropdown-menu-item__text">{children}</div>
{shortcut && !device.isMobile && ( {shortcut && !device.editor.isMobile && (
<div className="dropdown-menu-item__shortcut">{shortcut}</div> <div className="dropdown-menu-item__shortcut">{shortcut}</div>
)} )}
</> </>

View File

@ -18,7 +18,7 @@ const MenuTrigger = ({
`dropdown-menu-button ${className}`, `dropdown-menu-button ${className}`,
"zen-mode-transition", "zen-mode-transition",
{ {
"dropdown-menu-button--mobile": device.isMobile, "dropdown-menu-button--mobile": device.editor.isMobile,
}, },
).trim(); ).trim();
return ( return (

View File

@ -29,7 +29,7 @@ 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.isMobile const onClickOutside = device.editor.isMobile
? undefined ? undefined
: () => setAppState({ openMenu: null }); : () => setAppState({ openMenu: null });
@ -54,7 +54,7 @@ const MainMenu = Object.assign(
})} })}
> >
{children} {children}
{device.isMobile && appState.collaborators.size > 0 && ( {device.editor.isMobile && appState.collaborators.size > 0 && (
<fieldset className="UserList-Wrapper"> <fieldset className="UserList-Wrapper">
<legend>{t("labels.collaborators")}</legend> <legend>{t("labels.collaborators")}</legend>
<UserList <UserList

View File

@ -21,7 +21,7 @@ const WelcomeScreenMenuItemContent = ({
<> <>
<div className="welcome-screen-menu-item__icon">{icon}</div> <div className="welcome-screen-menu-item__icon">{icon}</div>
<div className="welcome-screen-menu-item__text">{children}</div> <div className="welcome-screen-menu-item__text">{children}</div>
{shortcut && !device.isMobile && ( {shortcut && !device.editor.isMobile && (
<div className="welcome-screen-menu-item__shortcut">{shortcut}</div> <div className="welcome-screen-menu-item__shortcut">{shortcut}</div>
)} )}
</> </>

View File

@ -220,8 +220,6 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
// breakpoints // breakpoints
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// sm screen
export const MQ_SM_MAX_WIDTH = 640;
// md screen // md screen
export const MQ_MAX_WIDTH_PORTRAIT = 730; export const MQ_MAX_WIDTH_PORTRAIT = 730;
export const MQ_MAX_WIDTH_LANDSCAPE = 1000; export const MQ_MAX_WIDTH_LANDSCAPE = 1000;

View File

@ -249,8 +249,10 @@ describe("textWysiwyg", () => {
beforeEach(async () => { beforeEach(async () => {
await render(<Excalidraw handleKeyboardGlobally={true} />); await render(<Excalidraw handleKeyboardGlobally={true} />);
//@ts-ignore // @ts-ignore
h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!); h.app.refreshViewportBreakpoints();
// @ts-ignore
h.app.refreshEditorBreakpoints();
textElement = UI.createElement("text"); textElement = UI.createElement("text");

View File

@ -1,4 +1,4 @@
import { useState, useRef, useLayoutEffect } from "react"; import { useState, useLayoutEffect } from "react";
import { useDevice, useExcalidrawContainer } from "../components/App"; import { useDevice, useExcalidrawContainer } from "../components/App";
import { useUIAppState } from "../context/ui-appState"; import { useUIAppState } from "../context/ui-appState";
@ -10,8 +10,6 @@ export const useCreatePortalContainer = (opts?: {
const device = useDevice(); const device = useDevice();
const { theme } = useUIAppState(); const { theme } = useUIAppState();
const isMobileRef = useRef(device.isMobile);
isMobileRef.current = device.isMobile;
const { container: excalidrawContainer } = useExcalidrawContainer(); const { container: excalidrawContainer } = useExcalidrawContainer();
@ -19,11 +17,10 @@ export const useCreatePortalContainer = (opts?: {
if (div) { if (div) {
div.className = ""; div.className = "";
div.classList.add("excalidraw", ...(opts?.className?.split(/\s+/) || [])); div.classList.add("excalidraw", ...(opts?.className?.split(/\s+/) || []));
div.classList.toggle("excalidraw--mobile", device.isMobile); div.classList.toggle("excalidraw--mobile", device.editor.isMobile);
div.classList.toggle("excalidraw--mobile", isMobileRef.current);
div.classList.toggle("theme--dark", theme === "dark"); div.classList.toggle("theme--dark", theme === "dark");
} }
}, [div, theme, device.isMobile, opts?.className]); }, [div, theme, device.editor.isMobile, opts?.className]);
useLayoutEffect(() => { useLayoutEffect(() => {
const container = opts?.parentSelector const container = opts?.parentSelector

View File

@ -19,6 +19,10 @@ Please add the latest change on the top under the correct section.
- Regenerate ids by default when using transform api and also update bindings by 0.5px to avoid possible overlapping [#7195](https://github.com/excalidraw/excalidraw/pull/7195) - Regenerate ids by default when using transform api and also update bindings by 0.5px to avoid possible overlapping [#7195](https://github.com/excalidraw/excalidraw/pull/7195)
- Add `selected` prop for `MainMenu.Item` and `MainMenu.ItemCustom` components to indicate active state. [#7078](https://github.com/excalidraw/excalidraw/pull/7078) - Add `selected` prop for `MainMenu.Item` and `MainMenu.ItemCustom` components to indicate active state. [#7078](https://github.com/excalidraw/excalidraw/pull/7078)
#### BREAKING CHANGES
- [`useDevice`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils#usedevice) hook's return value was changed to differentiate between `editor` and `viewport` breakpoints. [#7243](https://github.com/excalidraw/excalidraw/pull/7243)
## 0.16.1 (2023-09-21) ## 0.16.1 (2023-09-21)
## Excalidraw Library ## Excalidraw Library

View File

@ -8,7 +8,7 @@ const MobileFooter = ({
excalidrawAPI: ExcalidrawImperativeAPI; excalidrawAPI: ExcalidrawImperativeAPI;
}) => { }) => {
const device = useDevice(); const device = useDevice();
if (device.isMobile) { if (device.editor.isMobile) {
return ( return (
<Footer> <Footer>
<CustomFooter excalidrawAPI={excalidrawAPI} /> <CustomFooter excalidrawAPI={excalidrawAPI} />

View File

@ -173,14 +173,18 @@ export const withExcalidrawDimensions = async (
) => { ) => {
mockBoundingClientRect(dimensions); mockBoundingClientRect(dimensions);
// @ts-ignore // @ts-ignore
window.h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!); h.app.refreshViewportBreakpoints();
// @ts-ignore
h.app.refreshEditorBreakpoints();
window.h.app.refresh(); window.h.app.refresh();
await cb(); await cb();
restoreOriginalGetBoundingClientRect(); restoreOriginalGetBoundingClientRect();
// @ts-ignore // @ts-ignore
window.h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!); h.app.refreshViewportBreakpoints();
// @ts-ignore
h.app.refreshEditorBreakpoints();
window.h.app.refresh(); window.h.app.refresh();
}; };

View File

@ -667,11 +667,15 @@ export type ExcalidrawImperativeAPI = {
}; };
export type Device = Readonly<{ export type Device = Readonly<{
isSmScreen: boolean; viewport: {
isMobile: boolean; isMobile: boolean;
isLandscape: boolean;
};
editor: {
isMobile: boolean;
canFitSidebar: boolean;
};
isTouchScreen: boolean; isTouchScreen: boolean;
canDeviceFitSidebar: boolean;
isLandscape: boolean;
}>; }>;
type FrameNameBounds = { type FrameNameBounds = {