1
- import { ReactNode } from "react" ;
1
+ import { ReactNode , useState , useRef , useEffect , useCallback } from "react" ;
2
2
import { breakpoints , colors , layouts , mixins } from "../theme" ;
3
3
import { AvailablePoints , StepBase , StepWithData } from "../types" ;
4
4
import styled from "styled-components" ;
@@ -194,6 +194,21 @@ const StepCardQuestion = styled.div<{ unpadded?: boolean }>`
194
194
}
195
195
` ;
196
196
197
+ export const StyledOverlay = styled . div `
198
+ display: flex;
199
+ flex-direction: column;
200
+ justify-content: center;
201
+ align-items: center;
202
+ position: absolute;
203
+ top: 50%;
204
+ left: 50%;
205
+ transform: translate(-50%, -50%);
206
+ width: 100%;
207
+ height: 100%;
208
+ background-color: #FFFFFF80;
209
+ z-index: 2;
210
+ ` ;
211
+
197
212
interface SharedProps {
198
213
questionNumber : number ;
199
214
numberOfQuestions : number ;
@@ -212,6 +227,7 @@ export interface StepCardProps extends SharedProps {
212
227
questionId ?: string ;
213
228
multipartBadge ?: ReactNode ;
214
229
isHomework : boolean ;
230
+ overlayChildren ?: React . ReactNode ;
215
231
}
216
232
217
233
const StepCard = ( {
@@ -229,35 +245,104 @@ const StepCard = ({
229
245
leftHeaderChildren,
230
246
rightHeaderChildren,
231
247
headerTitleChildren,
248
+ overlayChildren,
232
249
...otherProps } : StepCardProps ) => {
233
250
251
+ // Helps to stop focusing first child when is already focused
252
+ const [ previousFocusedElement , setPreviousFocusedElement ] = useState < HTMLElement | null > ( null ) ;
253
+ const overlayRef = useRef < HTMLDivElement > ( null ) ;
254
+ const [ showOverlay , setShowOverlay ] = useState < boolean > ( false ) ;
255
+
234
256
const formattedQuestionNumber = numberOfQuestions > 1
235
257
? `Questions ${ questionNumber } - ${ questionNumber + numberOfQuestions - 1 } `
236
258
: `Question ${ questionNumber } ` ;
237
259
260
+ const handleOverlayBlur = ( event : React . FocusEvent < HTMLDivElement > ) => {
261
+ if ( overlayRef . current && ! overlayRef . current . contains ( event . relatedTarget as Node ) ) {
262
+ setShowOverlay ( false ) ;
263
+ }
264
+ } ;
265
+
266
+ const handleOverlayFocus = useCallback ( ( event : FocusEvent ) => {
267
+ setShowOverlay ( true ) ;
268
+ const firstOverlayFocusableElement = document . getElementById ( 'overlay-element' ) ?. querySelector (
269
+ 'button, [href], input, select, textarea'
270
+ ) as HTMLElement ;
271
+
272
+ if (
273
+ ( firstOverlayFocusableElement !== previousFocusedElement ) &&
274
+ ( event . target === overlayRef . current )
275
+ ) {
276
+ setPreviousFocusedElement ( firstOverlayFocusableElement ) ;
277
+ firstOverlayFocusableElement . focus ( ) ;
278
+ }
279
+ } , [ overlayRef , previousFocusedElement ] ) ;
280
+
281
+ const hideFocusableElements = useCallback ( ( ) => {
282
+ const focusableElements = Array . from ( document . getElementById ( "step-card" ) ?. querySelectorAll (
283
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
284
+ ) || [ ] ) ;
285
+
286
+ focusableElements . forEach ( ( el ) => {
287
+ ( el as HTMLElement ) . setAttribute ( 'tabindex' , '-1' ) ;
288
+ } ) ;
289
+ } , [ ] ) ;
290
+
291
+ useEffect ( ( ) => {
292
+ const currentOverlayRef = overlayRef . current ;
293
+ if ( currentOverlayRef && overlayChildren ) {
294
+ currentOverlayRef . addEventListener ( 'focus' , handleOverlayFocus ) ;
295
+ hideFocusableElements ( ) ;
296
+ }
297
+ return ( ) => {
298
+ currentOverlayRef ?. removeEventListener ( 'focus' , handleOverlayFocus ) ;
299
+ } ;
300
+ } , [ overlayChildren , overlayRef , handleOverlayFocus , hideFocusableElements ] ) ;
301
+
238
302
return (
239
303
< OuterStepCard { ...otherProps } >
240
304
{ multipartBadge }
241
305
< InnerStepCard className = { className } >
242
- { questionNumber && isHomework && stepType === 'exercise' &&
243
- < StepCardHeader className = "step-card-header" >
244
- < div >
245
- { leftHeaderChildren }
246
- < h2 className = "question-info" >
247
- { headerTitleChildren }
248
- < span > { formattedQuestionNumber } </ span >
249
- { showTotalQuestions ? < span className = "num-questions" > / { numberOfQuestions } </ span > : null }
250
- < span className = "separator" > |</ span >
251
- < span className = "question-id" > ID: { questionId } </ span >
252
- </ h2 >
253
- </ div >
254
- { availablePoints || rightHeaderChildren ? < div >
255
- { availablePoints && < div className = "points" > { availablePoints } Points</ div > }
256
- { rightHeaderChildren }
257
- </ div > : null }
258
- </ StepCardHeader >
259
- }
260
- < StepCardQuestion unpadded = { unpadded } > { children } </ StepCardQuestion >
306
+ < div
307
+ ref = { overlayRef }
308
+ {
309
+ ...( overlayChildren
310
+ ? {
311
+ onMouseOver : ( ) => setShowOverlay ( true ) ,
312
+ onMouseLeave : ( ) => setShowOverlay ( false ) ,
313
+ onBlur : handleOverlayBlur ,
314
+ tabIndex : 0 ,
315
+ }
316
+ : { } )
317
+ }
318
+ >
319
+ { ( overlayChildren && showOverlay ) &&
320
+ < StyledOverlay id = "overlay-element" >
321
+ { overlayChildren }
322
+ </ StyledOverlay >
323
+ }
324
+ < div id = "step-card" >
325
+ { questionNumber && isHomework && stepType === 'exercise' &&
326
+ < StepCardHeader className = "step-card-header" >
327
+ < div >
328
+ { leftHeaderChildren }
329
+ < h2 className = "question-info" >
330
+ { headerTitleChildren }
331
+ < span > { formattedQuestionNumber } </ span >
332
+ { showTotalQuestions ? < span className = "num-questions" > / { numberOfQuestions } </ span > : null }
333
+ < span className = "separator" > |</ span >
334
+ < span className = "question-id" > ID: { questionId } </ span >
335
+ </ h2 >
336
+ </ div >
337
+ { availablePoints || rightHeaderChildren ? < div >
338
+ { availablePoints && < div className = "points" > { availablePoints } Points</ div > }
339
+ { rightHeaderChildren }
340
+ </ div > : null }
341
+ </ StepCardHeader >
342
+ }
343
+ < StepCardQuestion unpadded = { unpadded } > { children } </ StepCardQuestion >
344
+ </ div >
345
+ </ div >
261
346
</ InnerStepCard >
262
347
</ OuterStepCard >
263
348
)
@@ -267,9 +352,11 @@ StepCard.displayName = 'OSStepCard';
267
352
export interface TaskStepCardProps extends SharedProps {
268
353
className ?: string ;
269
354
children ?: ReactNode ;
355
+ tabIndex ?: number ;
270
356
step : StepBase | StepWithData ;
271
357
questionNumber : number ;
272
358
numberOfQuestions : number ;
359
+ overlayChildren ?: React . ReactNode ;
273
360
}
274
361
275
362
const TaskStepCard = ( {
@@ -278,6 +365,7 @@ const TaskStepCard = ({
278
365
numberOfQuestions,
279
366
children,
280
367
className,
368
+ overlayChildren,
281
369
...otherProps
282
370
} : TaskStepCardProps ) =>
283
371
( < StepCard { ...otherProps }
@@ -291,6 +379,7 @@ const TaskStepCard = ({
291
379
// availablePoints={step.available_points}
292
380
className = { cn ( `${ ( 'type' in step ? step . type : 'exercise' ) } -step` , className ) }
293
381
questionId = { step . uid }
382
+ overlayChildren = { overlayChildren }
294
383
>
295
384
{ children }
296
385
</ StepCard > ) ;
0 commit comments