-
Notifications
You must be signed in to change notification settings - Fork 646
[Bug] Fix useFocusTrap scrolling issue #7242
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
Changes from 10 commits
64cd089
d846dcd
7615e85
8f0174b
79d95f4
950d57e
d2a13a9
c57b958
6aef135
953e02e
22b2fb4
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,5 @@ | ||
| --- | ||
| "@primer/react": patch | ||
| --- | ||
|
|
||
| useFocusTrap - Fix bug related to restoring focus on scrolling | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||
| import React from 'react' | ||||||||
| import {focusTrap} from '@primer/behaviors' | ||||||||
| import {useProvidedRefOrCreate} from './useProvidedRefOrCreate' | ||||||||
| import {useOnOutsideClick} from './useOnOutsideClick' | ||||||||
|
|
||||||||
| export interface FocusTrapHookSettings { | ||||||||
| /** | ||||||||
|
|
@@ -34,6 +35,12 @@ export interface FocusTrapHookSettings { | |||||||
| * Overrides restoreFocusOnCleanUp | ||||||||
| */ | ||||||||
| returnFocusRef?: React.RefObject<HTMLElement | null> | ||||||||
| /** | ||||||||
| * If true, it should allow focus to escape the trap when clicking outside of the trap container and mark it as disabled. | ||||||||
| * | ||||||||
| * Overrides restoreFocusOnCleanUp and returnFocusRef | ||||||||
| */ | ||||||||
| allowOutsideClick?: boolean | ||||||||
|
Comment on lines
+38
to
+43
|
||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
|
|
@@ -45,6 +52,7 @@ export function useFocusTrap( | |||||||
| settings?: FocusTrapHookSettings, | ||||||||
| dependencies: React.DependencyList = [], | ||||||||
| ): {containerRef: React.RefObject<HTMLElement | null>; initialFocusRef: React.RefObject<HTMLElement | null>} { | ||||||||
| const [outsideClicked, setOutsideClicked] = React.useState(false) | ||||||||
|
||||||||
| const containerRef = useProvidedRefOrCreate(settings?.containerRef) | ||||||||
| const initialFocusRef = useProvidedRefOrCreate(settings?.initialFocusRef) | ||||||||
| const disabled = settings?.disabled | ||||||||
|
|
@@ -53,14 +61,17 @@ export function useFocusTrap( | |||||||
|
|
||||||||
| // If we are enabling a focus trap and haven't already stored the previously focused element | ||||||||
| // go ahead an do that so we can restore later when the trap is disabled. | ||||||||
| if (!previousFocusedElement.current && !settings?.disabled) { | ||||||||
| if (!previousFocusedElement.current && !disabled) { | ||||||||
| previousFocusedElement.current = document.activeElement | ||||||||
| } | ||||||||
|
|
||||||||
| // This function removes the event listeners that enable the focus trap and restores focus | ||||||||
| // to the previously-focused element (if necessary). | ||||||||
| function disableTrap() { | ||||||||
| abortController.current?.abort() | ||||||||
| if (settings?.allowOutsideClick && outsideClicked) { | ||||||||
| return | ||||||||
| } | ||||||||
| if (settings?.returnFocusRef && settings.returnFocusRef.current instanceof HTMLElement) { | ||||||||
| settings.returnFocusRef.current.focus() | ||||||||
| } else if (settings?.restoreFocusOnCleanUp && previousFocusedElement.current instanceof HTMLElement) { | ||||||||
|
|
@@ -85,6 +96,17 @@ export function useFocusTrap( | |||||||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||||||||
| [containerRef, initialFocusRef, disabled, ...dependencies], | ||||||||
| ) | ||||||||
| useOnOutsideClick({ | ||||||||
| containerRef: containerRef as React.RefObject<HTMLDivElement>, | ||||||||
| onClickOutside: () => { | ||||||||
| setOutsideClicked(true) | ||||||||
| if (settings?.allowOutsideClick) { | ||||||||
| if (settings.returnFocusRef) settings.returnFocusRef = undefined | ||||||||
| settings.restoreFocusOnCleanUp = false | ||||||||
|
Comment on lines
+104
to
+105
|
||||||||
| abortController.current?.abort() | ||||||||
| } | ||||||||
| }, | ||||||||
|
||||||||
| }, | |
| }, | |
| disabled: !settings?.allowOutsideClick, |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ import type {Meta} from '@storybook/react-vite' | |||||
|
|
||||||
| import {Button, Flash, Stack, Text} from '..' | ||||||
| import {useFocusTrap} from '../hooks/useFocusTrap' | ||||||
| import {useOnEscapePress} from '../hooks/useOnEscapePress' | ||||||
| import classes from './FocusTrap.stories.module.css' | ||||||
|
|
||||||
| export default { | ||||||
|
|
@@ -118,6 +119,95 @@ export const RestoreFocus = () => { | |||||
| ) | ||||||
| } | ||||||
|
|
||||||
| export const RestoreFocusMinimal = () => { | ||||||
| const [enabled, setEnabled] = React.useState(false) | ||||||
| const toggleButtonRef = React.useRef<HTMLButtonElement>(null) | ||||||
| const {containerRef} = useFocusTrap({ | ||||||
| disabled: !enabled, | ||||||
| restoreFocusOnCleanUp: true, | ||||||
| returnFocusRef: toggleButtonRef, | ||||||
| allowOutsideClick: true, | ||||||
| }) | ||||||
|
|
||||||
| useOnEscapePress( | ||||||
| React.useCallback( | ||||||
| e => { | ||||||
| if (!enabled) return | ||||||
| e.preventDefault() | ||||||
| setEnabled(false) | ||||||
| }, | ||||||
| [enabled, setEnabled], | ||||||
| ), | ||||||
| [enabled, setEnabled], | ||||||
| ) | ||||||
|
|
||||||
| return ( | ||||||
| <> | ||||||
| <HelperGlobalStyling /> | ||||||
| <Stack direction="vertical" gap="normal"> | ||||||
| <Flash style={{marginBottom: 'var(--base-size-12)'}}> | ||||||
| Minimal focus trap example. Click to toggle focus trap to toggle. While enabled, focus stays inside the green | ||||||
|
||||||
| Minimal focus trap example. Click to toggle focus trap to toggle. While enabled, focus stays inside the green | |
| Minimal focus trap example. Click the button to toggle the focus trap. While enabled, focus stays inside the green |
Uh oh!
There was an error while loading. Please reload this page.