-
Notifications
You must be signed in to change notification settings - Fork 71
feat(drawer): add focus management to embedded drawers #3139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ce621ea
a76826e
58892b1
84dcd65
3192ddf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
'@leafygreen-ui/drawer': minor | ||
--- | ||
|
||
- Adds focus management to embedded drawers. Embedded drawers will now automatically focus the first focusable element when opened and restore focus to the previously focused element when closed. | ||
- Fixes bug that prevented the toolbar from being focused when the drawer is opened. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,13 @@ | ||
import React, { forwardRef } from 'react'; | ||
import React, { forwardRef, useRef } from 'react'; | ||
|
||
import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; | ||
import { useIsomorphicLayoutEffect } from '@leafygreen-ui/hooks'; | ||
|
||
import { useDrawerLayoutContext } from '../../DrawerLayout'; | ||
|
||
import { getPanelGridStyles } from './PanelGrid.styles'; | ||
import { PanelGridProps } from './PanelGrid.types'; | ||
import { useForwardedRef } from '@leafygreen-ui/hooks'; | ||
|
||
/** | ||
* @internal | ||
|
@@ -31,9 +33,45 @@ | |
isDrawerOpen, | ||
} = useDrawerLayoutContext(); | ||
|
||
const layoutRef = useForwardedRef(forwardedRef, null); | ||
const previouslyFocusedRef = useRef<HTMLElement | null>(null); | ||
const hasHandledFocusRef = useRef<boolean>(false); | ||
|
||
/** | ||
* Focuses the first focusable element in the drawer when the drawer is opened. | ||
* Also handles restoring focus when the drawer is closed. | ||
*/ | ||
useIsomorphicLayoutEffect(() => { | ||
if (isDrawerOpen && !hasHandledFocusRef.current) { | ||
// Store the currently focused element when opening (only once per open session) | ||
previouslyFocusedRef.current = document.activeElement as HTMLElement; | ||
hasHandledFocusRef.current = true; | ||
|
||
// Focus the first focusable element in the drawer | ||
const firstFocusable = layoutRef.current?.querySelector( | ||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', | ||
) as HTMLElement; | ||
Comment on lines
+51
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The CSS selector for focusable elements should be extracted to a constant or utility function to avoid duplication and improve maintainability. This same selector appears to be used elsewhere in the codebase. Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||
|
||
firstFocusable?.focus(); | ||
} else if (!isDrawerOpen && hasHandledFocusRef.current) { | ||
// Restore focus when closing (only if we had handled focus during this session) | ||
if (previouslyFocusedRef.current) { | ||
// Check if the previously focused element is still in the DOM | ||
if (document.contains(previouslyFocusedRef.current)) { | ||
previouslyFocusedRef.current.focus(); | ||
} else { | ||
// If the previously focused element is no longer in the DOM, focus the body | ||
document.body.focus(); | ||
} | ||
Comment on lines
+60
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Focusing Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||
previouslyFocusedRef.current = null; // Clear the ref | ||
} | ||
hasHandledFocusRef.current = false; // Reset for next open session | ||
} | ||
}, [isDrawerOpen]); | ||
|
||
return ( | ||
<div | ||
ref={forwardedRef} | ||
ref={layoutRef} | ||
className={getPanelGridStyles({ | ||
className, | ||
isDrawerOpen, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import statements should be grouped and ordered consistently. The
useForwardedRef
import on line 10 should be combined with the other hooks import on line 4.Copilot uses AI. Check for mistakes.