diff --git a/package.json b/package.json index a6125853c..9a98788ed 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "react": "^19.2.7", "react-app-polyfill": "^2.0.0", "react-confirm-alert": "^2.8.0", - "react-container-query": "^0.13.0", "react-cookie": "^4.1.1", "react-dom": "^19.2.7", "react-i18next": "^12.0.0", diff --git a/src/components/Editor/Project/Project.jsx b/src/components/Editor/Project/Project.jsx index c0533e202..f6063bcae 100644 --- a/src/components/Editor/Project/Project.jsx +++ b/src/components/Editor/Project/Project.jsx @@ -1,9 +1,7 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useState } from "react"; import { useSelector } from "react-redux"; import "react-tabs/style/react-tabs.css"; import "react-toastify/dist/ReactToastify.css"; -import { useContainerQuery } from "react-container-query"; import classnames from "classnames"; import "../../../assets/stylesheets/Project.scss"; @@ -14,7 +12,7 @@ import ScratchProjectBar from "../../ProjectBar/ScratchProjectBar"; import Sidebar from "../../Menus/Sidebar/Sidebar"; import EditorInput from "../EditorInput/EditorInput"; import ResizableWithHandle from "../../../utils/ResizableWithHandle"; -import { projContainer } from "../../../utils/containerQueries"; +import { useContainerMinWidth } from "../../../hooks/useContainerMinWidth"; import ScratchContainer from "./ScratchContainer"; const Project = (props) => { @@ -43,21 +41,12 @@ const Project = (props) => { } }, [autosave, isCodeEditorScratchProject, saving]); - const [params, containerRef] = useContainerQuery(projContainer); - const [defaultWidth, setDefaultWidth] = useState("auto"); - const [defaultHeight, setDefaultHeight] = useState("auto"); - const [maxWidth, setMaxWidth] = useState("100%"); - const [handleDirection, setHandleDirection] = useState("right"); + const [isDesktop, containerRef] = useContainerMinWidth(720); const [loading, setLoading] = useState(true); - - useMemo(() => { - const isDesktop = params["width-larger-than-720"]; - - setDefaultWidth(isDesktop ? "50%" : "100%"); - setDefaultHeight(isDesktop ? "100%" : "50%"); - setMaxWidth(isDesktop ? "75%" : "100%"); - setHandleDirection(isDesktop ? "right" : "bottom"); - }, [params["width-larger-than-720"]]); + const defaultWidth = isDesktop ? "50%" : "100%"; + const defaultHeight = isDesktop ? "100%" : "50%"; + const maxWidth = isDesktop ? "75%" : "100%"; + const handleDirection = isDesktop ? "right" : "bottom"; useEffect(() => { setLoading(false); diff --git a/src/components/Editor/Project/Project.test.js b/src/components/Editor/Project/Project.test.js index e275a4c16..00fd62ee6 100644 --- a/src/components/Editor/Project/Project.test.js +++ b/src/components/Editor/Project/Project.test.js @@ -6,6 +6,7 @@ import configureStore from "redux-mock-store"; import Project from "./Project"; import { showSavedMessage } from "../../../utils/Notifications"; import { MemoryRouter } from "react-router-dom"; +import { useContainerMinWidth } from "../../../hooks/useContainerMinWidth"; window.HTMLElement.prototype.scrollIntoView = jest.fn(); @@ -15,9 +16,16 @@ jest.mock("react-router-dom", () => ({ })); jest.mock("../../../utils/Notifications"); +jest.mock("../../../hooks/useContainerMinWidth", () => ({ + useContainerMinWidth: jest.fn(), +})); jest.useFakeTimers(); +beforeEach(() => { + useContainerMinWidth.mockReturnValue([false, jest.fn()]); +}); + const user1 = { access_token: "myAccessToken", profile: { @@ -204,6 +212,46 @@ describe("When not logged in and falling on default container width", () => { .length, ).toBe(1); }); + + test("Shows right drag bar with expected params on desktop containers", () => { + useContainerMinWidth.mockReturnValue([true, jest.fn()]); + + const middlewares = []; + const mockStore = configureStore(middlewares); + const initialState = { + editor: { + project: project, + openFiles: [["main.py"]], + focussedFileIndices: [0], + webComponent: false, + }, + auth: {}, + instructions: {}, + }; + const mockedStore = mockStore(initialState); + const { getByTestId } = render( + + +
+ +
+
+
, + ); + + const container = getByTestId("proj-editor-container"); + expect(container).toHaveStyle({ + "min-width": "25%", + "max-width": "75%", + width: "50%", + height: "100%", + }); + + expect( + container.getElementsByClassName("resizable-with-handle__handle--right") + .length, + ).toBe(1); + }); }); test("Successful manual save prompts project saved message", async () => { diff --git a/src/hooks/useContainerMinWidth.js b/src/hooks/useContainerMinWidth.js new file mode 100644 index 000000000..509ddc89c --- /dev/null +++ b/src/hooks/useContainerMinWidth.js @@ -0,0 +1,38 @@ +import { useCallback, useLayoutEffect, useState } from "react"; + +const getObservedWidth = (entry) => { + const contentBoxSize = Array.isArray(entry.contentBoxSize) + ? entry.contentBoxSize[0] + : entry.contentBoxSize; + + return contentBoxSize?.inlineSize ?? entry.contentRect?.width; +}; + +export const useContainerMinWidth = (minWidth) => { + const [container, setContainer] = useState(null); + const [matches, setMatches] = useState(false); + const containerRef = useCallback((node) => setContainer(node), []); + + useLayoutEffect(() => { + if (!container) return undefined; + + const update = (width) => { + if (typeof width === "number") { + setMatches(width >= minWidth); + } + }; + + update(container.getBoundingClientRect().width); + + if (!window.ResizeObserver) return undefined; + + const observer = new window.ResizeObserver((entries) => { + if (entries[0]) update(getObservedWidth(entries[0])); + }); + observer.observe(container); + + return () => observer.disconnect(); + }, [container, minWidth]); + + return [matches, containerRef]; +}; diff --git a/src/hooks/useContainerMinWidth.test.js b/src/hooks/useContainerMinWidth.test.js new file mode 100644 index 000000000..e74743819 --- /dev/null +++ b/src/hooks/useContainerMinWidth.test.js @@ -0,0 +1,84 @@ +import { act, renderHook } from "@testing-library/react"; + +import { useContainerMinWidth } from "./useContainerMinWidth"; + +const originalResizeObserver = window.ResizeObserver; +let observers; + +class MockResizeObserver { + constructor(callback) { + this.callback = callback; + this.observe = jest.fn(); + this.disconnect = jest.fn(); + observers.push(this); + } +} + +const makeElement = (width) => ({ + getBoundingClientRect: jest.fn(() => ({ width })), +}); + +const renderObservedHook = (width) => { + const hook = renderHook(() => useContainerMinWidth(720)); + const element = makeElement(width); + + act(() => { + hook.result.current[1](element); + }); + + return { ...hook, element }; +}; + +describe("useContainerMinWidth", () => { + beforeEach(() => { + observers = []; + window.ResizeObserver = MockResizeObserver; + }); + + afterEach(() => { + window.ResizeObserver = originalResizeObserver; + }); + + test("reports whether the container is at least the minimum width", () => { + const { result } = renderObservedHook(600); + + expect(result.current[0]).toBe(false); + expect(observers[0].observe).toHaveBeenCalled(); + + act(() => { + observers[0].callback([{ contentRect: { width: 720 } }]); + }); + + expect(result.current[0]).toBe(true); + }); + + test("uses contentBoxSize when available", () => { + const { result } = renderObservedHook(600); + + act(() => { + observers[0].callback([ + { contentBoxSize: [{ inlineSize: 721 }], contentRect: { width: 600 } }, + ]); + }); + + expect(result.current[0]).toBe(true); + }); + + test("disconnects the observer on unmount", () => { + const { unmount } = renderObservedHook(800); + const observer = observers[0]; + + unmount(); + + expect(observer.disconnect).toHaveBeenCalled(); + }); + + test("falls back to a one-time measurement without ResizeObserver", () => { + window.ResizeObserver = undefined; + + const { result } = renderObservedHook(800); + + expect(result.current[0]).toBe(true); + expect(observers).toEqual([]); + }); +}); diff --git a/src/utils/containerQueries.js b/src/utils/containerQueries.js deleted file mode 100644 index d0979c509..000000000 --- a/src/utils/containerQueries.js +++ /dev/null @@ -1,8 +0,0 @@ -export const projContainer = { - "width-larger-than-720": { - minWidth: 720, - }, - "width-smaller-than-600": { - maxWidth: 600, - }, -}; diff --git a/yarn.lock b/yarn.lock index 7b3c44d91..4d6512032 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4222,7 +4222,6 @@ __metadata: react: "npm:^19.2.7" react-app-polyfill: "npm:^2.0.0" react-confirm-alert: "npm:^2.8.0" - react-container-query: "npm:^0.13.0" react-cookie: "npm:^4.1.1" react-dev-utils: "npm:^11.0.3" react-dom: "npm:^19.2.7" @@ -7086,13 +7085,6 @@ __metadata: languageName: node linkType: hard -"batch-processor@npm:^1.0.0": - version: 1.0.0 - resolution: "batch-processor@npm:1.0.0" - checksum: 10/59452655203eeb94101770a4c31a3aa81a60f6403ef4e66870f2970f0873ebc795c442610aa420be34535f1e51d644a12f0c5a37fb3bde08bf5c00109ee67d97 - languageName: node - linkType: hard - "batch@npm:0.6.1": version: 0.6.1 resolution: "batch@npm:0.6.1" @@ -8251,13 +8243,6 @@ __metadata: languageName: node linkType: hard -"container-query-toolkit@npm:0.1.3": - version: 0.1.3 - resolution: "container-query-toolkit@npm:0.1.3" - checksum: 10/ac9931c73fa66d80eb25a61b374c8b90b6424d0205b7d5b81d8b208ab03c34d1c69e10561250ca142e1a9f2194e07876c290c06110d58a681bdb613faff150bb - languageName: node - linkType: hard - "content-disposition@npm:0.5.4": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" @@ -9904,15 +9889,6 @@ __metadata: languageName: node linkType: hard -"element-resize-detector@npm:1.1.13": - version: 1.1.13 - resolution: "element-resize-detector@npm:1.1.13" - dependencies: - batch-processor: "npm:^1.0.0" - checksum: 10/206f82f91f2fde29bb9f484f38012b184beb55d9abaaa25372b9e76930d0d917e446ac4302da161824d1ba4d7dc1a620bc8f50fe9a35fefb39274546b9293e10 - languageName: node - linkType: hard - "element-size@npm:^1.1.1": version: 1.1.1 resolution: "element-size@npm:1.1.1" @@ -17968,19 +17944,6 @@ __metadata: languageName: node linkType: hard -"react-container-query@npm:^0.13.0": - version: 0.13.0 - resolution: "react-container-query@npm:0.13.0" - dependencies: - container-query-toolkit: "npm:0.1.3" - resize-observer-lite: "npm:0.2.3" - peerDependencies: - react: ^0.14.0 || ^15.0.0-0 || ^16.0.0-0 || ^17 || ^18 - react-dom: ^0.14.0 || ^15.0.0-0 || ^16.0.0-0 || ^17 || ^18 - checksum: 10/2c310dd6f232c2ed9d5fbd6b9fd72e5a7892a4bcbba667db07046388206a5a4928f5b4c61342528c2761d29cb228a1e2ee09fd541d30fd4a0a8875de03ab4573 - languageName: node - linkType: hard - "react-cookie@npm:^4.1.1": version: 4.1.1 resolution: "react-cookie@npm:4.1.1" @@ -18894,15 +18857,6 @@ __metadata: languageName: node linkType: hard -"resize-observer-lite@npm:0.2.3": - version: 0.2.3 - resolution: "resize-observer-lite@npm:0.2.3" - dependencies: - element-resize-detector: "npm:1.1.13" - checksum: 10/4e947e2df787b39e3608ef779b5af31aafe16138b82322cedb8249106d607299f0d58fee4c375ca97dd689f63685b46ae00a02a8e5a08b1ff9d72308e0638e94 - languageName: node - linkType: hard - "resolve-cwd@npm:^3.0.0": version: 3.0.0 resolution: "resolve-cwd@npm:3.0.0"