From 7c13469ee555340e60654d0b008e54687fa32afa Mon Sep 17 00:00:00 2001 From: Jo Humphrey <31373245+jamdelion@users.noreply.github.com> Date: Wed, 9 Sep 2026 15:57:23 +0100 Subject: [PATCH 1/2] Experiment to add additional project instructions collapse button --- public/translations/en-US.json | 4 +- public/translations/en.json | 2 + src/assets/stylesheets/Project.scss | 26 ++++++ src/assets/stylesheets/Sidebar.scss | 32 +++++++ src/components/Editor/Project/Project.jsx | 21 +++-- .../InstructionsPanel/InstructionsPanel.jsx | 31 ++++++- .../InstructionsPanel.test.jsx | 38 +++++++++ src/components/Menus/Sidebar/Sidebar.jsx | 5 +- src/components/Menus/Sidebar/SidebarBar.jsx | 12 +-- .../Menus/Sidebar/SidebarExpandButton.jsx | 64 ++++++++++++++ .../Sidebar/SidebarExpandButton.test.jsx | 83 +++++++++++++++++++ src/components/Menus/Sidebar/SidebarPanel.jsx | 7 +- 12 files changed, 309 insertions(+), 16 deletions(-) create mode 100644 src/components/Menus/Sidebar/SidebarExpandButton.jsx create mode 100644 src/components/Menus/Sidebar/SidebarExpandButton.test.jsx 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 +113,37 @@ const InstructionsPanel = () => { const panelRef = useRef(null); + const collapsePanel = () => { + toggleOption("instructions"); + if (window.plausible) { + window.plausible("Collapse file pane"); + } + }; + return ( + // + ) : 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; From bc6f0aa467913af946e426f13052d38ab298c95c Mon Sep 17 00:00:00 2001 From: Jo Humphrey <31373245+jamdelion@users.noreply.github.com> Date: Wed, 9 Sep 2026 16:20:36 +0100 Subject: [PATCH 2/2] Use ReactNode instead of material icon string --- .../InstructionsPanel/InstructionsPanel.jsx | 41 ++++++++----------- .../Menus/Sidebar/SidebarExpandButton.jsx | 2 +- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx index 4750db049..b95efc425 100644 --- a/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx +++ b/src/components/Menus/Sidebar/InstructionsPanel/InstructionsPanel.jsx @@ -1,13 +1,13 @@ /* eslint-disable jsx-a11y/anchor-has-content */ // This is disabled because the empty anchor tag is used for translation and will have content when rendered. -import React, { useRef, useState } from "react"; +import { Button } from "@raspberrypifoundation/design-system-react"; +import { useRef, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { Tab, TabList, TabPanel, Tabs } from "react-tabs"; -import SidebarPanel from "../SidebarPanel"; - - +import BinIcon from "../../../../assets/icons/bin.svg"; +import DoubleArrowLeft from "../../../../assets/icons/double_arrow_left.svg"; import PlusIcon from "../../../../assets/icons/plus.svg"; import demoInstructions from "../../../../assets/markdown/demoInstructions.md?raw"; import "../../../../assets/stylesheets/Instructions.scss?inline"; @@ -18,19 +18,16 @@ import { } from "../../../../redux/InstructionsSlice"; import { insertStepAfter, - removeStepAt, - updateStepMarkdown, REMOVE_ALL_STEPS, REMOVE_CURRENT_STEP, + removeStepAt, + updateStepMarkdown, } from "../../../../utils/instructionSteps"; import populateMarkdownTemplate from "../../../../utils/populateMarkdownTemplate"; -import { Button } from "@raspberrypifoundation/design-system-react"; import RemoveInstructionStepModal from "../../../Modals/RemoveInstructionStepModal"; +import SidebarPanel from "../SidebarPanel"; import InstructionsStep from "./InstructionsStep/InstructionsStep"; import ProgressBar from "./ProgressBar/ProgressBar"; -import BinIcon from "../../../../assets/icons/bin.svg"; -import DoubleArrowLeft from "../../../../assets/icons/double_arrow_left.svg"; -import EditorButton from "../../../Button/Button"; const INSTRUCTIONS_GUIDE_URL = "https://help.editor.raspberrypi.org/hc/en-us/articles/52495086715028-How-to-write-project-instructions"; @@ -127,21 +124,15 @@ const InstructionsPanel = ({ toggleOption, isMobile }) => { heading={t("instructionsPanel.projectSteps")} headingPrefix={ !isMobile && toggleOption ? ( -