Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion src/courseware/course/Course.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const Course = ({
const SidebarProviderComponent = isNewDiscussionSidebarViewEnabled ? NewSidebarProvider : SidebarProvider;

return (
<SidebarProviderComponent courseId={courseId} unitId={unitId}>
<SidebarProviderComponent courseId={courseId} unitId={unitId} sectionId={section ? section.id : null}>
<Helmet>
<title>{`${pageTitleBreadCrumbs.join(' | ')} | ${getConfig().SITE_NAME}`}</title>
</Helmet>
Expand Down
7 changes: 4 additions & 3 deletions src/courseware/course/Course.test.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import React from 'react';

import { Factory } from 'rosie';

import { breakpoints } from '@openedx/paragon';
Expand All @@ -11,6 +9,7 @@ import * as celebrationUtils from './celebration/utils';
import { handleNextSectionCelebration } from './celebration';
import Course from './Course';
import setupDiscussionSidebar from './test-utils';
import messages from './messages';

jest.mock('@edx/frontend-platform/analytics');
jest.mock('@edx/frontend-lib-special-exams/dist/data/thunks.js', () => ({
Expand Down Expand Up @@ -194,7 +193,9 @@ describe('Course', () => {
it('handles click to open/close notification tray', async () => {
await setupDiscussionSidebar();
waitFor(() => {
const notificationShowButton = screen.findByRole('button', { name: /Show notification tray/i });
const notificationShowButton = screen.findByRole('button', {
name: messages.openNotificationTrigger.defaultMessage,
});
expect(screen.queryByRole('region', { name: /notification tray/i })).not.toBeInTheDocument();
fireEvent.click(notificationShowButton);
expect(screen.queryByRole('region', { name: /notification tray/i })).toBeInTheDocument();
Expand Down
1 change: 1 addition & 0 deletions src/courseware/course/JumpNavMenuItem.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const mockData = {
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
sequenceId: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',

currentSequence: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious. Why was this change only added now?

Copy link
Author

Choose a reason for hiding this comment

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

my mistake, removed

title: 'Demo Menu Item',
courseId: 'course-v1:edX+DemoX+Demo_Course',
currentUnit: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
Expand Down
2 changes: 1 addition & 1 deletion src/courseware/course/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const messages = defineMessages({
},
openNotificationTrigger: {
id: 'notification.open.button',
defaultMessage: 'Show notification tray',
defaultMessage: 'Notifications tray',
description: 'Button to open the notification tray and show notifications',
},
closeNotificationTrigger: {
Expand Down
5 changes: 4 additions & 1 deletion src/courseware/course/sidebar/SidebarContextProvider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { SIDEBARS } from './sidebars';
const SidebarProvider = ({
courseId,
unitId,
sectionId,
children,
}) => {
const { verifiedMode } = useModel('courseHomeMeta', courseId);
Expand Down Expand Up @@ -72,8 +73,9 @@ const SidebarProvider = ({
shouldDisplayFullScreen,
courseId,
unitId,
sectionId,
}), [courseId, currentSidebar, notificationStatus, onNotificationSeen, shouldDisplayFullScreen,
shouldDisplaySidebarOpen, toggleSidebar, unitId, upgradeNotificationCurrentState]);
shouldDisplaySidebarOpen, toggleSidebar, unitId, upgradeNotificationCurrentState, sectionId]);

return (
<SidebarContext.Provider value={contextValue}>
Expand All @@ -85,6 +87,7 @@ const SidebarProvider = ({
SidebarProvider.propTypes = {
courseId: PropTypes.string.isRequired,
unitId: PropTypes.string.isRequired,
sectionId: PropTypes.string.isRequired,
children: PropTypes.node,
};

Expand Down
127 changes: 121 additions & 6 deletions src/courseware/course/sidebar/common/SidebarBase.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import { Icon, IconButton } from '@openedx/paragon';
import { ArrowBackIos, Close } from '@openedx/paragon/icons';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { useCallback, useContext } from 'react';
import {
useCallback, useContext, useEffect, useRef,
} from 'react';

import { useEventListener } from '@src/generic/hooks';
import { setSessionStorage, getSessionStorage } from '@src/data/sessionStorage';
import messages from '../../messages';
import SidebarContext from '../SidebarContext';

Expand All @@ -19,21 +23,127 @@ const SidebarBase = ({
}) => {
const intl = useIntl();
const {
courseId,
toggleSidebar,
shouldDisplayFullScreen,
currentSidebar,
} = useContext(SidebarContext);

const closeBtnRef = useRef(null);
const responsiveCloseNotificationTrayRef = useRef(null);
const isOpenNotificationTray = getSessionStorage(`notificationTrayStatus.${courseId}`) === 'open';
const isFocusedNotificationTray = getSessionStorage(`notificationTrayFocus.${courseId}`) === 'true';

useEffect(() => {
if (isOpenNotificationTray && isFocusedNotificationTray && closeBtnRef.current) {
closeBtnRef.current.focus();
}

if (shouldDisplayFullScreen) {
responsiveCloseNotificationTrayRef.current?.focus();
}
});

const receiveMessage = useCallback(({ data }) => {
const { type } = data;
if (type === 'learning.events.sidebar.close') {
toggleSidebar(null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sidebarId, toggleSidebar]);
}, [toggleSidebar]);
Copy link
Contributor

Choose a reason for hiding this comment

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

[clarify]: Is there any reason why sidebarId was removed from deps? Also, do I need to remove // ​​eslint-disable-next-line react-hooks/exhaustive-deps?

Copy link
Author

Choose a reason for hiding this comment

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

no reason, removed

Copy link
Contributor

Choose a reason for hiding this comment

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

My question is related to the fact that sidebarId was previously included in the dependency array. Why was it removed?

Copy link
Author

Choose a reason for hiding this comment

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

because sidebarId it was an unnecessary dependency


useEventListener('message', receiveMessage);

const focusSidebarTriggerBtn = () => {
const performFocus = () => {
const sidebarTriggerBtn = document.querySelector('.sidebar-trigger-btn');
if (sidebarTriggerBtn) {
sidebarTriggerBtn.focus();
}
};

requestAnimationFrame(() => {
requestAnimationFrame(performFocus);
});
};

const handleCloseNotificationTray = () => {
toggleSidebar(null);
setSessionStorage(`notificationTrayFocus.${courseId}`, 'true');
setSessionStorage(`notificationTrayStatus.${courseId}`, 'closed');
focusSidebarTriggerBtn();
};

const handleKeyDown = useCallback((event) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit]: I think it's worth adding a meaningful comment here that would describe how the logic of moving focus around the page works and for which elements.

Copy link
Author

Choose a reason for hiding this comment

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

i added describe text for function, please check it

const { key, shiftKey, target } = event;

if (key !== 'Tab' || target !== closeBtnRef.current) {
return;
}

// Shift + Tab
if (shiftKey) {
event.preventDefault();
focusSidebarTriggerBtn();
return;
}

// Tab
const courseOutlineTrigger = document.querySelector('#courseOutlineTrigger');
Copy link
Contributor

Choose a reason for hiding this comment

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

[optional]: Maybe it would be better to clarify what exactly this is a sidebar trigger.

Suggested change
const courseOutlineTrigger = document.querySelector('#courseOutlineTrigger');
const courseOutlineSidebarTrigger = document.querySelector('#courseOutlineTrigger');

Copy link
Author

Choose a reason for hiding this comment

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

yes, this naming better, fixed

if (courseOutlineTrigger) {
event.preventDefault();
courseOutlineTrigger.focus();
return;
}

const leftArrow = document.querySelector('.previous-button');
if (leftArrow && !leftArrow.disabled) {
event.preventDefault();
leftArrow.focus();
return;
}

const rightArrow = document.querySelector('.next-button');
if (rightArrow && !rightArrow.disabled) {
event.preventDefault();
rightArrow.focus();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

[question]: These are two almost identical constructs. Can we create a function for this that would be convenient to reuse here?

Copy link
Author

Choose a reason for hiding this comment

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

thx, create helper function for this, fixed

}, [focusSidebarTriggerBtn, closeBtnRef]);

useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]);

const handleKeyDownNotificationTray = (event) => {
const { key, shiftKey } = event;
const currentElement = event.target === responsiveCloseNotificationTrayRef.current;
const sidebarTriggerBtn = document.querySelector('.call-to-action-btn');

switch (key) {
case 'Enter':
if (currentElement) {
handleCloseNotificationTray();
}
break;

case 'Tab':
if (!shiftKey && sidebarTriggerBtn) {
event.preventDefault();
sidebarTriggerBtn.focus();
} else if (shiftKey) {
event.preventDefault();
responsiveCloseNotificationTrayRef.current?.focus();
}
break;

default:
break;
}
};

return (
<section
className={classNames('ml-0 border border-light-400 rounded-sm h-auto align-top zindex-0', {
Expand All @@ -49,9 +159,10 @@ const SidebarBase = ({
{shouldDisplayFullScreen ? (
<div
className="pt-2 pb-2.5 border-bottom border-light-400 d-flex align-items-center ml-2"
onClick={() => toggleSidebar(null)}
onKeyDown={() => toggleSidebar(null)}
onClick={handleCloseNotificationTray}
onKeyDown={handleKeyDownNotificationTray}
role="button"
ref={responsiveCloseNotificationTrayRef}
tabIndex="0"
>
<Icon src={ArrowBackIos} />
Expand All @@ -63,16 +174,20 @@ const SidebarBase = ({
{showTitleBar && (
<>
<div className="d-flex align-items-center mb-2">
<strong className="p-2.5 d-inline-block course-sidebar-title">{title}</strong>
{/* TODO: view this title in UI and decide */}
{/* <strong className="p-2.5 d-inline-block course-sidebar-title">{title}</strong> */}
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need these comments?

Copy link
Author

Choose a reason for hiding this comment

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

removed

<h2 className="p-2.5 d-inline-block m-0 text-gray-700 h4">{title}</h2>
{shouldDisplayFullScreen
? null
: (
<div className="d-inline-flex mr-2 ml-auto">
<IconButton
className="sidebar-close-btn"
src={Close}
size="sm"
ref={closeBtnRef}
iconAs={Icon}
onClick={() => toggleSidebar(null)}
onClick={handleCloseNotificationTray}
variant="primary"
alt={intl.formatMessage(messages.closeNotificationTrigger)}
/>
Expand Down
20 changes: 15 additions & 5 deletions src/courseware/course/sidebar/common/TriggerBase.jsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
import PropTypes from 'prop-types';
import React from 'react';
import { forwardRef } from 'react';

const SidebarTriggerBase = ({
const SidebarTriggerBase = forwardRef(({
onClick,
onKeyDown,
ariaLabel,
children,
}) => (
isOpenNotificationStatusBar,
sectionId,
}, ref) => (
<button
className="border border-light-400 bg-transparent align-items-center align-content-center d-flex notification-btn"
className="border border-light-400 bg-transparent align-items-center align-content-center d-flex notification-btn sidebar-trigger-btn"
type="button"
onClick={onClick}
onKeyDown={onKeyDown}
aria-label={ariaLabel}
aria-expanded={isOpenNotificationStatusBar}
aria-controls={sectionId}
ref={ref}
>
<div className="icon-container d-flex position-relative align-items-center">
{children}
</div>
</button>
);
));

SidebarTriggerBase.propTypes = {
onClick: PropTypes.func.isRequired,
onKeyDown: PropTypes.func.isRequired,
ariaLabel: PropTypes.string.isRequired,
children: PropTypes.element.isRequired,
isOpenNotificationStatusBar: PropTypes.bool.isRequired,
sectionId: PropTypes.string.isRequired,
};

export default SidebarTriggerBase;
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const CourseOutlineTrigger = ({ isMobileView }) => {
>
<IconButton
alt={intl.formatMessage(messages.toggleCourseOutlineTrigger)}
id="courseOutlineTrigger"
className="outline-sidebar-toggle-btn flex-shrink-0 text-dark bg-light-200 rounded-0"
iconAs={MenuOpenIcon}
onClick={handleToggleCollapse}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const NotificationTray = () => {
shouldDisplayFullScreen,
upgradeNotificationCurrentState,
setUpgradeNotificationCurrentState,
currentSidebar,
} = useContext(SidebarContext);
const course = useModel('coursewareMeta', courseId);

Expand Down Expand Up @@ -80,6 +81,7 @@ const NotificationTray = () => {
courseId={courseId}
notificationCurrentState={upgradeNotificationCurrentState}
setNotificationCurrentState={setUpgradeNotificationCurrentState}
currentSidebar={currentSidebar}
/>
) : (
<p className="p-3 small">{intl.formatMessage(messages.noNotificationsMessage)}</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { breakpoints } from '@openedx/paragon';

import MockAdapter from 'axios-mock-adapter';
import React from 'react';
import { Factory } from 'rosie';
import messages from '../../../messages';
Copy link
Contributor

Choose a reason for hiding this comment

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

[question]: Does it make sense to use aliases here?

Suggested change
import { Factory } from 'rosie';
import messages from '../../../messages';
import { Factory } from 'rosie';
import messages from '../../../messages';

Copy link
Author

Choose a reason for hiding this comment

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

fixed

import {
fireEvent, initializeMockApp, render, screen, waitFor,
} from '../../../../../setupTest';
Expand Down Expand Up @@ -66,7 +66,9 @@ describe('NotificationTray', () => {
);
expect(screen.getByText('Notifications'))
.toBeInTheDocument();
const notificationCloseIconButton = screen.getByRole('button', { name: /Close notification tray/i });
const notificationCloseIconButton = screen.getByRole('button', {
name: messages.closeNotificationTrigger.defaultMessage,
});
expect(notificationCloseIconButton)
.toBeInTheDocument();
expect(notificationCloseIconButton)
Expand Down
Loading