mirror of
https://github.com/excalidraw/excalidraw.git
synced 2024-11-02 03:25:53 +01:00
feat: make device breakpoints more specific (#7243)
This commit is contained in:
parent
18a7b97515
commit
b1037b342d
@ -34,7 +34,7 @@ Open the `Menu` in the below playground and you will see the `custom footer` ren
|
||||
```jsx live noInline
|
||||
const MobileFooter = ({}) => {
|
||||
const device = useDevice();
|
||||
if (device.isMobile) {
|
||||
if (device.editor.isMobile) {
|
||||
return (
|
||||
<Footer>
|
||||
<button
|
||||
|
@ -299,7 +299,7 @@ Open the `main menu` in the below example to view the footer.
|
||||
```jsx live noInline
|
||||
const MobileFooter = ({}) => {
|
||||
const device = useDevice();
|
||||
if (device.isMobile) {
|
||||
if (device.editor.isMobile) {
|
||||
return (
|
||||
<Footer>
|
||||
<button
|
||||
@ -335,7 +335,6 @@ The `device` has the following `attributes`
|
||||
|
||||
| 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` |
|
||||
| `isTouchScreen` | `boolean` | Set to `true` for `touch` devices |
|
||||
| `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` |
|
||||
|
@ -17,8 +17,10 @@ describe("Test MobileMenu", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await render(<ExcalidrawApp />);
|
||||
//@ts-ignore
|
||||
h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!);
|
||||
// @ts-ignore
|
||||
h.app.refreshViewportBreakpoints();
|
||||
// @ts-ignore
|
||||
h.app.refreshEditorBreakpoints();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
@ -28,11 +30,15 @@ describe("Test MobileMenu", () => {
|
||||
it("should set device correctly", () => {
|
||||
expect(h.app.device).toMatchInlineSnapshot(`
|
||||
{
|
||||
"canDeviceFitSidebar": false,
|
||||
"isLandscape": true,
|
||||
"isMobile": true,
|
||||
"isSmScreen": false,
|
||||
"editor": {
|
||||
"canFitSidebar": false,
|
||||
"isMobile": true,
|
||||
},
|
||||
"isTouchScreen": false,
|
||||
"viewport": {
|
||||
"isLandscape": false,
|
||||
"isMobile": true,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -217,7 +217,7 @@ export const actionSaveFileToDisk = register({
|
||||
icon={saveAs}
|
||||
title={t("buttons.saveAs")}
|
||||
aria-label={t("buttons.saveAs")}
|
||||
showAriaLabel={useDevice().isMobile}
|
||||
showAriaLabel={useDevice().editor.isMobile}
|
||||
hidden={!nativeFileSystemSupported}
|
||||
onClick={() => updateData(null)}
|
||||
data-testid="save-as-button"
|
||||
|
@ -328,7 +328,7 @@ export const actionChangeFillStyle = register({
|
||||
trackEvent(
|
||||
"element",
|
||||
"changeFillStyle",
|
||||
`${value} (${app.device.isMobile ? "mobile" : "desktop"})`,
|
||||
`${value} (${app.device.editor.isMobile ? "mobile" : "desktop"})`,
|
||||
);
|
||||
return {
|
||||
elements: changeProperty(elements, appState, (el) =>
|
||||
|
@ -29,7 +29,7 @@ const trackAction = (
|
||||
trackEvent(
|
||||
action.trackEvent.category,
|
||||
action.trackEvent.action || action.name,
|
||||
`${source} (${app.device.isMobile ? "mobile" : "desktop"})`,
|
||||
`${source} (${app.device.editor.isMobile ? "mobile" : "desktop"})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -202,8 +202,8 @@ export const SelectedShapeActions = ({
|
||||
<fieldset>
|
||||
<legend>{t("labels.actions")}</legend>
|
||||
<div className="buttonList">
|
||||
{!device.isMobile && renderAction("duplicateSelection")}
|
||||
{!device.isMobile && renderAction("deleteSelectedElements")}
|
||||
{!device.editor.isMobile && renderAction("duplicateSelection")}
|
||||
{!device.editor.isMobile && renderAction("deleteSelectedElements")}
|
||||
{renderAction("group")}
|
||||
{renderAction("ungroup")}
|
||||
{showLinkIcon && renderAction("hyperlink")}
|
||||
|
@ -74,7 +74,6 @@ import {
|
||||
MQ_MAX_WIDTH_LANDSCAPE,
|
||||
MQ_MAX_WIDTH_PORTRAIT,
|
||||
MQ_RIGHT_SIDEBAR_MIN_WIDTH,
|
||||
MQ_SM_MAX_WIDTH,
|
||||
POINTER_BUTTON,
|
||||
ROUNDNESS,
|
||||
SCROLL_TIMEOUT,
|
||||
@ -381,11 +380,15 @@ const AppContext = React.createContext<AppClassProperties>(null!);
|
||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||
|
||||
const deviceContextInitialValue = {
|
||||
isSmScreen: false,
|
||||
isMobile: false,
|
||||
viewport: {
|
||||
isMobile: false,
|
||||
isLandscape: false,
|
||||
},
|
||||
editor: {
|
||||
isMobile: false,
|
||||
canFitSidebar: false,
|
||||
},
|
||||
isTouchScreen: false,
|
||||
canDeviceFitSidebar: false,
|
||||
isLandscape: false,
|
||||
};
|
||||
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
|
||||
DeviceContext.displayName = "DeviceContext";
|
||||
@ -436,6 +439,9 @@ export const useExcalidrawSetAppState = () =>
|
||||
export const useExcalidrawActionManager = () =>
|
||||
useContext(ExcalidrawActionManagerContext);
|
||||
|
||||
const supportsResizeObserver =
|
||||
typeof window !== "undefined" && "ResizeObserver" in window;
|
||||
|
||||
let didTapTwice: boolean = false;
|
||||
let tappedTwiceTimer = 0;
|
||||
let isHoldingSpace: boolean = false;
|
||||
@ -472,7 +478,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
unmounted: boolean = false;
|
||||
actionManager: ActionManager;
|
||||
device: Device = deviceContextInitialValue;
|
||||
detachIsMobileMqHandler?: () => void;
|
||||
|
||||
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
@ -1180,7 +1185,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
<div
|
||||
className={clsx("excalidraw excalidraw-container", {
|
||||
"excalidraw--view-mode": this.state.viewModeEnabled,
|
||||
"excalidraw--mobile": this.device.isMobile,
|
||||
"excalidraw--mobile": this.device.editor.isMobile,
|
||||
})}
|
||||
style={{
|
||||
["--ui-pointerEvents" as any]:
|
||||
@ -1657,20 +1662,62 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
};
|
||||
|
||||
private refreshDeviceState = (container: HTMLDivElement) => {
|
||||
const { width, height } = container.getBoundingClientRect();
|
||||
private isMobileBreakpoint = (width: number, height: number) => {
|
||||
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 =
|
||||
this.props.UIOptions.dockedSidebarBreakpoint != null
|
||||
? this.props.UIOptions.dockedSidebarBreakpoint
|
||||
: MQ_RIGHT_SIDEBAR_MIN_WIDTH;
|
||||
this.device = updateObject(this.device, {
|
||||
isLandscape: width > height,
|
||||
isSmScreen: width < MQ_SM_MAX_WIDTH,
|
||||
isMobile:
|
||||
width < MQ_MAX_WIDTH_PORTRAIT ||
|
||||
(height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE),
|
||||
canDeviceFitSidebar: width > sidebarBreakpoint,
|
||||
|
||||
const prevEditorState = this.device.editor;
|
||||
|
||||
const nextEditorState = updateObject(prevEditorState, {
|
||||
isMobile: this.isMobileBreakpoint(editorWidth, editorHeight),
|
||||
canFitSidebar: editorWidth > sidebarBreakpoint,
|
||||
});
|
||||
|
||||
if (prevEditorState !== nextEditorState) {
|
||||
this.device = { ...this.device, editor: nextEditorState };
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
public async componentDidMount() {
|
||||
@ -1712,52 +1759,21 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
if (
|
||||
this.excalidrawContainerRef.current &&
|
||||
// bounding rects don't work in tests so updating
|
||||
// the state on init would result in making the test enviro run
|
||||
// in mobile breakpoint (0 width/height), making everything fail
|
||||
!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(() => {
|
||||
// recompute device dimensions state
|
||||
// ---------------------------------------------------------------------
|
||||
this.refreshDeviceState(this.excalidrawContainerRef.current!);
|
||||
// refresh offsets
|
||||
// ---------------------------------------------------------------------
|
||||
this.refreshEditorBreakpoints();
|
||||
this.updateDOMRect();
|
||||
});
|
||||
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));
|
||||
@ -1802,6 +1818,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene
|
||||
.getElementsIncludingDeleted()
|
||||
.forEach((element) => ShapeCache.delete(element));
|
||||
this.refreshViewportBreakpoints();
|
||||
this.updateDOMRect();
|
||||
if (!supportsResizeObserver) {
|
||||
this.refreshEditorBreakpoints();
|
||||
}
|
||||
this.setState({});
|
||||
});
|
||||
|
||||
@ -1855,7 +1876,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
false,
|
||||
);
|
||||
|
||||
this.detachIsMobileMqHandler?.();
|
||||
window.removeEventListener(EVENT.MESSAGE, this.onWindowMessage, false);
|
||||
}
|
||||
|
||||
@ -1940,11 +1960,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
if (
|
||||
this.excalidrawContainerRef.current &&
|
||||
prevProps.UIOptions.dockedSidebarBreakpoint !==
|
||||
this.props.UIOptions.dockedSidebarBreakpoint
|
||||
this.props.UIOptions.dockedSidebarBreakpoint
|
||||
) {
|
||||
this.refreshDeviceState(this.excalidrawContainerRef.current);
|
||||
this.refreshEditorBreakpoints();
|
||||
}
|
||||
|
||||
if (
|
||||
@ -2410,7 +2429,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// from library, not when pasting from clipboard. Alas.
|
||||
openSidebar:
|
||||
this.state.openSidebar &&
|
||||
this.device.canDeviceFitSidebar &&
|
||||
this.device.editor.canFitSidebar &&
|
||||
jotaiStore.get(isSidebarDockedAtom)
|
||||
? this.state.openSidebar
|
||||
: null,
|
||||
@ -2624,7 +2643,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
!isPlainPaste &&
|
||||
textElements.length > 1 &&
|
||||
PLAIN_PASTE_TOAST_SHOWN === false &&
|
||||
!this.device.isMobile
|
||||
!this.device.editor.isMobile
|
||||
) {
|
||||
this.setToast({
|
||||
message: t("toast.pasteAsSingleElement", {
|
||||
@ -2658,7 +2677,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
trackEvent(
|
||||
"toolbar",
|
||||
"toggleLock",
|
||||
`${source} (${this.device.isMobile ? "mobile" : "desktop"})`,
|
||||
`${source} (${this.device.editor.isMobile ? "mobile" : "desktop"})`,
|
||||
);
|
||||
}
|
||||
this.setState((prevState) => {
|
||||
@ -3153,7 +3172,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
trackEvent(
|
||||
"toolbar",
|
||||
shape,
|
||||
`keyboard (${this.device.isMobile ? "mobile" : "desktop"})`,
|
||||
`keyboard (${
|
||||
this.device.editor.isMobile ? "mobile" : "desktop"
|
||||
})`,
|
||||
);
|
||||
}
|
||||
this.setActiveTool({ type: shape });
|
||||
@ -3887,7 +3908,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
element,
|
||||
this.state,
|
||||
[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.state,
|
||||
[lastPointerDownCoords.x, lastPointerDownCoords.y],
|
||||
this.device.isMobile,
|
||||
this.device.editor.isMobile,
|
||||
);
|
||||
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
||||
this.lastPointerUpEvent!,
|
||||
@ -3929,7 +3950,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.hitLinkElement,
|
||||
this.state,
|
||||
[lastPointerUpCoords.x, lastPointerUpCoords.y],
|
||||
this.device.isMobile,
|
||||
this.device.editor.isMobile,
|
||||
);
|
||||
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
||||
let url = this.hitLinkElement.link;
|
||||
@ -4791,7 +4812,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
const clicklength =
|
||||
event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0);
|
||||
if (this.device.isMobile && clicklength < 300) {
|
||||
if (this.device.editor.isMobile && clicklength < 300) {
|
||||
const hitElement = this.getElementAtPosition(
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
|
@ -98,7 +98,7 @@ export const ColorInput = ({
|
||||
}}
|
||||
/>
|
||||
{/* TODO reenable on mobile with a better UX */}
|
||||
{!device.isMobile && (
|
||||
{!device.editor.isMobile && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
|
@ -80,7 +80,7 @@ const ColorPickerPopupContent = ({
|
||||
);
|
||||
|
||||
const { container } = useExcalidrawContainer();
|
||||
const { isMobile, isLandscape } = useDevice();
|
||||
const device = useDevice();
|
||||
|
||||
const colorInputJSX = (
|
||||
<div>
|
||||
@ -136,8 +136,16 @@ const ColorPickerPopupContent = ({
|
||||
updateData({ openPopup: null });
|
||||
setActiveColorPickerSection(null);
|
||||
}}
|
||||
side={isMobile && !isLandscape ? "bottom" : "right"}
|
||||
align={isMobile && !isLandscape ? "center" : "start"}
|
||||
side={
|
||||
device.editor.isMobile && !device.viewport.isLandscape
|
||||
? "bottom"
|
||||
: "right"
|
||||
}
|
||||
align={
|
||||
device.editor.isMobile && !device.viewport.isLandscape
|
||||
? "center"
|
||||
: "start"
|
||||
}
|
||||
alignOffset={-16}
|
||||
sideOffset={20}
|
||||
style={{
|
||||
|
@ -119,7 +119,7 @@ export const Dialog = (props: DialogProps) => {
|
||||
title={t("buttons.close")}
|
||||
aria-label={t("buttons.close")}
|
||||
>
|
||||
{device.isMobile ? back : CloseIcon}
|
||||
{device.editor.isMobile ? back : CloseIcon}
|
||||
</button>
|
||||
<div className="Dialog__content">{props.children}</div>
|
||||
</Island>
|
||||
|
@ -22,7 +22,7 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
||||
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
||||
const multiMode = appState.multiElement !== null;
|
||||
|
||||
if (appState.openSidebar && !device.canDeviceFitSidebar) {
|
||||
if (appState.openSidebar && !device.editor.canFitSidebar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -246,7 +246,7 @@ const LayerUI = ({
|
||||
>
|
||||
<HintViewer
|
||||
appState={appState}
|
||||
isMobile={device.isMobile}
|
||||
isMobile={device.editor.isMobile}
|
||||
device={device}
|
||||
app={app}
|
||||
/>
|
||||
@ -314,7 +314,7 @@ const LayerUI = ({
|
||||
)}
|
||||
>
|
||||
<UserList collaborators={appState.collaborators} />
|
||||
{renderTopRightUI?.(device.isMobile, appState)}
|
||||
{renderTopRightUI?.(device.editor.isMobile, appState)}
|
||||
{!appState.viewModeEnabled &&
|
||||
// hide button when sidebar docked
|
||||
(!isSidebarDocked ||
|
||||
@ -335,7 +335,7 @@ const LayerUI = ({
|
||||
trackEvent(
|
||||
"sidebar",
|
||||
`toggleDock (${docked ? "dock" : "undock"})`,
|
||||
`(${device.isMobile ? "mobile" : "desktop"})`,
|
||||
`(${device.editor.isMobile ? "mobile" : "desktop"})`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@ -363,7 +363,7 @@ const LayerUI = ({
|
||||
trackEvent(
|
||||
"sidebar",
|
||||
`${DEFAULT_SIDEBAR.name} (open)`,
|
||||
`button (${device.isMobile ? "mobile" : "desktop"})`,
|
||||
`button (${device.editor.isMobile ? "mobile" : "desktop"})`,
|
||||
);
|
||||
}
|
||||
}}
|
||||
@ -380,7 +380,7 @@ const LayerUI = ({
|
||||
{appState.errorMessage}
|
||||
</ErrorDialog>
|
||||
)}
|
||||
{eyeDropperState && !device.isMobile && (
|
||||
{eyeDropperState && !device.editor.isMobile && (
|
||||
<EyeDropper
|
||||
colorPickerType={eyeDropperState.colorPickerType}
|
||||
onCancel={() => {
|
||||
@ -450,7 +450,7 @@ const LayerUI = ({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{device.isMobile && (
|
||||
{device.editor.isMobile && (
|
||||
<MobileMenu
|
||||
app={app}
|
||||
appState={appState}
|
||||
@ -469,14 +469,14 @@ const LayerUI = ({
|
||||
renderWelcomeScreen={renderWelcomeScreen}
|
||||
/>
|
||||
)}
|
||||
{!device.isMobile && (
|
||||
{!device.editor.isMobile && (
|
||||
<>
|
||||
<div
|
||||
className="layer-ui__wrapper"
|
||||
style={
|
||||
appState.openSidebar &&
|
||||
isSidebarDocked &&
|
||||
device.canDeviceFitSidebar
|
||||
device.editor.canFitSidebar
|
||||
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
|
||||
: {}
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ export const LibraryUnit = memo(
|
||||
}, [svg]);
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const isMobile = useDevice().isMobile;
|
||||
const isMobile = useDevice().editor.isMobile;
|
||||
const adder = isPending && (
|
||||
<div className="library-unit__adder">{PlusIcon}</div>
|
||||
);
|
||||
|
@ -113,11 +113,11 @@ export const SidebarInner = forwardRef(
|
||||
if ((event.target as Element).closest(".sidebar-trigger")) {
|
||||
return;
|
||||
}
|
||||
if (!docked || !device.canDeviceFitSidebar) {
|
||||
if (!docked || !device.editor.canFitSidebar) {
|
||||
closeLibrary();
|
||||
}
|
||||
},
|
||||
[closeLibrary, docked, device.canDeviceFitSidebar],
|
||||
[closeLibrary, docked, device.editor.canFitSidebar],
|
||||
),
|
||||
);
|
||||
|
||||
@ -125,7 +125,7 @@ export const SidebarInner = forwardRef(
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === KEYS.ESCAPE &&
|
||||
(!docked || !device.canDeviceFitSidebar)
|
||||
(!docked || !device.editor.canFitSidebar)
|
||||
) {
|
||||
closeLibrary();
|
||||
}
|
||||
@ -134,7 +134,7 @@ export const SidebarInner = forwardRef(
|
||||
return () => {
|
||||
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
|
||||
};
|
||||
}, [closeLibrary, docked, device.canDeviceFitSidebar]);
|
||||
}, [closeLibrary, docked, device.editor.canFitSidebar]);
|
||||
|
||||
return (
|
||||
<Island
|
||||
|
@ -18,7 +18,7 @@ export const SidebarHeader = ({
|
||||
const props = useContext(SidebarPropsContext);
|
||||
|
||||
const renderDockButton = !!(
|
||||
device.canDeviceFitSidebar && props.shouldRenderDockButton
|
||||
device.editor.canFitSidebar && props.shouldRenderDockButton
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -30,7 +30,7 @@ const MenuContent = ({
|
||||
});
|
||||
|
||||
const classNames = clsx(`dropdown-menu ${className}`, {
|
||||
"dropdown-menu--mobile": device.isMobile,
|
||||
"dropdown-menu--mobile": device.editor.isMobile,
|
||||
}).trim();
|
||||
|
||||
return (
|
||||
@ -43,7 +43,7 @@ const MenuContent = ({
|
||||
>
|
||||
{/* the zIndex ensures this menu has higher stacking order,
|
||||
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
||||
{device.isMobile ? (
|
||||
{device.editor.isMobile ? (
|
||||
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
||||
) : (
|
||||
<Island
|
||||
|
@ -14,7 +14,7 @@ const MenuItemContent = ({
|
||||
<>
|
||||
<div className="dropdown-menu-item__icon">{icon}</div>
|
||||
<div className="dropdown-menu-item__text">{children}</div>
|
||||
{shortcut && !device.isMobile && (
|
||||
{shortcut && !device.editor.isMobile && (
|
||||
<div className="dropdown-menu-item__shortcut">{shortcut}</div>
|
||||
)}
|
||||
</>
|
||||
|
@ -18,7 +18,7 @@ const MenuTrigger = ({
|
||||
`dropdown-menu-button ${className}`,
|
||||
"zen-mode-transition",
|
||||
{
|
||||
"dropdown-menu-button--mobile": device.isMobile,
|
||||
"dropdown-menu-button--mobile": device.editor.isMobile,
|
||||
},
|
||||
).trim();
|
||||
return (
|
||||
|
@ -29,7 +29,7 @@ const MainMenu = Object.assign(
|
||||
const device = useDevice();
|
||||
const appState = useUIAppState();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
const onClickOutside = device.isMobile
|
||||
const onClickOutside = device.editor.isMobile
|
||||
? undefined
|
||||
: () => setAppState({ openMenu: null });
|
||||
|
||||
@ -54,7 +54,7 @@ const MainMenu = Object.assign(
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
{device.isMobile && appState.collaborators.size > 0 && (
|
||||
{device.editor.isMobile && appState.collaborators.size > 0 && (
|
||||
<fieldset className="UserList-Wrapper">
|
||||
<legend>{t("labels.collaborators")}</legend>
|
||||
<UserList
|
||||
|
@ -21,7 +21,7 @@ const WelcomeScreenMenuItemContent = ({
|
||||
<>
|
||||
<div className="welcome-screen-menu-item__icon">{icon}</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>
|
||||
)}
|
||||
</>
|
||||
|
@ -220,8 +220,6 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
||||
|
||||
// breakpoints
|
||||
// -----------------------------------------------------------------------------
|
||||
// sm screen
|
||||
export const MQ_SM_MAX_WIDTH = 640;
|
||||
// md screen
|
||||
export const MQ_MAX_WIDTH_PORTRAIT = 730;
|
||||
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
|
||||
|
@ -249,8 +249,10 @@ describe("textWysiwyg", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
//@ts-ignore
|
||||
h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!);
|
||||
// @ts-ignore
|
||||
h.app.refreshViewportBreakpoints();
|
||||
// @ts-ignore
|
||||
h.app.refreshEditorBreakpoints();
|
||||
|
||||
textElement = UI.createElement("text");
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useLayoutEffect } from "react";
|
||||
import { useState, useLayoutEffect } from "react";
|
||||
import { useDevice, useExcalidrawContainer } from "../components/App";
|
||||
import { useUIAppState } from "../context/ui-appState";
|
||||
|
||||
@ -10,8 +10,6 @@ export const useCreatePortalContainer = (opts?: {
|
||||
|
||||
const device = useDevice();
|
||||
const { theme } = useUIAppState();
|
||||
const isMobileRef = useRef(device.isMobile);
|
||||
isMobileRef.current = device.isMobile;
|
||||
|
||||
const { container: excalidrawContainer } = useExcalidrawContainer();
|
||||
|
||||
@ -19,11 +17,10 @@ export const useCreatePortalContainer = (opts?: {
|
||||
if (div) {
|
||||
div.className = "";
|
||||
div.classList.add("excalidraw", ...(opts?.className?.split(/\s+/) || []));
|
||||
div.classList.toggle("excalidraw--mobile", device.isMobile);
|
||||
div.classList.toggle("excalidraw--mobile", isMobileRef.current);
|
||||
div.classList.toggle("excalidraw--mobile", device.editor.isMobile);
|
||||
div.classList.toggle("theme--dark", theme === "dark");
|
||||
}
|
||||
}, [div, theme, device.isMobile, opts?.className]);
|
||||
}, [div, theme, device.editor.isMobile, opts?.className]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = opts?.parentSelector
|
||||
|
@ -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)
|
||||
- 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)
|
||||
|
||||
## Excalidraw Library
|
||||
|
@ -8,7 +8,7 @@ const MobileFooter = ({
|
||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
if (device.isMobile) {
|
||||
if (device.editor.isMobile) {
|
||||
return (
|
||||
<Footer>
|
||||
<CustomFooter excalidrawAPI={excalidrawAPI} />
|
||||
|
@ -173,14 +173,18 @@ export const withExcalidrawDimensions = async (
|
||||
) => {
|
||||
mockBoundingClientRect(dimensions);
|
||||
// @ts-ignore
|
||||
window.h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!);
|
||||
h.app.refreshViewportBreakpoints();
|
||||
// @ts-ignore
|
||||
h.app.refreshEditorBreakpoints();
|
||||
window.h.app.refresh();
|
||||
|
||||
await cb();
|
||||
|
||||
restoreOriginalGetBoundingClientRect();
|
||||
// @ts-ignore
|
||||
window.h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!);
|
||||
h.app.refreshViewportBreakpoints();
|
||||
// @ts-ignore
|
||||
h.app.refreshEditorBreakpoints();
|
||||
window.h.app.refresh();
|
||||
};
|
||||
|
||||
|
12
src/types.ts
12
src/types.ts
@ -667,11 +667,15 @@ export type ExcalidrawImperativeAPI = {
|
||||
};
|
||||
|
||||
export type Device = Readonly<{
|
||||
isSmScreen: boolean;
|
||||
isMobile: boolean;
|
||||
viewport: {
|
||||
isMobile: boolean;
|
||||
isLandscape: boolean;
|
||||
};
|
||||
editor: {
|
||||
isMobile: boolean;
|
||||
canFitSidebar: boolean;
|
||||
};
|
||||
isTouchScreen: boolean;
|
||||
canDeviceFitSidebar: boolean;
|
||||
isLandscape: boolean;
|
||||
}>;
|
||||
|
||||
type FrameNameBounds = {
|
||||
|
Loading…
Reference in New Issue
Block a user