1
- import React , { useEffect } from 'react' ;
1
+ import React , { useEffect , useRef } from 'react' ;
2
2
import { useParams } from 'react-router' ;
3
3
import { useDispatch } from 'react-redux' ;
4
4
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics' ;
5
- import { injectIntl , intlShape } from '@edx/frontend-platform/i18n' ;
5
+ import { useIntl } from '@edx/frontend-platform/i18n' ;
6
6
import {
7
7
Alert , Button , Icon , Spinner ,
8
8
} from '@openedx/paragon' ;
@@ -18,7 +18,8 @@ import CoursewareSearchResultsFilterContainer from './CoursewareResultsFilter';
18
18
import { updateModel , useModel } from '../../generic/model-store' ;
19
19
import { searchCourseContent } from '../data/thunks' ;
20
20
21
- const CoursewareSearch = ( { intl, ...sectionProps } ) => {
21
+ const CoursewareSearch = ( { ...sectionProps } ) => {
22
+ const { formatMessage } = useIntl ( ) ;
22
23
const { courseId } = useParams ( ) ;
23
24
const { query : searchKeyword , setQuery, clearSearchParams } = useCoursewareSearchParams ( ) ;
24
25
const dispatch = useDispatch ( ) ;
@@ -29,6 +30,7 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
29
30
errors,
30
31
total,
31
32
} = useModel ( 'contentSearchResults' , courseId ) ;
33
+ const dialogRef = useRef ( ) ;
32
34
33
35
useLockScroll ( ) ;
34
36
@@ -44,7 +46,8 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
44
46
searchKeyword : '' ,
45
47
results : [ ] ,
46
48
errors : undefined ,
47
- loading : false ,
49
+ loading :
50
+ false ,
48
51
} ,
49
52
} ) ) ;
50
53
} ;
@@ -66,20 +69,46 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
66
69
setQuery ( value ) ;
67
70
} ;
68
71
69
- useEffect ( ( ) => {
70
- handleSubmit ( searchKeyword ) ;
71
- } , [ ] ) ;
72
-
73
72
const handleOnChange = ( value ) => {
74
73
if ( value === searchKeyword ) { return ; }
75
74
if ( ! value ) { clearSearch ( ) ; }
76
75
} ;
77
76
78
- const handleSearchCloseClick = ( ) => {
77
+ const close = ( ) => {
79
78
clearSearch ( ) ;
80
79
dispatch ( setShowSearch ( false ) ) ;
81
80
} ;
82
81
82
+ const handlePopState = ( ) => close ( ) ;
83
+
84
+ const handleBackdropClick = function ( event ) {
85
+ if ( event . target === dialogRef . current ) {
86
+ dialogRef . current . close ( ) ;
87
+ }
88
+ } ;
89
+
90
+ useEffect ( ( ) => {
91
+ // We need this to keep the dialog reference when unmounting.
92
+ const dialog = dialogRef . current ;
93
+
94
+ // Open the dialog as a modal on render to confine focus within it.
95
+ dialogRef . current . showModal ( ) ;
96
+
97
+ if ( searchKeyword ) {
98
+ handleSubmit ( searchKeyword ) ; // In case it's opened with a search link, we run the search.
99
+ }
100
+
101
+ const controller = new AbortController ( ) ;
102
+ const { signal } = controller ;
103
+
104
+ window . addEventListener ( 'popstate' , handlePopState , { signal } ) ;
105
+ dialog . addEventListener ( 'click' , handleBackdropClick , { signal } ) ;
106
+
107
+ return ( ) => controller . abort ( ) ; // Removes event listeners.
108
+ } , [ ] ) ;
109
+
110
+ const handleSearchClose = ( ) => close ( ) ;
111
+
83
112
let status = 'idle' ;
84
113
if ( loading ) {
85
114
status = 'loading' ;
@@ -90,34 +119,37 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
90
119
}
91
120
92
121
return (
93
- < section className = "courseware-search" style = { { '--modal-top-position' : top } } data-testid = "courseware-search-section" { ...sectionProps } >
94
- < div className = "courseware-search__close" >
95
- < Button
96
- variant = "tertiary"
97
- className = "p-1"
98
- aria-label = { intl . formatMessage ( messages . searchCloseAction ) }
99
- onClick = { handleSearchCloseClick }
100
- data-testid = "courseware-search-close-button"
101
- > < Icon src = { Close } />
102
- </ Button >
103
- </ div >
122
+ < dialog ref = { dialogRef } className = "courseware-search" style = { { '--modal-top-position' : top } } data-testid = "courseware-search-dialog" onClose = { handleSearchClose } { ...sectionProps } >
104
123
< div className = "courseware-search__outer-content" >
105
- < div className = "courseware-search__content" >
106
- < h1 className = "h2" > { intl . formatMessage ( messages . searchModuleTitle ) } </ h1 >
107
- < CoursewareSearchForm
108
- searchTerm = { searchKeyword }
109
- onSubmit = { handleSubmit }
110
- onChange = { handleOnChange }
111
- placeholder = { intl . formatMessage ( messages . searchBarPlaceholderText ) }
112
- />
124
+ < div className = "courseware-search__content" data-testid = "courseware-search-content" >
125
+ < div className = "courseware-search__form" >
126
+ < h1 className = "h2" > { formatMessage ( messages . searchModuleTitle ) } </ h1 >
127
+ < CoursewareSearchForm
128
+ searchTerm = { searchKeyword }
129
+ onSubmit = { handleSubmit }
130
+ onChange = { handleOnChange }
131
+ placeholder = { formatMessage ( messages . searchBarPlaceholderText ) }
132
+ />
133
+ < div className = "courseware-search__close" >
134
+ < Button
135
+ variant = "tertiary"
136
+ className = "p-1"
137
+ aria-label = { formatMessage ( messages . searchCloseAction ) }
138
+ onClick = { ( ) => dialogRef . current . close ( ) }
139
+ data-testid = "courseware-search-close-button"
140
+ > < Icon src = { Close } />
141
+ </ Button >
142
+ </ div >
143
+ </ div >
144
+
113
145
{ status === 'loading' ? (
114
146
< div className = "courseware-search__spinner" data-testid = "courseware-search-spinner" >
115
- < Spinner animation = "border" variant = "light" screenReaderText = { intl . formatMessage ( messages . loading ) } />
147
+ < Spinner animation = "border" variant = "light" screenReaderText = { formatMessage ( messages . loading ) } />
116
148
</ div >
117
149
) : null }
118
150
{ status === 'error' && (
119
151
< Alert className = "mt-4" variant = "danger" data-testid = "courseware-search-error" >
120
- { intl . formatMessage ( messages . searchResultsError ) }
152
+ { formatMessage ( messages . searchResultsError ) }
121
153
</ Alert >
122
154
) }
123
155
{ status === 'results' ? (
@@ -129,20 +161,16 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
129
161
aria-relevant = "all"
130
162
aria-atomic = "true"
131
163
data-testid = "courseware-search-summary"
132
- > { intl . formatMessage ( messages . searchResultsLabel , { total, keyword : lastSearchKeyword } ) }
164
+ > { formatMessage ( messages . searchResultsLabel , { total, keyword : lastSearchKeyword } ) }
133
165
</ div >
134
166
) : null }
135
167
< CoursewareSearchResultsFilterContainer />
136
168
</ >
137
169
) : null }
138
170
</ div >
139
171
</ div >
140
- </ section >
172
+ </ dialog >
141
173
) ;
142
174
} ;
143
175
144
- CoursewareSearch . propTypes = {
145
- intl : intlShape . isRequired ,
146
- } ;
147
-
148
- export default injectIntl ( CoursewareSearch ) ;
176
+ export default CoursewareSearch ;
0 commit comments