Skip to content

Commit 866abc0

Browse files
authored
Revert "ActionMenu: Remove focus trap (#1984)" (#2023)
This reverts commit e219598.
1 parent 080c502 commit 866abc0

File tree

9 files changed

+23
-266
lines changed

9 files changed

+23
-266
lines changed

.changeset/actionmenu-remove-focus-trap.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/ActionMenu.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,17 @@ import {useSSRSafeId} from '@react-aria/ssr'
33
import {TriangleDownIcon} from '@primer/octicons-react'
44
import {AnchoredOverlay, AnchoredOverlayProps} from './AnchoredOverlay'
55
import {OverlayProps} from './Overlay'
6-
import {useProvidedRefOrCreate, useProvidedStateOrCreate, useMenuKeyboardNavigation} from './hooks'
6+
import {useProvidedRefOrCreate, useProvidedStateOrCreate, useMenuInitialFocus, useTypeaheadFocus} from './hooks'
77
import {Divider} from './ActionList/Divider'
88
import {ActionListContainerContext} from './ActionList/ActionListContainerContext'
99
import {Button, ButtonProps} from './Button'
1010
import {MandateProps} from './utils/types'
1111
import {SxProp, merge} from './sx'
1212

13-
export type MenuContextProps = Pick<
13+
type MenuContextProps = Pick<
1414
AnchoredOverlayProps,
15-
'anchorRef' | 'renderAnchor' | 'open' | 'onOpen' | 'anchorId'
16-
> & {
17-
onClose?: (gesture: 'anchor-click' | 'click-outside' | 'escape' | 'tab') => void
18-
}
15+
'anchorRef' | 'renderAnchor' | 'open' | 'onOpen' | 'onClose' | 'anchorId'
16+
>
1917
const MenuContext = React.createContext<MenuContextProps>({renderAnchor: null, open: false})
2018

2119
export type ActionMenuProps = {
@@ -113,7 +111,8 @@ const Overlay: React.FC<MenuOverlayProps> = ({children, align = 'start', ...over
113111
>
114112

115113
const containerRef = React.createRef<HTMLDivElement>()
116-
const {openWithFocus} = useMenuKeyboardNavigation(open, onOpen, onClose, containerRef, anchorRef)
114+
const {openWithFocus} = useMenuInitialFocus(open, onOpen, containerRef)
115+
useTypeaheadFocus(open, containerRef)
117116

118117
return (
119118
<AnchoredOverlay
@@ -126,7 +125,6 @@ const Overlay: React.FC<MenuOverlayProps> = ({children, align = 'start', ...over
126125
align={align}
127126
overlayProps={overlayProps}
128127
focusZoneSettings={{focusOutBehavior: 'wrap'}}
129-
focusTrapSettings={{disabled: true}}
130128
>
131129
<div ref={containerRef}>
132130
<ActionListContainerContext.Provider

src/__tests__/ActionMenu.test.tsx

Lines changed: 3 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {cleanup, render as HTMLRender, waitFor, fireEvent} from '@testing-library/react'
2-
import userEvent from '@testing-library/user-event'
32
import 'babel-polyfill'
43
import {axe, toHaveNoViolations} from 'jest-axe'
54
import React from 'react'
@@ -59,7 +58,9 @@ describe('ActionMenu', () => {
5958
const component = HTMLRender(<Example />)
6059
const button = component.getByText('Toggle Menu')
6160

62-
fireEvent.keyDown(button, {key: 'Enter'})
61+
// We pass keycode here to navigate a implementation detail in react-testing-library
62+
// https://github.com/testing-library/react-testing-library/issues/269#issuecomment-455854112
63+
fireEvent.keyDown(button, {key: 'Enter', charCode: 13})
6364
expect(component.getByRole('menu')).toBeInTheDocument()
6465
cleanup()
6566
})
@@ -82,9 +83,6 @@ describe('ActionMenu', () => {
8283

8384
fireEvent.click(button)
8485
const menuItems = await waitFor(() => component.getAllByRole('menuitem'))
85-
86-
// We pass keycode here to navigate a implementation detail in react-testing-library
87-
// https://github.com/testing-library/react-testing-library/issues/269#issuecomment-455854112
8886
fireEvent.keyPress(menuItems[0], {key: 'Enter', charCode: 13})
8987
expect(component.queryByRole('menu')).toBeNull()
9088

@@ -138,86 +136,6 @@ describe('ActionMenu', () => {
138136
cleanup()
139137
})
140138

141-
it('should keep focus on Button when menu is opened with click', async () => {
142-
const component = HTMLRender(<Example />)
143-
const button = component.getByRole('button')
144-
145-
userEvent.tab() // tab into the story, this should focus on the first button
146-
expect(button).toEqual(document.activeElement) // trust, but verify
147-
148-
fireEvent.click(button)
149-
expect(component.queryByRole('menu')).toBeInTheDocument()
150-
151-
/** We use waitFor because the hook uses an effect with setTimeout
152-
* and we need to wait for that to happen in the next tick
153-
*/
154-
await waitFor(() => expect(document.activeElement).toEqual(button))
155-
156-
cleanup()
157-
})
158-
159-
it('should select first element when ArrowDown is pressed after opening Menu with click', async () => {
160-
const component = HTMLRender(<Example />)
161-
162-
const button = component.getByText('Toggle Menu')
163-
button.focus() // browsers do this automatically on click, but tests don't
164-
fireEvent.click(button)
165-
expect(component.queryByRole('menu')).toBeInTheDocument()
166-
167-
// button should be the active element
168-
fireEvent.keyDown(document.activeElement!, {key: 'ArrowDown', code: 'ArrowDown'})
169-
170-
await waitFor(() => {
171-
expect(component.getAllByRole('menuitem')[0]).toEqual(document.activeElement)
172-
})
173-
174-
cleanup()
175-
})
176-
177-
it('should select last element when ArrowUp is pressed after opening Menu with click', async () => {
178-
const component = HTMLRender(<Example />)
179-
180-
const button = component.getByText('Toggle Menu')
181-
button.focus() // browsers do this automatically on click, but tests don't
182-
fireEvent.click(button)
183-
expect(component.queryByRole('menu')).toBeInTheDocument()
184-
185-
// button should be the active element
186-
fireEvent.keyDown(document.activeElement!, {key: 'ArrowUp', code: 'ArrowUp'})
187-
188-
await waitFor(() => {
189-
expect(component.getAllByRole('menuitem').pop()).toEqual(document.activeElement)
190-
})
191-
192-
cleanup()
193-
})
194-
195-
it('should close the menu if Tab is pressed and move to next element', async () => {
196-
const component = HTMLRender(
197-
<>
198-
<Example />
199-
<input type="text" placeholder="next focusable element" />
200-
</>
201-
)
202-
const anchor = component.getByRole('button')
203-
204-
userEvent.tab() // tab into the story, this should focus on the first button
205-
expect(anchor).toEqual(document.activeElement) // trust, but verify
206-
207-
fireEvent.keyDown(anchor, {key: 'Enter'})
208-
expect(component.queryByRole('menu')).toBeInTheDocument()
209-
210-
expect(component.getAllByRole('menuitem')[0]).toEqual(document.activeElement)
211-
212-
await waitFor(() => {
213-
userEvent.tab()
214-
expect(document.activeElement).toEqual(component.getByPlaceholderText('next focusable element'))
215-
expect(component.queryByRole('menu')).not.toBeInTheDocument()
216-
})
217-
218-
cleanup()
219-
})
220-
221139
it('should have no axe violations', async () => {
222140
const {container} = HTMLRender(<Example />)
223141
const results = await axe(container)

src/__tests__/hooks/useMenuInitialFocus.test.tsx

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,11 @@ const Component = () => {
77
const onOpen = () => setOpen(!open)
88

99
const containerRef = React.createRef<HTMLDivElement>()
10-
const anchorRef = React.createRef<HTMLButtonElement>()
11-
const {openWithFocus} = useMenuInitialFocus(open, onOpen, containerRef, anchorRef)
10+
const {openWithFocus} = useMenuInitialFocus(open, onOpen, containerRef)
1211

1312
return (
1413
<>
15-
<button
16-
ref={anchorRef}
17-
onClick={() => openWithFocus('anchor-click')}
18-
onKeyDown={event => openWithFocus('anchor-key-press', event)}
19-
>
14+
<button onClick={() => setOpen(true)} onKeyDown={event => openWithFocus('anchor-key-press', event)}>
2015
open container
2116
</button>
2217
{open && (
@@ -88,17 +83,4 @@ describe('useMenuInitialFocus', () => {
8883
expect(document.body).toEqual(document.activeElement)
8984
})
9085
})
91-
92-
it('should keep focus on trigger when opened with click', async () => {
93-
const {getByText} = render(<Component />)
94-
95-
const button = getByText('open container')
96-
button.focus() // browsers do this automatically on click, but tests don't
97-
expect(button).toEqual(document.activeElement)
98-
fireEvent.click(button)
99-
100-
await waitFor(() => {
101-
expect(button).toEqual(document.activeElement)
102-
})
103-
})
10486
})

src/hooks/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,3 @@ export {useRenderForcingRef} from './useRenderForcingRef'
1212
export {useProvidedStateOrCreate} from './useProvidedStateOrCreate'
1313
export {useMenuInitialFocus} from './useMenuInitialFocus'
1414
export {useTypeaheadFocus} from './useTypeaheadFocus'
15-
export {useMenuKeyboardNavigation} from './useMenuKeyboardNavigation'

src/hooks/useMenuInitialFocus.ts

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,18 @@ import {useProvidedRefOrCreate} from './useProvidedRefOrCreate'
55
type Gesture = 'anchor-click' | 'anchor-key-press'
66
type Callback = (gesture: Gesture, event?: React.KeyboardEvent<HTMLElement>) => unknown
77

8-
export const useMenuInitialFocus = (
9-
open: boolean,
10-
onOpen?: Callback,
11-
providedContainerRef?: React.RefObject<HTMLElement>,
12-
providedAnchorRef?: React.RefObject<HTMLElement>
13-
) => {
14-
const containerRef = useProvidedRefOrCreate(providedContainerRef)
15-
const anchorRef = useProvidedRefOrCreate(providedAnchorRef)
8+
export const useMenuInitialFocus = (open: boolean, onOpen?: Callback, providedRef?: React.RefObject<HTMLElement>) => {
9+
const containerRef = useProvidedRefOrCreate(providedRef)
1610
const [openingKey, setOpeningKey] = React.useState<string | undefined>(undefined)
1711

1812
const openWithFocus: Callback = (gesture, event) => {
19-
if (gesture === 'anchor-click') setOpeningKey('mouse-click')
2013
if (gesture === 'anchor-key-press' && event) setOpeningKey(event.code)
14+
else setOpeningKey(undefined)
2115
if (typeof onOpen === 'function') onOpen(gesture, event)
2216
}
2317

2418
/**
2519
* Pick the first element to focus based on the key used to open the Menu
26-
* Click: anchor
2720
* ArrowDown | Space | Enter: first element
2821
* ArrowUp: last element
2922
*/
@@ -32,11 +25,7 @@ export const useMenuInitialFocus = (
3225
if (!openingKey || !containerRef.current) return
3326

3427
const iterable = iterateFocusableElements(containerRef.current)
35-
36-
if (openingKey === 'mouse-click') {
37-
if (anchorRef.current) anchorRef.current.focus()
38-
else throw new Error('For focus management, please attach anchorRef')
39-
} else if (['ArrowDown', 'Space', 'Enter'].includes(openingKey)) {
28+
if (['ArrowDown', 'Space', 'Enter'].includes(openingKey)) {
4029
const firstElement = iterable.next().value
4130
/** We push imperative focus to the next tick to prevent React's batching */
4231
setTimeout(() => firstElement?.focus())
@@ -45,7 +34,7 @@ export const useMenuInitialFocus = (
4534
const lastElement = elements[elements.length - 1]
4635
setTimeout(() => lastElement.focus())
4736
}
48-
}, [open, openingKey, containerRef, anchorRef])
37+
}, [open, openingKey, containerRef])
4938

50-
return {containerRef, anchorRef, openWithFocus}
39+
return {containerRef, openWithFocus}
5140
}

src/hooks/useMenuKeyboardNavigation.ts

Lines changed: 0 additions & 88 deletions
This file was deleted.

src/hooks/useTypeaheadFocus.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,12 @@ import {useProvidedRefOrCreate} from './useProvidedRefOrCreate'
44

55
export const TYPEAHEAD_TIMEOUT = 1000
66

7-
export const useTypeaheadFocus = (
8-
open: boolean,
9-
providedContainerRef: React.RefObject<HTMLElement>,
10-
providedAnchorRef?: React.RefObject<HTMLElement>
11-
) => {
12-
const containerRef = useProvidedRefOrCreate(providedContainerRef)
13-
const anchorRef = useProvidedRefOrCreate(providedAnchorRef)
7+
export const useTypeaheadFocus = (open: boolean, providedRef?: React.RefObject<HTMLElement>) => {
8+
const containerRef = useProvidedRefOrCreate(providedRef)
149

1510
React.useEffect(() => {
11+
if (!open || !containerRef.current) return
1612
const container = containerRef.current
17-
const anchor = anchorRef.current
18-
19-
// anchor is optional, but container isn't
20-
if (!open || !container) return
2113

2214
let query = ''
2315
let timeout: number | undefined
@@ -91,16 +83,12 @@ export const useTypeaheadFocus = (
9183
}
9284

9385
container.addEventListener('keydown', handler)
94-
anchor?.addEventListener('keydown', handler)
95-
return () => {
96-
container.removeEventListener('keydown', handler)
97-
anchor?.removeEventListener('keydown', handler)
98-
}
99-
}, [open, containerRef, anchorRef])
86+
return () => container.removeEventListener('keydown', handler)
87+
}, [open, containerRef])
10088

10189
const isAlphabetKey = (event: KeyboardEvent) => {
10290
return event.key.length === 1 && /[a-z\d]/i.test(event.key)
10391
}
10492

105-
return {containerRef, anchorRef}
93+
return {containerRef}
10694
}

0 commit comments

Comments
 (0)