1146 sidebar state mismatch on layout switch#1323
Conversation
The open/closed state was relying on the selected option being set (open) or null (closed). This also avoids needing to keep component state in sync.
There was a problem hiding this comment.
Pull request overview
Adds Redux-backed persistence for the currently selected sidebar tab to prevent mismatches when switching layouts (e.g., mobile/desktop) and when reopening the sidebar.
Changes:
- Introduces
selectedSidebarTabinEditorSlicewith actions to set/clear it. - Updates
Sidebar/SidebarBar/MobileProjectto read and write the selected tab via Redux. - Expands test coverage to assert the new sidebar tab persistence behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/redux/EditorSlice.js | Adds selectedSidebarTab to editor state and exports related actions. |
| src/components/Mobile/MobileProject/MobileProject.jsx | Sets a default sidebar tab in Redux when opening the sidebar on mobile. |
| src/components/Mobile/MobileProject/MobileProject.test.js | Updates expectations to include the new setSelectedSidebarTab dispatch. |
| src/components/Menus/Sidebar/Sidebar.jsx | Resolves initial sidebar option from Redux and dispatches tab updates on toggle. |
| src/components/Menus/Sidebar/SidebarBar.jsx | Uses Redux-selected tab to decide what to expand when the sidebar is collapsed. |
| src/components/Menus/Sidebar/Sidebar.test.js | Adds tests for persistence behavior and plugin auto-open interaction. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Use stored option if it exists and is valid, otherwise use default logic | ||
| const resolveSelectedOption = () => { | ||
| if ( | ||
| selectedSidebarTab && | ||
| menuOptions.find((opt) => opt.name === selectedSidebarTab) | ||
| ) { | ||
| return selectedSidebarTab; | ||
| } | ||
| // Calculate default | ||
| const defaultOption = autoOpenPlugin | ||
| ? autoOpenPlugin.name | ||
| : instructionsEditable || instructionsSteps | ||
| ? "instructions" | ||
| : "file", | ||
| ); | ||
| : "file"; | ||
| dispatch(setSelectedSidebarTab(defaultOption)); | ||
| return defaultOption; | ||
| }; | ||
|
|
||
| const [option, setOption] = useState(() => { | ||
| const selectedTab = resolveSelectedOption(); | ||
| return selectedTab; // Start with sidebar open by default | ||
| }); |
There was a problem hiding this comment.
resolveSelectedOption() dispatches setSelectedSidebarTab while computing the initial state for useState. Dispatching Redux actions during render/initialization is a side effect and can cause double-dispatches in React StrictMode or inconsistent state updates. Consider computing the initial option without dispatch, then syncing selectedSidebarTab in a useEffect after mount (and when dependencies like menuOptions/autoOpenPlugin change).
| // Use stored option if available, otherwise fall back to default logic | ||
| const optionToExpand = | ||
| selectedSidebarTab || (instructions.length > 0 ? "instructions" : "file"); |
There was a problem hiding this comment.
expandPopOut uses selectedSidebarTab without verifying it’s currently available in menuOptions. If available sidebar options change (e.g., images removed when image_list becomes empty), this can toggle to a non-existent panel and render an empty sidebar. Consider validating selectedSidebarTab against menuOptions (or falling back to the same default resolution logic as Sidebar) before calling toggleOption.
| // Use stored option if available, otherwise fall back to default logic | |
| const optionToExpand = | |
| selectedSidebarTab || (instructions.length > 0 ? "instructions" : "file"); | |
| // Use stored option if available and valid, otherwise fall back to default logic | |
| const validMenuOptionNames = menuOptions.map((menuOption) => menuOption.name); | |
| const isStoredTabValid = | |
| selectedSidebarTab && validMenuOptionNames.includes(selectedSidebarTab); | |
| const optionToExpand = isStoredTabValid | |
| ? selectedSidebarTab | |
| : instructions.length > 0 | |
| ? "instructions" | |
| : "file"; |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…//github.com/RaspberryPiFoundation/editor-ui into 1146-sidebar-state-mismatch-on-layout-switch
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| payload: "images", | ||
| }), | ||
| ]), | ||
| ); |
There was a problem hiding this comment.
These tests were changed to only assert that a setSelectedSidebarTab action was dispatched, but they no longer verify that the corresponding panel actually renders. This weakens coverage and could allow UI regressions to slip through; keep (or reintroduce) assertions that the expected panel content is present after the click.
| ); | |
| ); | |
| // Verify that the images panel content is rendered after clicking the images icon | |
| expect(screen.queryByText("image1.jpg")).toBeInTheDocument(); |
| test("Clicking instructions button a second time dispatches toggle action", () => { | ||
| const collapseButton = screen.getByTitle("sidebar.collapse"); | ||
| fireEvent.click(collapseButton); | ||
| const fileButton = screen.getByTitle("sidebar.file"); | ||
| fireEvent.click(fileButton); | ||
| fireEvent.click(fileButton); | ||
| expect( | ||
| screen.queryByText("instructionsPanel.projectSteps"), | ||
| ).not.toBeInTheDocument(); | ||
|
|
||
| const actions = store.getActions(); | ||
| expect(actions.length).toBeGreaterThan(0); // Verify actions were dispatched |
There was a problem hiding this comment.
This assertion (actions.length > 0) doesn't validate the intended behavior (closing/toggling the pane) or even that the correct action(s) were dispatched. Strengthen the test to assert the expected UI state change and/or the specific action types/payloads triggered by the second click.
Summary
Adds selected sidebar option (tab) to Redux state to persist he selected sidebar tab ensuring that the user's sidebar selection is maintained across renders and can be programmatically controlled.
Detail
selectedSidebarTabto the Redux state inEditorSlice, along with new actionssetSelectedSidebarTabandclearSelectedSidebarTabto manage the selected tab.Sidebar.jsxto use the ReduxselectedSidebarTabas the source of truth for the currently selected sidebar option, falling back to default logic if not set, and dispatching Redux actions when the selection changes.SidebarBar.jsxto use the ReduxselectedSidebarTabwhen collapsing/expanding the sidebar manually, ensuring previous option is shown when re-expanded.Sidebar behavior improvements:
autoOpen, instructions, or defaulting to "file", and that Redux is updated accordingly.Mobile sidebar integration:
MobileProject.jsxso that opening the sidebar on first load sets theselectedSidebarTabin Redux to the first available option, improving consistency with desktop behavior.Testing and validation:
Sidebar.test.jsto verify Redux state persistence, correct dispatching of actions, and priority of pluginautoOpenover stored state.setSelectedSidebarTabandshowSidebaractions when the sidebar is opened.