@@ -3,8 +3,12 @@ import { Icon, IconButton } from '@openedx/paragon';
33import { ArrowBackIos , Close } from '@openedx/paragon/icons' ;
44import classNames from 'classnames' ;
55import PropTypes from 'prop-types' ;
6- import { useCallback , useContext } from 'react' ;
6+ import {
7+ useCallback , useContext , useEffect , useRef ,
8+ } from 'react' ;
9+
710import { useEventListener } from '@src/generic/hooks' ;
11+ import { setSessionStorage , getSessionStorage } from '@src/data/sessionStorage' ;
812import messages from '../../messages' ;
913import SidebarContext from '../SidebarContext' ;
1014
@@ -19,21 +23,127 @@ const SidebarBase = ({
1923} ) => {
2024 const intl = useIntl ( ) ;
2125 const {
26+ courseId,
2227 toggleSidebar,
2328 shouldDisplayFullScreen,
2429 currentSidebar,
2530 } = useContext ( SidebarContext ) ;
2631
32+ const closeBtnRef = useRef ( null ) ;
33+ const responsiveCloseNotificationTrayRef = useRef ( null ) ;
34+ const isOpenNotificationTray = getSessionStorage ( `notificationTrayStatus.${ courseId } ` ) === 'open' ;
35+ const isFocusedNotificationTray = getSessionStorage ( `notificationTrayFocus.${ courseId } ` ) === 'true' ;
36+
37+ useEffect ( ( ) => {
38+ if ( isOpenNotificationTray && isFocusedNotificationTray && closeBtnRef . current ) {
39+ closeBtnRef . current . focus ( ) ;
40+ }
41+
42+ if ( shouldDisplayFullScreen ) {
43+ responsiveCloseNotificationTrayRef . current ?. focus ( ) ;
44+ }
45+ } ) ;
46+
2747 const receiveMessage = useCallback ( ( { data } ) => {
2848 const { type } = data ;
2949 if ( type === 'learning.events.sidebar.close' ) {
3050 toggleSidebar ( null ) ;
3151 }
3252 // eslint-disable-next-line react-hooks/exhaustive-deps
33- } , [ sidebarId , toggleSidebar ] ) ;
53+ } , [ toggleSidebar ] ) ;
3454
3555 useEventListener ( 'message' , receiveMessage ) ;
3656
57+ const focusSidebarTriggerBtn = ( ) => {
58+ const performFocus = ( ) => {
59+ const sidebarTriggerBtn = document . querySelector ( '.sidebar-trigger-btn' ) ;
60+ if ( sidebarTriggerBtn ) {
61+ sidebarTriggerBtn . focus ( ) ;
62+ }
63+ } ;
64+
65+ requestAnimationFrame ( ( ) => {
66+ requestAnimationFrame ( performFocus ) ;
67+ } ) ;
68+ } ;
69+
70+ const handleCloseNotificationTray = ( ) => {
71+ toggleSidebar ( null ) ;
72+ setSessionStorage ( `notificationTrayFocus.${ courseId } ` , 'true' ) ;
73+ setSessionStorage ( `notificationTrayStatus.${ courseId } ` , 'closed' ) ;
74+ focusSidebarTriggerBtn ( ) ;
75+ } ;
76+
77+ const handleKeyDown = useCallback ( ( event ) => {
78+ const { key, shiftKey, target } = event ;
79+
80+ if ( key !== 'Tab' || target !== closeBtnRef . current ) {
81+ return ;
82+ }
83+
84+ // Shift + Tab
85+ if ( shiftKey ) {
86+ event . preventDefault ( ) ;
87+ focusSidebarTriggerBtn ( ) ;
88+ return ;
89+ }
90+
91+ // Tab
92+ const courseOutlineTrigger = document . querySelector ( '#courseOutlineTrigger' ) ;
93+ if ( courseOutlineTrigger ) {
94+ event . preventDefault ( ) ;
95+ courseOutlineTrigger . focus ( ) ;
96+ return ;
97+ }
98+
99+ const leftArrow = document . querySelector ( '.previous-button' ) ;
100+ if ( leftArrow && ! leftArrow . disabled ) {
101+ event . preventDefault ( ) ;
102+ leftArrow . focus ( ) ;
103+ return ;
104+ }
105+
106+ const rightArrow = document . querySelector ( '.next-button' ) ;
107+ if ( rightArrow && ! rightArrow . disabled ) {
108+ event . preventDefault ( ) ;
109+ rightArrow . focus ( ) ;
110+ }
111+ } , [ focusSidebarTriggerBtn , closeBtnRef ] ) ;
112+
113+ useEffect ( ( ) => {
114+ document . addEventListener ( 'keydown' , handleKeyDown ) ;
115+ return ( ) => {
116+ document . removeEventListener ( 'keydown' , handleKeyDown ) ;
117+ } ;
118+ } , [ handleKeyDown ] ) ;
119+
120+ const handleKeyDownNotificationTray = ( event ) => {
121+ const { key, shiftKey } = event ;
122+ const currentElement = event . target === responsiveCloseNotificationTrayRef . current ;
123+ const sidebarTriggerBtn = document . querySelector ( '.call-to-action-btn' ) ;
124+
125+ switch ( key ) {
126+ case 'Enter' :
127+ if ( currentElement ) {
128+ handleCloseNotificationTray ( ) ;
129+ }
130+ break ;
131+
132+ case 'Tab' :
133+ if ( ! shiftKey && sidebarTriggerBtn ) {
134+ event . preventDefault ( ) ;
135+ sidebarTriggerBtn . focus ( ) ;
136+ } else if ( shiftKey ) {
137+ event . preventDefault ( ) ;
138+ responsiveCloseNotificationTrayRef . current ?. focus ( ) ;
139+ }
140+ break ;
141+
142+ default :
143+ break ;
144+ }
145+ } ;
146+
37147 return (
38148 < section
39149 className = { classNames ( 'ml-0 border border-light-400 rounded-sm h-auto align-top zindex-0' , {
@@ -49,9 +159,10 @@ const SidebarBase = ({
49159 { shouldDisplayFullScreen ? (
50160 < div
51161 className = "pt-2 pb-2.5 border-bottom border-light-400 d-flex align-items-center ml-2"
52- onClick = { ( ) => toggleSidebar ( null ) }
53- onKeyDown = { ( ) => toggleSidebar ( null ) }
162+ onClick = { handleCloseNotificationTray }
163+ onKeyDown = { handleKeyDownNotificationTray }
54164 role = "button"
165+ ref = { responsiveCloseNotificationTrayRef }
55166 tabIndex = "0"
56167 >
57168 < Icon src = { ArrowBackIos } />
@@ -63,16 +174,20 @@ const SidebarBase = ({
63174 { showTitleBar && (
64175 < >
65176 < div className = "d-flex align-items-center mb-2" >
66- < strong className = "p-2.5 d-inline-block course-sidebar-title" > { title } </ strong >
177+ { /* TODO: view this title in UI and decide */ }
178+ { /* <strong className="p-2.5 d-inline-block course-sidebar-title">{title}</strong> */ }
179+ < h2 className = "p-2.5 d-inline-block m-0 text-gray-700 h4" > { title } </ h2 >
67180 { shouldDisplayFullScreen
68181 ? null
69182 : (
70183 < div className = "d-inline-flex mr-2 ml-auto" >
71184 < IconButton
185+ className = "sidebar-close-btn"
72186 src = { Close }
73187 size = "sm"
188+ ref = { closeBtnRef }
74189 iconAs = { Icon }
75- onClick = { ( ) => toggleSidebar ( null ) }
190+ onClick = { handleCloseNotificationTray }
76191 variant = "primary"
77192 alt = { intl . formatMessage ( messages . closeNotificationTrigger ) }
78193 />
0 commit comments