diff --git a/public/translations/en-US.json b/public/translations/en-US.json index a73e8f6b1..62c0ef274 100644 --- a/public/translations/en-US.json +++ b/public/translations/en-US.json @@ -308,8 +308,10 @@ }, "sidebar": { "collapse": "Collapse sidebar", + "collapseInstructions": "Collapse project instructions", "download": "Download project", "expand": "Expand sidebar", + "expandInstructions": "Open project instructions", "file": "Project files", "images": "Image gallery", "settings": "Settings", @@ -355,4 +357,4 @@ "common": { "or": "or" } -} \ No newline at end of file +} diff --git a/public/translations/en.json b/public/translations/en.json index 0ecc5f9e4..178693e70 100644 --- a/public/translations/en.json +++ b/public/translations/en.json @@ -308,8 +308,10 @@ }, "sidebar": { "collapse": "Collapse sidebar", + "collapseInstructions": "Collapse project instructions", "download": "Download project", "expand": "Expand sidebar", + "expandInstructions": "Open project instructions", "file": "Project files", "images": "Image gallery", "settings": "Settings", diff --git a/src/assets/stylesheets/Project.scss b/src/assets/stylesheets/Project.scss index d9fe8c518..3323ef8f3 100644 --- a/src/assets/stylesheets/Project.scss +++ b/src/assets/stylesheets/Project.scss @@ -50,6 +50,32 @@ overflow: hidden; } + .project-bar-row { + display: flex; + align-items: stretch; + gap: var(--project-wrapper-grid-gap, #{$space-0-5}); + + .project-bar { + flex: 1 1 auto; + min-inline-size: 0; + } + } + + .project-bar-row__expand { + display: flex; + align-items: center; + flex-shrink: 0; + box-sizing: border-box; + padding: $space-0-5; + border-radius: $space-0-5; + background-color: var(--editor-color-layer-3); + border: 1px solid var(--editor-color-outline); + + .rpf-button { + border: none; + } + } + .proj-editor-wrapper { display: flex; flex: 0 1 auto; diff --git a/src/assets/stylesheets/Sidebar.scss b/src/assets/stylesheets/Sidebar.scss index 916669c3a..14ec3dea3 100644 --- a/src/assets/stylesheets/Sidebar.scss +++ b/src/assets/stylesheets/Sidebar.scss @@ -122,9 +122,41 @@ .sidebar__panel-heading { margin: 0; + min-inline-size: 0; @include font-size-1-5(regular); } +.sidebar__panel-heading-row { + display: flex; + align-items: center; + gap: var(--space-0-5); + + .rpf-button { + border: none; + } +} + +.sidebar__panel-collapse { + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + flex-shrink: 0; + margin: 0; + border: 1px solid var(--sidebar-border); + border-radius: var(--space-1); + padding: var(--space-1); + color: var(--editor-color-text); + + svg { + margin: 0; + } + + &:hover { + background-color: var(--sidebar-option-hover); + } +} + .sidebar__panel-header { display: flex; flex-direction: column; diff --git a/src/components/Editor/Project/Project.jsx b/src/components/Editor/Project/Project.jsx index a99fe0c36..bbb61fd5f 100644 --- a/src/components/Editor/Project/Project.jsx +++ b/src/components/Editor/Project/Project.jsx @@ -10,6 +10,7 @@ import { showSavedMessage } from "../../../utils/Notifications"; import ProjectBar from "../../ProjectBar/ProjectBar"; import ScratchProjectBar from "../../ProjectBar/ScratchProjectBar"; import Sidebar from "../../Menus/Sidebar/Sidebar"; +import SidebarExpandButton from "../../Menus/Sidebar/SidebarExpandButton"; import EditorInput from "../EditorInput/EditorInput"; import ResizableWithHandle from "../../../utils/ResizableWithHandle"; import { useContainerMinWidth } from "../../../hooks/useContainerMinWidth"; @@ -69,12 +70,20 @@ const Project = (props) => { /> )}
- {withProjectbar && - (isCodeEditorScratchProject ? ( - - ) : ( - - ))} + {withProjectbar && ( +
+ {withSidebar && ( + + )} + {isCodeEditorScratchProject ? ( + + ) : ( + + )} +
+ )} {!loading && !isCodeEditorScratchProject && (
{ +const InstructionsPanel = ({ toggleOption, isMobile }) => { const [tabIndex, setTabIndex] = useState(0); const [showRemoveStepModal, setShowRemoveStepModal] = useState(false); const [removeScope, setRemoveScope] = useState(REMOVE_CURRENT_STEP); @@ -110,11 +110,31 @@ const InstructionsPanel = () => { const panelRef = useRef(null); + const collapsePanel = () => { + toggleOption("instructions"); + if (window.plausible) { + window.plausible("Collapse file pane"); + } + }; + return ( } + iconOnly + text={t("sidebar.collapseInstructions")} + onClick={collapsePanel} + size="small" + type="tertiary" + /> + ) : undefined + } buttons={ instructionsEditable && !hasInstructions ? [ diff --git a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.test.jsx b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.test.jsx index 218233655..090723285 100644 --- a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.test.jsx +++ b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.test.jsx @@ -42,6 +42,44 @@ const selectRemoveModalScope = (scope) => fireEvent.click(removeModalScopeRadio(scope)); }); +describe("The panel header collapse button", () => { + const renderPanel = (props = {}) => + renderWithProviders(, { + preloadedState: { + editor: { + project: { instructions: [{ markdown_content: "instructions" }] }, + instructionsEditable: true, + }, + instructions: { permitOverride: true, currentStepPosition: 0 }, + }, + }); + + test("is not rendered when the panel cannot be toggled", () => { + renderPanel(); + + expect( + screen.queryByTitle("sidebar.collapseInstructions"), + ).not.toBeInTheDocument(); + }); + + test("is not rendered on mobile", () => { + renderPanel({ toggleOption: vi.fn(), isMobile: true }); + + expect( + screen.queryByTitle("sidebar.collapseInstructions"), + ).not.toBeInTheDocument(); + }); + + test("closes the instructions panel when clicked", () => { + const toggleOption = vi.fn(); + renderPanel({ toggleOption }); + + fireEvent.click(screen.getByTitle("sidebar.collapseInstructions")); + + expect(toggleOption).toHaveBeenCalledWith("instructions"); + }); +}); + describe("When instructionsEditable changes from false to true", () => { test("does not leave the rendered preview above the edit/view tabs", () => { const { container, store } = renderWithProviders(, { diff --git a/src/components/Menus/Sidebar/Sidebar.jsx b/src/components/Menus/Sidebar/Sidebar.jsx index 79d3157b0..0605b4a00 100644 --- a/src/components/Menus/Sidebar/Sidebar.jsx +++ b/src/components/Menus/Sidebar/Sidebar.jsx @@ -212,6 +212,8 @@ const Sidebar = ({ } else if (!optionIsAvailable) { setOption(nextDefaultOption); dispatch(setSidebarOption(nextDefaultOption)); + } else { + setOption(selectedSidebarOption); } }, [dispatch, nextDefaultOption, optionIsAvailable, selectedSidebarOption]); @@ -245,7 +247,7 @@ const Sidebar = ({ > )} diff --git a/src/components/Menus/Sidebar/SidebarBar.jsx b/src/components/Menus/Sidebar/SidebarBar.jsx index 78c38f655..4bbeff0a7 100644 --- a/src/components/Menus/Sidebar/SidebarBar.jsx +++ b/src/components/Menus/Sidebar/SidebarBar.jsx @@ -16,7 +16,7 @@ import { MOBILE_MEDIA_QUERY } from "../../../utils/mediaQueryBreakpoints"; const SidebarBar = (props) => { const { menuOptions, - option, + activeOption, toggleOption, instructions = false, allowMobileView = true, @@ -59,7 +59,7 @@ const SidebarBar = (props) => { }; const collapsePopOut = () => { - toggleOption(option); + toggleOption(activeOption); if (window.plausible) { window.plausible("Collapse file pane"); } @@ -81,7 +81,7 @@ const SidebarBar = (props) => { return (
@@ -95,7 +95,7 @@ const SidebarBar = (props) => { key={i} Icon={menuOption.icon} title={menuOption.title} - isActive={option === menuOption.name} + isActive={activeOption === menuOption.name} toggleOption={toggleOption} name={menuOption.name} /> @@ -107,13 +107,13 @@ const SidebarBar = (props) => { key={i} Icon={menuOption.icon} title={menuOption.title} - isActive={option === menuOption.name} + isActive={activeOption === menuOption.name} toggleOption={toggleOption} name={menuOption.name} /> ))} {!isMobile && - (option ? ( + (activeOption ? (
+ ); +}; + +SidebarExpandButton.propTypes = { + allowMobileView: PropTypes.bool, +}; + +export default SidebarExpandButton; diff --git a/src/components/Menus/Sidebar/SidebarExpandButton.test.jsx b/src/components/Menus/Sidebar/SidebarExpandButton.test.jsx new file mode 100644 index 000000000..9f302310b --- /dev/null +++ b/src/components/Menus/Sidebar/SidebarExpandButton.test.jsx @@ -0,0 +1,83 @@ +import React from "react"; +import { fireEvent, render, screen } from "@testing-library/react"; +import configureStore from "redux-mock-store"; +import { Provider } from "react-redux"; + +import SidebarExpandButton from "./SidebarExpandButton"; +import { setSidebarOption } from "../../../redux/EditorSlice"; + +const renderButton = (editorState = {}, instructionsState = {}, props = {}) => { + const mockStore = configureStore([]); + const store = mockStore({ + editor: { + project: { components: [] }, + ...editorState, + }, + instructions: instructionsState, + }); + + return { + store, + ...render( + + + , + ), + }; +}; + +describe("SidebarExpandButton", () => { + test("is hidden while the sidebar panel is open", () => { + renderButton({ selectedSidebarOption: "instructions" }); + + expect( + screen.queryByTitle("sidebar.expandInstructions"), + ).not.toBeInTheDocument(); + }); + + test("is hidden before a sidebar option has been stored", () => { + renderButton({ instructionsEditable: true }); + + expect( + screen.queryByTitle("sidebar.expandInstructions"), + ).not.toBeInTheDocument(); + }); + + test("is hidden when the project has no instructions to show", () => { + renderButton({ selectedSidebarOption: null, instructionsEditable: false }); + + expect( + screen.queryByTitle("sidebar.expandInstructions"), + ).not.toBeInTheDocument(); + }); + + test("is shown when the panel is collapsed and instructions are editable", () => { + renderButton({ selectedSidebarOption: null, instructionsEditable: true }); + + expect( + screen.queryByTitle("sidebar.expandInstructions"), + ).toBeInTheDocument(); + }); + + test("is shown when the panel is collapsed and the project has steps", () => { + renderButton( + { selectedSidebarOption: null, instructionsEditable: false }, + { project: { steps: [{ content: "

step 0

" }] } }, + ); + + expect( + screen.queryByTitle("sidebar.expandInstructions"), + ).toBeInTheDocument(); + }); + + test("opens the instructions panel when clicked", () => { + const { store } = renderButton({ + selectedSidebarOption: null, + instructionsEditable: true, + }); + + fireEvent.click(screen.getByTitle("sidebar.expandInstructions")); + + expect(store.getActions()).toEqual([setSidebarOption("instructions")]); + }); +}); diff --git a/src/components/Menus/Sidebar/SidebarPanel.jsx b/src/components/Menus/Sidebar/SidebarPanel.jsx index 713eead77..15af5feeb 100644 --- a/src/components/Menus/Sidebar/SidebarPanel.jsx +++ b/src/components/Menus/Sidebar/SidebarPanel.jsx @@ -13,6 +13,7 @@ const SidebarPanel = (props) => { className, buttons, headerContent, + headingPrefix, panelRef, defaultWidth = "320px", } = props; @@ -24,7 +25,10 @@ const SidebarPanel = (props) => { const panelContent = ( <>
-

{heading}

+
+ {headingPrefix} +

{heading}

+
{buttons && !buttonsIsEmptyArray && (
{buttons}
)} @@ -65,6 +69,7 @@ SidebarPanel.propTypes = { className: PropTypes.string, buttons: PropTypes.arrayOf(PropTypes.node), headerContent: PropTypes.node, + headingPrefix: PropTypes.node, }; export default SidebarPanel;