Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 35 additions & 15 deletions src/components/Menus/Sidebar/Sidebar.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import classNames from "classnames";
import { useSelector } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { useMediaQuery } from "react-responsive";

import FilePanel from "./FilePanel/FilePanel";
Expand All @@ -24,9 +24,11 @@ import FileIcon from "../../../utils/FileIcon";
import DownloadPanel from "./DownloadPanel/DownloadPanel";
import InstructionsPanel from "./InstructionsPanel/InstructionsPanel";
import SidebarPanel from "./SidebarPanel";
import { setSidebarOption } from "../../../redux/EditorSlice";

const Sidebar = ({ options = [], plugins = [], allowMobileView = true }) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const projectType = useSelector((state) => state.editor.project.project_type);
const projectImages = useSelector((state) => state.editor.project.image_list);
const instructionsSteps = useSelector(
Expand All @@ -35,6 +37,9 @@ const Sidebar = ({ options = [], plugins = [], allowMobileView = true }) => {
const instructionsEditable = useSelector(
(state) => state.editor.instructionsEditable,
);
const selectedSidebarOption = useSelector(
(state) => state.editor.selectedSidebarOption,
);
const viewportIsMobile = useMediaQuery({ query: MOBILE_MEDIA_QUERY });
const isMobile = allowMobileView && viewportIsMobile;

Expand Down Expand Up @@ -134,28 +139,43 @@ const Sidebar = ({ options = [], plugins = [], allowMobileView = true }) => {
}

const autoOpenPlugin = plugins?.find((plugin) => plugin.autoOpen);

const [option, setOption] = useState(
autoOpenPlugin
? autoOpenPlugin.name
: instructionsEditable || instructionsSteps
? "instructions"
: "file",
);

const hasInstructions = instructionsSteps && instructionsSteps.length > 0;
let defaultOption = "file";
if (autoOpenPlugin) {
defaultOption = autoOpenPlugin.name;
} else if (instructionsEditable || hasInstructions) {
defaultOption = "instructions";
}
const defaultOptionIsAvailable = menuOptions.some(
(menuOption) => menuOption.name === defaultOption,
);
const nextDefaultOption = defaultOptionIsAvailable ? defaultOption : null;
const initialOption =
selectedSidebarOption === undefined
? nextDefaultOption
: selectedSidebarOption;
const [option, setOption] = useState(initialOption);
const optionIsAvailable =
option === null ||
menuOptions.some((menuOption) => menuOption.name === option);

useEffect(() => {
if (!autoOpenPlugin && (instructionsEditable || hasInstructions)) {
setOption("instructions");
if (!optionIsAvailable) {
setOption(nextDefaultOption);
dispatch(setSidebarOption(nextDefaultOption));
}
}, [autoOpenPlugin, instructionsEditable, hasInstructions]);
}, [dispatch, nextDefaultOption, optionIsAvailable]);

const updateOption = (nextOption) => {
setOption(nextOption);
dispatch(setSidebarOption(nextOption));
};

const toggleOption = (newOption) => {
if (option !== newOption) {
setOption(newOption);
updateOption(newOption);
} else if (!isMobile) {
setOption(null);
updateOption(null);
}
};

Expand Down
145 changes: 142 additions & 3 deletions src/components/WebComponentProject/WebComponentProject.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
import React from "react";
import { act, render, screen } from "@testing-library/react";
import { act, fireEvent, render, screen } from "@testing-library/react";
import { Provider } from "react-redux";
import configureStore from "redux-mock-store";
import {
combineReducers,
configureStore as configureRealStore,
} from "@reduxjs/toolkit";
import { matchMedia, setMedia } from "mock-match-media";
import WebComponentProject from "./WebComponentProject";
import { MOBILE_BREAKPOINT } from "../../utils/mediaQueryBreakpoints";
import EditorReducer, { editorInitialState } from "../../redux/EditorSlice";
import InstructionsReducer, {
instructionsInitialState,
} from "../../redux/InstructionsSlice";
import AuthReducer, {
authInitialState,
} from "../../redux/WebComponentAuthSlice";

let mockMediaQuery = (query) => {
return matchMedia(query).matches;
};

jest.mock("react-responsive", () => ({
...jest.requireActual("react-responsive"),
useMediaQuery: ({ query }) => mockMediaQuery(query),
}));

const codeChangedHandler = jest.fn();
const runStartedHandler = jest.fn();
Expand All @@ -23,6 +45,7 @@ let store;
const renderWebComponentProject = ({
projectType,
instructions,
imageList = [],
permitOverride = true,
loading,
codeRunTriggered = false,
Expand All @@ -38,7 +61,7 @@ const renderWebComponentProject = ({
components: [
{ name: "main", extension: "py", content: "print('hello')" },
],
image_list: [],
image_list: imageList,
instructions,
},
loading,
Expand All @@ -55,7 +78,55 @@ const renderWebComponentProject = ({
};
store = mockStore(initialState);

render(
return render(
<Provider store={store}>
<WebComponentProject {...props} />
</Provider>,
);
};

const renderWebComponentProjectWithRealStore = ({
projectType,
instructions,
imageList = [],
permitOverride = true,
loading,
props = {},
editorOverrides = {},
}) => {
const rootReducer = combineReducers({
editor: EditorReducer,
auth: AuthReducer,
instructions: InstructionsReducer,
});
const preloadedState = {
editor: {
...editorInitialState,
...editorOverrides,
project: {
...editorInitialState.project,
project_type: projectType,
components: [
{ name: "main", extension: "py", content: "print('hello')" },
],
image_list: imageList,
instructions,
},
loading,
openFiles: [],
focussedFileIndices: [],
},
instructions: {
...instructionsInitialState,
currentStepPosition: 3,
permitOverride,
},
auth: authInitialState,
};

store = configureRealStore({ reducer: rootReducer, preloadedState });

return render(
<Provider store={store}>
<WebComponentProject {...props} />
</Provider>,
Expand Down Expand Up @@ -254,6 +325,9 @@ describe("When code run finishes", () => {

describe("When withSidebar is true", () => {
beforeEach(() => {
setMedia({
width: "1000px",
});
renderWebComponentProject({
props: { withSidebar: true, sidebarOptions: ["settings"] },
});
Expand All @@ -268,6 +342,71 @@ describe("When withSidebar is true", () => {
});
});

describe("When resizing across the mobile breakpoint", () => {
test("Keeps the selected sidebar panel after switching to mobile and back", () => {
setMedia({
width: "1000px",
});

const view = renderWebComponentProjectWithRealStore({
imageList: [{ filename: "earth.png", url: "/earth.png" }],
props: { withSidebar: true, sidebarOptions: ["file", "images"] },
});

fireEvent.click(screen.getByTitle("sidebar.images"));
expect(screen.queryByText("imagePanel.gallery")).toBeInTheDocument();

act(() => {
setMedia({
width: MOBILE_BREAKPOINT,
});
});

view.rerender(
<Provider store={store}>
<WebComponentProject
withSidebar={true}
sidebarOptions={["file", "images"]}
/>
</Provider>,
);

fireEvent.click(screen.getByText("mobile.menu"));
expect(screen.queryByText("imagePanel.gallery")).toBeInTheDocument();

act(() => {
setMedia({
width: "1000px",
});
});

view.rerender(
<Provider store={store}>
<WebComponentProject
withSidebar={true}
sidebarOptions={["file", "images"]}
/>
</Provider>,
);

expect(screen.queryByText("imagePanel.gallery")).toBeInTheDocument();
});

test("Keeps the sidebar collapsed when persisted as null", () => {
setMedia({
width: "1000px",
});

renderWebComponentProjectWithRealStore({
props: { withSidebar: true, sidebarOptions: ["file", "images"] },
editorOverrides: { selectedSidebarOption: null },
});

expect(screen.queryByTitle("sidebar.expand")).toBeInTheDocument();
expect(screen.queryByText("filePanel.files")).not.toBeInTheDocument();
});
});

describe("When withProjectbar is true", () => {
beforeEach(() => {
renderWebComponentProject({
Expand Down
5 changes: 5 additions & 0 deletions src/redux/EditorSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export const editorInitialState = {
newFileModalShowing: false,
renameFileModalShowing: false,
sidebarShowing: true,
selectedSidebarOption: undefined,
modals: {},
errorDetails: {},
runnerBeingLoaded: null | "pyodide" | "skulpt",
Expand Down Expand Up @@ -362,6 +363,9 @@ export const EditorSlice = createSlice({
hideSidebar: (state) => {
state.sidebarShowing = false;
},
setSidebarOption: (state, action) => {
state.selectedSidebarOption = action.payload;
},
disableTheming: (state) => {
state.isThemeable = false;
},
Expand Down Expand Up @@ -482,6 +486,7 @@ export const {
closeRenameFileModal,
showSidebar,
hideSidebar,
setSidebarOption,
disableTheming,
setErrorDetails,
} = EditorSlice.actions;
Expand Down
Loading