Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const CourseOutline = () => {
return (
<div className={classNames('outline-sidebar-wrapper', {
'flex-shrink-0 mr-4 h-auto': !shouldDisplayFullScreen,
'bg-white m-0 fixed-top w-100 vh-100': shouldDisplayFullScreen,
'bg-white m-0 fixed-top w-100 dvh-100': shouldDisplayFullScreen,
})}
>
<section className="outline-sidebar w-100">
Expand All @@ -80,7 +80,7 @@ export const CourseOutline = () => {
return (
<div className={classNames('outline-sidebar-wrapper', {
'flex-shrink-0 mr-4 h-auto': !shouldDisplayFullScreen,
'bg-white m-0 fixed-top w-100 vh-100': shouldDisplayFullScreen,
'bg-white m-0 fixed-top w-100 dvh-100': shouldDisplayFullScreen,
})}
>
<section className="outline-sidebar w-100">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
.outline-sidebar-wrapper {
width: 32.125rem;
max-width: 100%;
position: relative;
flex-shrink: 0;

// The desktop sidebar is positioned relatively. On small screens the sidebar
// is rendered as a full-screen overlay via Bootstrap's `fixed-top` utility.
&:not(.fixed-top) {
position: relative;
}

// The fixed, full-height overlay needs its own scroll context.
&.fixed-top {
overflow-y: auto;
}
}

.outline-sidebar {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ describe('<CourseOutlineTray />', () => {
let courseId;
let mockData;

const { innerWidth: originalInnerWidth, innerHeight: originalInnerHeight } = window;

afterEach(() => {
window.innerWidth = originalInnerWidth;
window.innerHeight = originalInnerHeight;
});

const initTestStore = async (options) => {
store = await initializeTestStore(options);
const state = store.getState();
Expand Down Expand Up @@ -108,6 +115,55 @@ describe('<CourseOutlineTray />', () => {
expect(mockToggleSidebar).toHaveBeenCalledWith(null);
});

it('does not collapse sidebar when only the window height changes', async () => {
const mockToggleSidebar = jest.fn();
window.innerWidth = 500;
window.innerHeight = 800;
await initTestStore();
renderWithProvider({ toggleSidebar: mockToggleSidebar });

// Mobile browsers fire `resize` while scrolling, when the URL bar shows/hides. Only the
// height changes, and the sidebar must stay open so its content remains scrollable.
window.innerHeight = 650;
window.dispatchEvent(new Event('resize'));

expect(mockToggleSidebar).not.toHaveBeenCalled();
});

it('does not collapse sidebar when resized to a width that still displays it', async () => {
const mockToggleSidebar = jest.fn();
window.innerWidth = 1300;
await initTestStore();
renderWithProvider({ toggleSidebar: mockToggleSidebar });

window.innerWidth = 1250;
window.dispatchEvent(new Event('resize'));

expect(mockToggleSidebar).not.toHaveBeenCalled();
});

it('tracks the last window width across resize events', async () => {
const mockToggleSidebar = jest.fn();
window.innerWidth = 1300;
await initTestStore();
renderWithProvider({ toggleSidebar: mockToggleSidebar });

// A width change above the breakpoint is a no-op, but it updates the tracked width.
window.innerWidth = 1250;
window.dispatchEvent(new Event('resize'));
expect(mockToggleSidebar).not.toHaveBeenCalled();

// A subsequent resize below the breakpoint is still detected as a width change.
window.innerWidth = 1100;
window.dispatchEvent(new Event('resize'));
expect(mockToggleSidebar).toHaveBeenCalledWith(null);

// Repeating the same width does not trigger another collapse.
mockToggleSidebar.mockClear();
window.dispatchEvent(new Event('resize'));
expect(mockToggleSidebar).not.toHaveBeenCalled();
});

it('navigates to section or sequence level correctly on click by back/section button', async () => {
const user = userEvent.setup();
await initTestStore();
Expand Down
13 changes: 12 additions & 1 deletion src/courseware/course/sidebar/sidebars/course-outline/hooks.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
useContext, useEffect, useLayoutEffect, useState,
useContext, useEffect, useLayoutEffect, useRef, useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
Expand Down Expand Up @@ -103,8 +103,19 @@ export const useCourseOutlineSidebar = () => {
}, [courseId, courseOutlineShouldUpdate]);

// Collapse sidebar if screen resized to a width that displays the sidebar automatically
const lastWindowWidth = useRef(global.innerWidth);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be simplified by using matchMedia?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brian-smith-tcril, I like this idea. However, I'm not sure about all the consequences. E.g., it would likely change existing behavior (e.g., when rotating the phone) beyond what we can consider a bug fix. A follow-up refactor would be more suitable for such a change.

useLayoutEffect(() => {
const handleResize = () => {
const widthChanged = global.innerWidth !== lastWindowWidth.current;
lastWindowWidth.current = global.innerWidth;

// Only react to actual width changes. Mobile browsers fire `resize` on vertical scroll
// (the URL bar showing/hiding changes only the viewport height), and reacting to those
// would close the sidebar while the user is simply scrolling its content.
if (!widthChanged) {
return;
}

// breakpoints.large.maxWidth is 1200px and currently the breakpoint for showing the sidebar
if (currentSidebar === ID && global.innerWidth < breakpoints.large.maxWidth) {
collapseSidebar();
Expand Down