Skip to content

Commit a2536bf

Browse files
authored
Overlay: Add proper roles w/ keyboard expectations to stories (#5175)
* Add proper roles w/ keyboard expectations to stories * Make prop optional
1 parent c9e68d2 commit a2536bf

File tree

1 file changed

+99
-9
lines changed

1 file changed

+99
-9
lines changed

packages/react/src/Overlay/Overlay.features.stories.tsx

Lines changed: 99 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, {useState, useRef, useCallback} from 'react'
22
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'
44
import {
55
Overlay,
66
ButtonGroup,
@@ -16,15 +16,18 @@ import {
1616
Label,
1717
ActionList,
1818
ActionMenu,
19+
useFocusTrap,
1920
} from '..'
2021
import type {AnchorSide} from '@primer/behaviors'
22+
import type {AriaRole} from '../utils/types'
2123
import {Tooltip} from '../TooltipV2'
2224

2325
export default {
2426
title: 'Private/Components/Overlay/Features',
2527
component: Overlay,
2628
args: {
2729
anchorSide: 'inside-top',
30+
role: 'dialog',
2831
},
2932
argTypes: {
3033
anchorSide: {
@@ -43,16 +46,22 @@ export default {
4346
'outside-right',
4447
],
4548
},
49+
role: {
50+
type: 'string',
51+
},
4652
},
4753
} as Meta
4854

4955
interface OverlayProps {
50-
anchorSide: AnchorSide
56+
anchorSide?: AnchorSide
57+
role?: AriaRole
58+
right?: boolean
5159
}
5260

5361
export const DropdownOverlay = ({anchorSide}: OverlayProps) => {
5462
const [isOpen, setIsOpen] = useState(false)
5563
const buttonRef = useRef<HTMLButtonElement>(null)
64+
5665
return (
5766
<>
5867
<Button ref={buttonRef} sx={{position: 'relative'}} onClick={() => setIsOpen(!isOpen)}>
@@ -67,8 +76,9 @@ export const DropdownOverlay = ({anchorSide}: OverlayProps) => {
6776
onEscape={() => setIsOpen(false)}
6877
onClickOutside={() => setIsOpen(false)}
6978
anchorSide={anchorSide}
79+
role="none"
7080
>
71-
<ActionList>
81+
<ActionList role="menu">
7282
<ActionList.Item>Copy link</ActionList.Item>
7383
<ActionList.Item>Quote reply</ActionList.Item>
7484
<ActionList.Item>Reference in new issue</ActionList.Item>
@@ -82,12 +92,15 @@ export const DropdownOverlay = ({anchorSide}: OverlayProps) => {
8292
)
8393
}
8494

85-
export const DialogOverlay = ({anchorSide}: OverlayProps) => {
95+
export const DialogOverlay = ({anchorSide, role}: OverlayProps) => {
8696
const [isOpen, setIsOpen] = useState(false)
8797
const buttonRef = useRef<HTMLButtonElement>(null)
98+
const containerRef = useRef<HTMLDivElement>(null)
8899
const confirmButtonRef = useRef<HTMLButtonElement>(null)
89100
const anchorRef = useRef<HTMLDivElement>(null)
90101
const closeOverlay = () => setIsOpen(false)
102+
useFocusTrap({containerRef, disabled: !isOpen, initialFocusRef: confirmButtonRef, returnFocusRef: buttonRef})
103+
91104
return (
92105
<Box ref={anchorRef}>
93106
<Button ref={buttonRef} onClick={() => setIsOpen(!isOpen)}>
@@ -102,6 +115,9 @@ export const DialogOverlay = ({anchorSide}: OverlayProps) => {
102115
onClickOutside={closeOverlay}
103116
width="small"
104117
anchorSide={anchorSide}
118+
role={role}
119+
aria-modal={role === 'dialog' ? 'true' : undefined}
120+
ref={containerRef}
105121
>
106122
<Box display="flex" flexDirection="column" p={2}>
107123
<Text>Are you sure?</Text>
@@ -118,7 +134,7 @@ export const DialogOverlay = ({anchorSide}: OverlayProps) => {
118134
)
119135
}
120136

121-
export const OverlayOnTopOfOverlay = ({anchorSide}: OverlayProps) => {
137+
export const OverlayOnTopOfOverlay = ({anchorSide, role}: OverlayProps) => {
122138
const [isOpen, setIsOpen] = useState(false)
123139
const [isSecondaryOpen, setIsSecondaryOpen] = useState(false)
124140
const buttonRef = useRef<HTMLButtonElement>(null)
@@ -130,6 +146,14 @@ export const OverlayOnTopOfOverlay = ({anchorSide}: OverlayProps) => {
130146
const items = ['🔵 Cyan', '🔴 Magenta', '🟡 Yellow']
131147
const [selectedItem, setSelectedItem] = React.useState(items[0])
132148

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+
133157
return (
134158
<Box position="absolute" top={0} left={0} bottom={0} right={0} ref={anchorRef}>
135159
<input placeholder="Input for focus testing" />
@@ -145,6 +169,9 @@ export const OverlayOnTopOfOverlay = ({anchorSide}: OverlayProps) => {
145169
onClickOutside={closeOverlay}
146170
width="small"
147171
anchorSide={anchorSide}
172+
role={role}
173+
aria-modal={role === 'dialog' ? 'true' : undefined}
174+
ref={primaryContainer}
148175
>
149176
<Button ref={secondaryButtonRef} onClick={() => setIsSecondaryOpen(!isSecondaryOpen)}>
150177
open overlay
@@ -158,6 +185,9 @@ export const OverlayOnTopOfOverlay = ({anchorSide}: OverlayProps) => {
158185
width="small"
159186
sx={{top: '40px'}}
160187
anchorSide={anchorSide}
188+
role={role}
189+
aria-modal={role === 'dialog' ? 'true' : undefined}
190+
ref={secondaryContainer}
161191
>
162192
<Box display="flex" flexDirection="column" p={2}>
163193
<Text>Select an option!</Text>
@@ -186,12 +216,14 @@ export const OverlayOnTopOfOverlay = ({anchorSide}: OverlayProps) => {
186216
)
187217
}
188218

189-
export const MemexNestedOverlays = () => {
219+
export const MemexNestedOverlays = ({role}: OverlayProps) => {
190220
const [overlayOpen, setOverlayOpen] = React.useState(false)
191221
const buttonRef = useRef<HTMLButtonElement>(null)
222+
const containerRef = useRef<HTMLDivElement>(null)
192223

193224
const durations = ['days', 'weeks']
194225
const [duration, setDuration] = React.useState(durations[0])
226+
useFocusTrap({containerRef, disabled: !overlayOpen, returnFocusRef: buttonRef})
195227

196228
return (
197229
<div>
@@ -213,6 +245,9 @@ export const MemexNestedOverlays = () => {
213245
ignoreClickRefs={[buttonRef]}
214246
top={60}
215247
left={16}
248+
role={role}
249+
aria-modal={role === 'dialog' ? 'true' : undefined}
250+
ref={containerRef}
216251
>
217252
<Box as="form" onSubmit={() => setOverlayOpen(false)} sx={{display: 'flex', flexDirection: 'column', py: 2}}>
218253
<Box sx={{paddingX: 3, display: 'flex', alignItems: 'center', gap: 1}}>
@@ -247,12 +282,19 @@ export const MemexNestedOverlays = () => {
247282
)
248283
}
249284

250-
export const NestedOverlays = () => {
285+
export const NestedOverlays = ({role}: OverlayProps) => {
251286
const [listOverlayOpen, setListOverlayOpen] = React.useState(false)
252287
const [createListOverlayOpen, setCreateListOverlayOpen] = React.useState(false)
253288

254289
const buttonRef = useRef<HTMLButtonElement>(null)
255290
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+
})
256298

257299
React.useEffect(() => {
258300
// eslint-disable-next-line no-console
@@ -285,6 +327,9 @@ export const NestedOverlays = () => {
285327
ignoreClickRefs={[buttonRef]}
286328
top={100}
287329
left={16}
330+
ref={primaryContainer}
331+
role={role}
332+
aria-modal={role === 'dialog' ? 'true' : undefined}
288333
>
289334
<Box sx={{display: 'flex', flexDirection: 'column', py: 2}}>
290335
<Box sx={{paddingX: 3, paddingY: 2}}>
@@ -324,6 +369,9 @@ export const NestedOverlays = () => {
324369
ignoreClickRefs={[secondaryButtonRef]}
325370
top={120}
326371
left={64}
372+
role={role}
373+
aria-modal={role === 'dialog' ? 'true' : undefined}
374+
ref={secondaryContainer}
327375
>
328376
<Box as="form" sx={{display: 'flex', flexDirection: 'column', p: 3}}>
329377
<Text color="fg.muted" sx={{fontSize: 1, mb: 3}}>
@@ -344,11 +392,12 @@ export const NestedOverlays = () => {
344392
)
345393
}
346394

347-
export const MemexIssueOverlay = () => {
395+
export const MemexIssueOverlay = ({role}: OverlayProps) => {
348396
const [overlayOpen, setOverlayOpen] = React.useState(false)
349397
const linkRef = useRef<HTMLAnchorElement>(null)
350398
const inputRef = useRef<HTMLInputElement>(null)
351399
const buttonRef = useRef<HTMLButtonElement>(null)
400+
const containerRef = useRef<HTMLDivElement>(null)
352401

353402
const [title, setTitle] = React.useState('Implement draft issue editor')
354403
const [editing, setEditing] = React.useState(false)
@@ -358,6 +407,8 @@ export const MemexIssueOverlay = () => {
358407
if (editing) inputRef.current?.focus()
359408
}, [editing])
360409

410+
useFocusTrap({containerRef, disabled: !overlayOpen, initialFocusRef: buttonRef, returnFocusRef: linkRef})
411+
361412
return (
362413
<>
363414
<Link
@@ -389,6 +440,9 @@ export const MemexIssueOverlay = () => {
389440
returnFocusRef={linkRef}
390441
top={0}
391442
left="calc(100vw - 480px)"
443+
role={role}
444+
aria-modal={role === 'dialog' ? 'true' : undefined}
445+
ref={containerRef}
392446
>
393447
<Box sx={{p: 4, height: '100vh'}}>
394448
<Box sx={{display: 'flex', alignItems: 'center', gap: 1, mb: 2}}>
@@ -451,13 +505,21 @@ export const MemexIssueOverlay = () => {
451505
)
452506
}
453507

454-
export const PositionedOverlays = ({right}: {right?: boolean}) => {
508+
export const PositionedOverlays = ({right, role}: OverlayProps) => {
455509
const [isOpen, setIsOpen] = useState(false)
456510
const [direction, setDirection] = useState<'left' | 'right'>(right ? 'right' : 'left')
457511
const buttonRef = useRef<HTMLButtonElement>(null)
458512
const confirmButtonRef = useRef<HTMLButtonElement>(null)
459513
const anchorRef = useRef<HTMLDivElement>(null)
460514
const closeOverlay = () => setIsOpen(false)
515+
516+
const containerRef = useRef<HTMLDivElement>(null)
517+
518+
useFocusTrap({
519+
containerRef,
520+
disabled: !isOpen,
521+
})
522+
461523
return (
462524
<Box ref={anchorRef}>
463525
<Button
@@ -491,6 +553,9 @@ export const PositionedOverlays = ({right}: {right?: boolean}) => {
491553
onClickOutside={closeOverlay}
492554
width="auto"
493555
anchorSide="inside-right"
556+
role={role}
557+
aria-modal={role === 'dialog' ? 'true' : undefined}
558+
ref={containerRef}
494559
>
495560
<Box
496561
sx={{
@@ -501,6 +566,17 @@ export const PositionedOverlays = ({right}: {right?: boolean}) => {
501566
alignItems: 'center',
502567
}}
503568
>
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+
/>
504580
<Text>Look! left aligned</Text>
505581
</Box>
506582
</Overlay>
@@ -515,6 +591,9 @@ export const PositionedOverlays = ({right}: {right?: boolean}) => {
515591
anchorSide={'inside-left'}
516592
right={0}
517593
position="fixed"
594+
role={role}
595+
aria-modal={role === 'dialog' ? 'true' : undefined}
596+
ref={containerRef}
518597
>
519598
<Box
520599
sx={{
@@ -525,6 +604,17 @@ export const PositionedOverlays = ({right}: {right?: boolean}) => {
525604
alignItems: 'center',
526605
}}
527606
>
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+
/>
528618
<Text>Look! right aligned</Text>
529619
</Box>
530620
</Overlay>

0 commit comments

Comments
 (0)