diff --git a/src/generic/resizable/Resizable.test.tsx b/src/generic/resizable/Resizable.test.tsx new file mode 100644 index 0000000000..446a65719b --- /dev/null +++ b/src/generic/resizable/Resizable.test.tsx @@ -0,0 +1,89 @@ +import { + initializeMocks, + render, + screen, +} from '@src/testUtils'; + +import { ResizableBox } from './Resizable'; + +describe('', () => { + beforeEach(() => { + initializeMocks(); + window.localStorage.clear(); + }); + + it('reads initial width from localStorage when storageKey is provided', () => { + window.localStorage.setItem('test-sidebar-width', JSON.stringify(600)); + + render( + +
content
+
, + ); + + const root = screen.getByTestId('resizable-box'); + expect(root.getAttribute('style')).toContain('600px'); + }); + + it('falls back to initialWidth when no localStorage value exists', () => { + render( + +
content
+
, + ); + + const root = screen.getByTestId('resizable-box'); + expect(root.getAttribute('style')).toContain('530px'); + }); + + it('falls back to minWidth when neither storageKey value nor initialWidth is set', () => { + render( + +
content
+
, + ); + + const root = screen.getByTestId('resizable-box'); + expect(root.getAttribute('style')).toContain('400px'); + }); + + it('uses initialWidth when storageKey localStorage value is invalid', () => { + window.localStorage.setItem('test-sidebar-width', '"not-a-number"'); + + render( + +
content
+
, + ); + + const root = screen.getByTestId('resizable-box'); + expect(root.getAttribute('style')).toContain('530px'); + }); + + it('merges className onto the root element', () => { + render( + +
content
+
, + ); + + const root = screen.getByTestId('resizable-box'); + expect(root).toHaveClass('resizable'); + expect(root).toHaveClass('my-class'); + }); +}); diff --git a/src/generic/resizable/Resizable.tsx b/src/generic/resizable/Resizable.tsx index adfe1606bb..114b368be1 100644 --- a/src/generic/resizable/Resizable.tsx +++ b/src/generic/resizable/Resizable.tsx @@ -1,8 +1,10 @@ import { useWindowSize } from '@openedx/paragon'; +import classNames from 'classnames'; import React, { useRef, useState, useCallback, + useEffect, useMemo, } from 'react'; @@ -12,6 +14,10 @@ interface ResizableBoxProps { children: React.ReactNode; minWidth?: number; maxWidth?: number; + initialWidth?: number; + storageKey?: string; + className?: string; + 'data-testid'?: string; } /** @@ -21,11 +27,31 @@ export const ResizableBox = ({ children, minWidth = MIN_WIDTH, maxWidth, + initialWidth, + storageKey, + className, + 'data-testid': dataTestId, }: ResizableBoxProps) => { const boxRef = useRef(null); - const [width, setWidth] = useState(minWidth); // initial width + const [width, setWidth] = useState(() => { + if (storageKey) { + const stored = window.localStorage.getItem(storageKey); + if (stored !== null) { + const parsed = Number(JSON.parse(stored)); + if (Number.isFinite(parsed)) { return parsed; } + } + } + return initialWidth ?? minWidth; + }); const { width: windowWidth } = useWindowSize(); + // Persist width to localStorage when storageKey is set + useEffect(() => { + if (storageKey) { + window.localStorage.setItem(storageKey, JSON.stringify(width)); + } + }, [storageKey, width]); + // Store the start values while dragging const startXRef = useRef(0); const startWidthRef = useRef(0); @@ -36,6 +62,11 @@ export const ResizableBox = ({ return Math.abs(windowWidth * 0.65); }, [windowWidth]); + // Clamp width when constraints change (e.g. window resize) + useEffect(() => { + setWidth((w) => Math.min(Math.max(w, minWidth), maxWidth || defaultMaxWidth)); + }, [minWidth, maxWidth, defaultMaxWidth]); + const onMouseMove = useCallback((e: MouseEvent) => { const dx = e.clientX - startXRef.current; // positive = mouse moved right const newWidth = Math.min( @@ -62,7 +93,8 @@ export const ResizableBox = ({ return (
diff --git a/src/library-authoring/LibraryAuthoringPage.scss b/src/library-authoring/LibraryAuthoringPage.scss index 5f0ceb7881..9f5454a467 100644 --- a/src/library-authoring/LibraryAuthoringPage.scss +++ b/src/library-authoring/LibraryAuthoringPage.scss @@ -13,12 +13,16 @@ .library-authoring-sidebar { z-index: 1000; // same as header - flex: 530px 0 0; + flex: 0 0 auto; position: sticky; top: 0; right: 0; height: 100vh; - overflow-y: auto; + + > .w-100 { + height: 100%; + overflow-y: auto; + } } .dropdown-menu { diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 29f71acfaf..e169cd5c1d 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -41,7 +41,7 @@ import { MainFilters } from '@src/library-authoring/library-filters/MainFilters' import { useMultiLibraryContext } from '@src/library-authoring/common/context/MultiLibraryContext'; import { usePublishedFilterContext } from '@src/library-authoring/common/context/PublishedFilterContext'; import LibraryContent from './LibraryContent'; -import { LibrarySidebar } from './library-sidebar'; +import { LibrarySidebarPanel } from './library-sidebar'; import { useComponentPickerContext } from './common/context/ComponentPickerContext'; import { useOptionalLibraryContext } from './common/context/LibraryContext'; import { SidebarBodyItemId, useSidebarContext } from './common/context/SidebarContext'; @@ -404,11 +404,7 @@ const LibraryAuthoringPage = ({ {!componentPickerMode && }
- {!!sidebarItemInfo?.type && ( -
- -
- )} + {!!sidebarItemInfo?.type && } ); }; diff --git a/src/library-authoring/collections/LibraryCollectionPage.tsx b/src/library-authoring/collections/LibraryCollectionPage.tsx index 2b482a2a66..ab53daa77b 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.tsx @@ -33,7 +33,7 @@ import { useComponentPickerContext } from '../common/context/ComponentPickerCont import { useOptionalLibraryContext } from '../common/context/LibraryContext'; import { SidebarBodyItemId, useSidebarContext } from '../common/context/SidebarContext'; import messages from './messages'; -import { LibrarySidebar } from '../library-sidebar'; +import { LibrarySidebarPanel } from '../library-sidebar'; import LibraryCollectionComponents from './LibraryCollectionComponents'; import LibraryFilterByPublished from '../generic/filter-by-published'; @@ -236,11 +236,7 @@ const LibraryCollectionPage = () => { {!componentPickerMode && } - {!!sidebarItemInfo?.type && ( -
- -
- )} + {!!sidebarItemInfo?.type && } ); }; diff --git a/src/library-authoring/library-sidebar/LibrarySidebarPanel.tsx b/src/library-authoring/library-sidebar/LibrarySidebarPanel.tsx new file mode 100644 index 0000000000..ada38cbabb --- /dev/null +++ b/src/library-authoring/library-sidebar/LibrarySidebarPanel.tsx @@ -0,0 +1,15 @@ +import { ResizableBox } from '@src/generic/resizable/Resizable'; + +import { LibrarySidebar } from '.'; + +export const LibrarySidebarPanel = () => ( + + + +); diff --git a/src/library-authoring/library-sidebar/index.ts b/src/library-authoring/library-sidebar/index.ts index 36d2d4a760..55fefea3d1 100644 --- a/src/library-authoring/library-sidebar/index.ts +++ b/src/library-authoring/library-sidebar/index.ts @@ -1 +1,2 @@ export { default as LibrarySidebar } from './LibrarySidebar'; +export { LibrarySidebarPanel } from './LibrarySidebarPanel'; diff --git a/src/library-authoring/section-subsections/LibrarySectionPage.tsx b/src/library-authoring/section-subsections/LibrarySectionPage.tsx index bb2b488d5b..3cc8c0df5a 100644 --- a/src/library-authoring/section-subsections/LibrarySectionPage.tsx +++ b/src/library-authoring/section-subsections/LibrarySectionPage.tsx @@ -13,7 +13,7 @@ import Header from '../../header'; import SubHeader from '../../generic/sub-header/SubHeader'; import { SubHeaderTitle } from '../LibraryAuthoringPage'; import { messages, sectionMessages } from './messages'; -import { LibrarySidebar } from '../library-sidebar'; +import { LibrarySidebarPanel } from '../library-sidebar'; import { LibraryContainerChildren } from './LibraryContainerChildren'; import { ContainerEditableTitle, FooterActions, HeaderActions } from '../containers'; @@ -114,14 +114,7 @@ export const LibrarySectionPage = () => { - {!!sidebarItemInfo?.type && ( -
- -
- )} + {!!sidebarItemInfo?.type && } ); }; diff --git a/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx b/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx index de5e525c5d..034dfe40eb 100644 --- a/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx +++ b/src/library-authoring/section-subsections/LibrarySubsectionPage.tsx @@ -14,7 +14,7 @@ import Header from '../../header'; import SubHeader from '../../generic/sub-header/SubHeader'; import { SubHeaderTitle } from '../LibraryAuthoringPage'; import { subsectionMessages } from './messages'; -import { LibrarySidebar } from '../library-sidebar'; +import { LibrarySidebarPanel } from '../library-sidebar'; import { ParentBreadcrumbs } from '../generic/parent-breadcrumbs'; import { LibraryContainerChildren } from './LibraryContainerChildren'; import { ContainerEditableTitle, FooterActions, HeaderActions } from '../containers'; @@ -104,14 +104,7 @@ export const LibrarySubsectionPage = () => { - {!!sidebarItemInfo?.type && ( -
- -
- )} + {!!sidebarItemInfo?.type && } ); }; diff --git a/src/library-authoring/units/LibraryUnitPage.tsx b/src/library-authoring/units/LibraryUnitPage.tsx index 83de6f55a6..b6123b4224 100644 --- a/src/library-authoring/units/LibraryUnitPage.tsx +++ b/src/library-authoring/units/LibraryUnitPage.tsx @@ -13,7 +13,7 @@ import Header from '../../header'; import { useLibraryContext } from '../common/context/LibraryContext'; import { useSidebarContext } from '../common/context/SidebarContext'; import { useContentFromSearchIndex, useContentLibrary } from '../data/apiHooks'; -import { LibrarySidebar } from '../library-sidebar'; +import { LibrarySidebarPanel } from '../library-sidebar'; import { ParentBreadcrumbs } from '../generic/parent-breadcrumbs'; import { SubHeaderTitle } from '../LibraryAuthoringPage'; import { LibraryUnitBlocks } from './LibraryUnitBlocks'; @@ -108,14 +108,7 @@ export const LibraryUnitPage = () => { - {!!sidebarItemInfo?.type && ( -
- -
- )} + {!!sidebarItemInfo?.type && } ); };