1
1
import React , { useState , useRef , useCallback } from 'react'
2
2
import type { Meta } from '@storybook/react'
3
- import { TriangleDownIcon , PlusIcon , IssueDraftIcon } from '@primer/octicons-react'
3
+ import { TriangleDownIcon , PlusIcon , IssueDraftIcon , XIcon } from '@primer/octicons-react'
4
4
import {
5
5
Overlay ,
6
6
ButtonGroup ,
@@ -16,15 +16,18 @@ import {
16
16
Label ,
17
17
ActionList ,
18
18
ActionMenu ,
19
+ useFocusTrap ,
19
20
} from '..'
20
21
import type { AnchorSide } from '@primer/behaviors'
22
+ import type { AriaRole } from '../utils/types'
21
23
import { Tooltip } from '../TooltipV2'
22
24
23
25
export default {
24
26
title : 'Private/Components/Overlay/Features' ,
25
27
component : Overlay ,
26
28
args : {
27
29
anchorSide : 'inside-top' ,
30
+ role : 'dialog' ,
28
31
} ,
29
32
argTypes : {
30
33
anchorSide : {
@@ -43,16 +46,22 @@ export default {
43
46
'outside-right' ,
44
47
] ,
45
48
} ,
49
+ role : {
50
+ type : 'string' ,
51
+ } ,
46
52
} ,
47
53
} as Meta
48
54
49
55
interface OverlayProps {
50
- anchorSide : AnchorSide
56
+ anchorSide ?: AnchorSide
57
+ role ?: AriaRole
58
+ right ?: boolean
51
59
}
52
60
53
61
export const DropdownOverlay = ( { anchorSide} : OverlayProps ) => {
54
62
const [ isOpen , setIsOpen ] = useState ( false )
55
63
const buttonRef = useRef < HTMLButtonElement > ( null )
64
+
56
65
return (
57
66
< >
58
67
< Button ref = { buttonRef } sx = { { position : 'relative' } } onClick = { ( ) => setIsOpen ( ! isOpen ) } >
@@ -67,8 +76,9 @@ export const DropdownOverlay = ({anchorSide}: OverlayProps) => {
67
76
onEscape = { ( ) => setIsOpen ( false ) }
68
77
onClickOutside = { ( ) => setIsOpen ( false ) }
69
78
anchorSide = { anchorSide }
79
+ role = "none"
70
80
>
71
- < ActionList >
81
+ < ActionList role = "menu" >
72
82
< ActionList . Item > Copy link</ ActionList . Item >
73
83
< ActionList . Item > Quote reply</ ActionList . Item >
74
84
< ActionList . Item > Reference in new issue</ ActionList . Item >
@@ -82,12 +92,15 @@ export const DropdownOverlay = ({anchorSide}: OverlayProps) => {
82
92
)
83
93
}
84
94
85
- export const DialogOverlay = ( { anchorSide} : OverlayProps ) => {
95
+ export const DialogOverlay = ( { anchorSide, role } : OverlayProps ) => {
86
96
const [ isOpen , setIsOpen ] = useState ( false )
87
97
const buttonRef = useRef < HTMLButtonElement > ( null )
98
+ const containerRef = useRef < HTMLDivElement > ( null )
88
99
const confirmButtonRef = useRef < HTMLButtonElement > ( null )
89
100
const anchorRef = useRef < HTMLDivElement > ( null )
90
101
const closeOverlay = ( ) => setIsOpen ( false )
102
+ useFocusTrap ( { containerRef, disabled : ! isOpen , initialFocusRef : confirmButtonRef , returnFocusRef : buttonRef } )
103
+
91
104
return (
92
105
< Box ref = { anchorRef } >
93
106
< Button ref = { buttonRef } onClick = { ( ) => setIsOpen ( ! isOpen ) } >
@@ -102,6 +115,9 @@ export const DialogOverlay = ({anchorSide}: OverlayProps) => {
102
115
onClickOutside = { closeOverlay }
103
116
width = "small"
104
117
anchorSide = { anchorSide }
118
+ role = { role }
119
+ aria-modal = { role === 'dialog' ? 'true' : undefined }
120
+ ref = { containerRef }
105
121
>
106
122
< Box display = "flex" flexDirection = "column" p = { 2 } >
107
123
< Text > Are you sure?</ Text >
@@ -118,7 +134,7 @@ export const DialogOverlay = ({anchorSide}: OverlayProps) => {
118
134
)
119
135
}
120
136
121
- export const OverlayOnTopOfOverlay = ( { anchorSide} : OverlayProps ) => {
137
+ export const OverlayOnTopOfOverlay = ( { anchorSide, role } : OverlayProps ) => {
122
138
const [ isOpen , setIsOpen ] = useState ( false )
123
139
const [ isSecondaryOpen , setIsSecondaryOpen ] = useState ( false )
124
140
const buttonRef = useRef < HTMLButtonElement > ( null )
@@ -130,6 +146,14 @@ export const OverlayOnTopOfOverlay = ({anchorSide}: OverlayProps) => {
130
146
const items = [ '🔵 Cyan' , '🔴 Magenta' , '🟡 Yellow' ]
131
147
const [ selectedItem , setSelectedItem ] = React . useState ( items [ 0 ] )
132
148
149
+ const primaryContainer = useRef < HTMLDivElement > ( null )
150
+ const secondaryContainer = useRef < HTMLDivElement > ( null )
151
+
152
+ useFocusTrap ( {
153
+ containerRef : ! isSecondaryOpen ? primaryContainer : secondaryContainer ,
154
+ disabled : ! isOpen ,
155
+ } )
156
+
133
157
return (
134
158
< Box position = "absolute" top = { 0 } left = { 0 } bottom = { 0 } right = { 0 } ref = { anchorRef } >
135
159
< input placeholder = "Input for focus testing" />
@@ -145,6 +169,9 @@ export const OverlayOnTopOfOverlay = ({anchorSide}: OverlayProps) => {
145
169
onClickOutside = { closeOverlay }
146
170
width = "small"
147
171
anchorSide = { anchorSide }
172
+ role = { role }
173
+ aria-modal = { role === 'dialog' ? 'true' : undefined }
174
+ ref = { primaryContainer }
148
175
>
149
176
< Button ref = { secondaryButtonRef } onClick = { ( ) => setIsSecondaryOpen ( ! isSecondaryOpen ) } >
150
177
open overlay
@@ -158,6 +185,9 @@ export const OverlayOnTopOfOverlay = ({anchorSide}: OverlayProps) => {
158
185
width = "small"
159
186
sx = { { top : '40px' } }
160
187
anchorSide = { anchorSide }
188
+ role = { role }
189
+ aria-modal = { role === 'dialog' ? 'true' : undefined }
190
+ ref = { secondaryContainer }
161
191
>
162
192
< Box display = "flex" flexDirection = "column" p = { 2 } >
163
193
< Text > Select an option!</ Text >
@@ -186,12 +216,14 @@ export const OverlayOnTopOfOverlay = ({anchorSide}: OverlayProps) => {
186
216
)
187
217
}
188
218
189
- export const MemexNestedOverlays = ( ) => {
219
+ export const MemexNestedOverlays = ( { role } : OverlayProps ) => {
190
220
const [ overlayOpen , setOverlayOpen ] = React . useState ( false )
191
221
const buttonRef = useRef < HTMLButtonElement > ( null )
222
+ const containerRef = useRef < HTMLDivElement > ( null )
192
223
193
224
const durations = [ 'days' , 'weeks' ]
194
225
const [ duration , setDuration ] = React . useState ( durations [ 0 ] )
226
+ useFocusTrap ( { containerRef, disabled : ! overlayOpen , returnFocusRef : buttonRef } )
195
227
196
228
return (
197
229
< div >
@@ -213,6 +245,9 @@ export const MemexNestedOverlays = () => {
213
245
ignoreClickRefs = { [ buttonRef ] }
214
246
top = { 60 }
215
247
left = { 16 }
248
+ role = { role }
249
+ aria-modal = { role === 'dialog' ? 'true' : undefined }
250
+ ref = { containerRef }
216
251
>
217
252
< Box as = "form" onSubmit = { ( ) => setOverlayOpen ( false ) } sx = { { display : 'flex' , flexDirection : 'column' , py : 2 } } >
218
253
< Box sx = { { paddingX : 3 , display : 'flex' , alignItems : 'center' , gap : 1 } } >
@@ -247,12 +282,19 @@ export const MemexNestedOverlays = () => {
247
282
)
248
283
}
249
284
250
- export const NestedOverlays = ( ) => {
285
+ export const NestedOverlays = ( { role } : OverlayProps ) => {
251
286
const [ listOverlayOpen , setListOverlayOpen ] = React . useState ( false )
252
287
const [ createListOverlayOpen , setCreateListOverlayOpen ] = React . useState ( false )
253
288
254
289
const buttonRef = useRef < HTMLButtonElement > ( null )
255
290
const secondaryButtonRef = useRef < HTMLButtonElement > ( null )
291
+ const primaryContainer = useRef < HTMLDivElement > ( null )
292
+ const secondaryContainer = useRef < HTMLDivElement > ( null )
293
+
294
+ useFocusTrap ( {
295
+ containerRef : ! createListOverlayOpen ? primaryContainer : secondaryContainer ,
296
+ disabled : ! listOverlayOpen ,
297
+ } )
256
298
257
299
React . useEffect ( ( ) => {
258
300
// eslint-disable-next-line no-console
@@ -285,6 +327,9 @@ export const NestedOverlays = () => {
285
327
ignoreClickRefs = { [ buttonRef ] }
286
328
top = { 100 }
287
329
left = { 16 }
330
+ ref = { primaryContainer }
331
+ role = { role }
332
+ aria-modal = { role === 'dialog' ? 'true' : undefined }
288
333
>
289
334
< Box sx = { { display : 'flex' , flexDirection : 'column' , py : 2 } } >
290
335
< Box sx = { { paddingX : 3 , paddingY : 2 } } >
@@ -324,6 +369,9 @@ export const NestedOverlays = () => {
324
369
ignoreClickRefs = { [ secondaryButtonRef ] }
325
370
top = { 120 }
326
371
left = { 64 }
372
+ role = { role }
373
+ aria-modal = { role === 'dialog' ? 'true' : undefined }
374
+ ref = { secondaryContainer }
327
375
>
328
376
< Box as = "form" sx = { { display : 'flex' , flexDirection : 'column' , p : 3 } } >
329
377
< Text color = "fg.muted" sx = { { fontSize : 1 , mb : 3 } } >
@@ -344,11 +392,12 @@ export const NestedOverlays = () => {
344
392
)
345
393
}
346
394
347
- export const MemexIssueOverlay = ( ) => {
395
+ export const MemexIssueOverlay = ( { role } : OverlayProps ) => {
348
396
const [ overlayOpen , setOverlayOpen ] = React . useState ( false )
349
397
const linkRef = useRef < HTMLAnchorElement > ( null )
350
398
const inputRef = useRef < HTMLInputElement > ( null )
351
399
const buttonRef = useRef < HTMLButtonElement > ( null )
400
+ const containerRef = useRef < HTMLDivElement > ( null )
352
401
353
402
const [ title , setTitle ] = React . useState ( 'Implement draft issue editor' )
354
403
const [ editing , setEditing ] = React . useState ( false )
@@ -358,6 +407,8 @@ export const MemexIssueOverlay = () => {
358
407
if ( editing ) inputRef . current ?. focus ( )
359
408
} , [ editing ] )
360
409
410
+ useFocusTrap ( { containerRef, disabled : ! overlayOpen , initialFocusRef : buttonRef , returnFocusRef : linkRef } )
411
+
361
412
return (
362
413
< >
363
414
< Link
@@ -389,6 +440,9 @@ export const MemexIssueOverlay = () => {
389
440
returnFocusRef = { linkRef }
390
441
top = { 0 }
391
442
left = "calc(100vw - 480px)"
443
+ role = { role }
444
+ aria-modal = { role === 'dialog' ? 'true' : undefined }
445
+ ref = { containerRef }
392
446
>
393
447
< Box sx = { { p : 4 , height : '100vh' } } >
394
448
< Box sx = { { display : 'flex' , alignItems : 'center' , gap : 1 , mb : 2 } } >
@@ -451,13 +505,21 @@ export const MemexIssueOverlay = () => {
451
505
)
452
506
}
453
507
454
- export const PositionedOverlays = ( { right} : { right ?: boolean } ) => {
508
+ export const PositionedOverlays = ( { right, role } : OverlayProps ) => {
455
509
const [ isOpen , setIsOpen ] = useState ( false )
456
510
const [ direction , setDirection ] = useState < 'left' | 'right' > ( right ? 'right' : 'left' )
457
511
const buttonRef = useRef < HTMLButtonElement > ( null )
458
512
const confirmButtonRef = useRef < HTMLButtonElement > ( null )
459
513
const anchorRef = useRef < HTMLDivElement > ( null )
460
514
const closeOverlay = ( ) => setIsOpen ( false )
515
+
516
+ const containerRef = useRef < HTMLDivElement > ( null )
517
+
518
+ useFocusTrap ( {
519
+ containerRef,
520
+ disabled : ! isOpen ,
521
+ } )
522
+
461
523
return (
462
524
< Box ref = { anchorRef } >
463
525
< Button
@@ -491,6 +553,9 @@ export const PositionedOverlays = ({right}: {right?: boolean}) => {
491
553
onClickOutside = { closeOverlay }
492
554
width = "auto"
493
555
anchorSide = "inside-right"
556
+ role = { role }
557
+ aria-modal = { role === 'dialog' ? 'true' : undefined }
558
+ ref = { containerRef }
494
559
>
495
560
< Box
496
561
sx = { {
@@ -501,6 +566,17 @@ export const PositionedOverlays = ({right}: {right?: boolean}) => {
501
566
alignItems : 'center' ,
502
567
} }
503
568
>
569
+ < IconButton
570
+ aria-label = "Close"
571
+ onClick = { closeOverlay }
572
+ icon = { XIcon }
573
+ variant = "invisible"
574
+ sx = { {
575
+ position : 'absolute' ,
576
+ left : '5px' ,
577
+ top : '5px' ,
578
+ } }
579
+ />
504
580
< Text > Look! left aligned</ Text >
505
581
</ Box >
506
582
</ Overlay >
@@ -515,6 +591,9 @@ export const PositionedOverlays = ({right}: {right?: boolean}) => {
515
591
anchorSide = { 'inside-left' }
516
592
right = { 0 }
517
593
position = "fixed"
594
+ role = { role }
595
+ aria-modal = { role === 'dialog' ? 'true' : undefined }
596
+ ref = { containerRef }
518
597
>
519
598
< Box
520
599
sx = { {
@@ -525,6 +604,17 @@ export const PositionedOverlays = ({right}: {right?: boolean}) => {
525
604
alignItems : 'center' ,
526
605
} }
527
606
>
607
+ < IconButton
608
+ aria-label = "Close"
609
+ onClick = { closeOverlay }
610
+ icon = { XIcon }
611
+ variant = "invisible"
612
+ sx = { {
613
+ position : 'absolute' ,
614
+ right : '5px' ,
615
+ top : '5px' ,
616
+ } }
617
+ />
528
618
< Text > Look! right aligned</ Text >
529
619
</ Box >
530
620
</ Overlay >
0 commit comments