Skip to content
Draft
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
89 changes: 89 additions & 0 deletions src/generic/resizable/Resizable.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
initializeMocks,
render,
screen,
} from '@src/testUtils';

import { ResizableBox } from './Resizable';

describe('<ResizableBox>', () => {
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(
<ResizableBox
data-testid="resizable-box"
storageKey="test-sidebar-width"
minWidth={400}
initialWidth={530}
>
<div>content</div>
</ResizableBox>,
);

const root = screen.getByTestId('resizable-box');
expect(root.getAttribute('style')).toContain('600px');
});

it('falls back to initialWidth when no localStorage value exists', () => {
render(
<ResizableBox
data-testid="resizable-box"
storageKey="test-sidebar-width"
minWidth={400}
initialWidth={530}
>
<div>content</div>
</ResizableBox>,
);

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(
<ResizableBox data-testid="resizable-box" minWidth={400}>
<div>content</div>
</ResizableBox>,
);

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(
<ResizableBox
data-testid="resizable-box"
storageKey="test-sidebar-width"
minWidth={400}
initialWidth={530}
>
<div>content</div>
</ResizableBox>,
);

const root = screen.getByTestId('resizable-box');
expect(root.getAttribute('style')).toContain('530px');
});

it('merges className onto the root element', () => {
render(
<ResizableBox data-testid="resizable-box" className="my-class" minWidth={400}>
<div>content</div>
</ResizableBox>,
);

const root = screen.getByTestId('resizable-box');
expect(root).toHaveClass('resizable');
expect(root).toHaveClass('my-class');
});
});
36 changes: 34 additions & 2 deletions src/generic/resizable/Resizable.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useWindowSize } from '@openedx/paragon';
import classNames from 'classnames';
import React, {
useRef,
useState,
useCallback,
useEffect,
useMemo,
} from 'react';

Expand All @@ -12,6 +14,10 @@ interface ResizableBoxProps {
children: React.ReactNode;
minWidth?: number;
maxWidth?: number;
initialWidth?: number;
storageKey?: string;
className?: string;
'data-testid'?: string;
Comment thread
ormsbee marked this conversation as resolved.
}

/**
Expand All @@ -21,11 +27,31 @@ export const ResizableBox = ({
children,
minWidth = MIN_WIDTH,
maxWidth,
initialWidth,
storageKey,
className,
'data-testid': dataTestId,
}: ResizableBoxProps) => {
const boxRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState<number>(minWidth); // initial width
const [width, setWidth] = useState<number>(() => {
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<number>(0);
const startWidthRef = useRef<number>(0);
Expand All @@ -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(
Expand All @@ -62,7 +93,8 @@ export const ResizableBox = ({

return (
<div
className="resizable"
className={classNames('resizable', className)}
data-testid={dataTestId}
ref={boxRef}
style={{ width: `${width}px` }}
>
Expand Down
8 changes: 6 additions & 2 deletions src/library-authoring/LibraryAuthoringPage.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 2 additions & 6 deletions src/library-authoring/LibraryAuthoringPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -404,11 +404,7 @@ const LibraryAuthoringPage = ({
</Container>
{!componentPickerMode && <StudioFooterSlot containerProps={{ size: undefined }} />}
</div>
{!!sidebarItemInfo?.type && (
<div className="library-authoring-sidebar box-shadow-left-1 bg-white" data-testid="library-sidebar">
<LibrarySidebar />
</div>
)}
{!!sidebarItemInfo?.type && <LibrarySidebarPanel />}
</div>
);
};
Expand Down
8 changes: 2 additions & 6 deletions src/library-authoring/collections/LibraryCollectionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -236,11 +236,7 @@ const LibraryCollectionPage = () => {
</Container>
{!componentPickerMode && <StudioFooterSlot containerProps={{ size: undefined }} />}
</div>
{!!sidebarItemInfo?.type && (
<div className="library-authoring-sidebar box-shadow-left-1 bg-white" data-testid="library-sidebar">
<LibrarySidebar />
</div>
)}
{!!sidebarItemInfo?.type && <LibrarySidebarPanel />}
</div>
);
};
Expand Down
15 changes: 15 additions & 0 deletions src/library-authoring/library-sidebar/LibrarySidebarPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ResizableBox } from '@src/generic/resizable/Resizable';

import { LibrarySidebar } from '.';

export const LibrarySidebarPanel = () => (
<ResizableBox
className="library-authoring-sidebar box-shadow-left-1 bg-white"
data-testid="library-sidebar"
storageKey="library-authoring-sidebar-width"
initialWidth={530}
minWidth={400}
>
<LibrarySidebar />
</ResizableBox>
);
1 change: 1 addition & 0 deletions src/library-authoring/library-sidebar/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as LibrarySidebar } from './LibrarySidebar';
export { LibrarySidebarPanel } from './LibrarySidebarPanel';
11 changes: 2 additions & 9 deletions src/library-authoring/section-subsections/LibrarySectionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -114,14 +114,7 @@ export const LibrarySectionPage = () => {
</Container>
</Container>
</div>
{!!sidebarItemInfo?.type && (
<div
className="library-authoring-sidebar box-shadow-left-1 bg-white"
data-testid="library-sidebar"
>
<LibrarySidebar />
</div>
)}
{!!sidebarItemInfo?.type && <LibrarySidebarPanel />}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -104,14 +104,7 @@ export const LibrarySubsectionPage = () => {
</Container>
</Container>
</div>
{!!sidebarItemInfo?.type && (
<div
className="library-authoring-sidebar box-shadow-left-1 bg-white"
data-testid="library-sidebar"
>
<LibrarySidebar />
</div>
)}
{!!sidebarItemInfo?.type && <LibrarySidebarPanel />}
</div>
);
};
11 changes: 2 additions & 9 deletions src/library-authoring/units/LibraryUnitPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -108,14 +108,7 @@ export const LibraryUnitPage = () => {
</Container>
</Container>
</div>
{!!sidebarItemInfo?.type && (
<div
className="library-authoring-sidebar box-shadow-left-1 bg-white"
data-testid="library-sidebar"
>
<LibrarySidebar />
</div>
)}
{!!sidebarItemInfo?.type && <LibrarySidebarPanel />}
</div>
);
};