-
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
base: main
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: 953e02e The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. Or, apply the |
|
👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/7563 |
|
🟢 ci completed with status |
|
👋 Hi, there are new commits since the last successful integration test. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. Or, apply the |
|
I spoke with @ericwbailey and @JoyceZhu to validate that this is the right approach. |
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.
Pull request overview
This PR addresses a scrolling-related bug in the useFocusTrap hook by adding an allowOutsideClick option that prevents focus restoration when users click outside the trap container. The change allows the browser to maintain the user's current scroll position when the trap is dismissed via outside click, rather than scrolling back to restore focus to the original element.
Key Changes:
- Added
allowOutsideClickboolean option toFocusTrapHookSettingsinterface - Integrated
useOnOutsideClickhook to detect clicks outside the trap container - Modified focus restoration logic to skip restoration when exiting via outside click
- Added comprehensive Storybook example (
RestoreFocusMinimal) demonstrating the fix
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
packages/react/src/hooks/useFocusTrap.ts |
Adds allowOutsideClick option and integrates outside click detection to conditionally prevent focus restoration |
packages/react/src/stories/useFocusTrap.stories.tsx |
Adds new story demonstrating the scrolling fix with a minimal reproducible example |
.changeset/unlucky-icons-speak.md |
Documents the patch-level bug fix for release notes |
| if (settings.returnFocusRef) settings.returnFocusRef = undefined | ||
| settings.restoreFocusOnCleanUp = false |
Copilot
AI
Dec 4, 2025
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.
Mutating the settings object properties is problematic. This violates React's principles of immutability and can lead to unexpected behavior. The settings object is passed from the parent component and should not be modified directly. Additionally, setting returnFocusRef to undefined doesn't actually remove the property from the object, and mutating restoreFocusOnCleanUp will affect the original settings object in the parent component.
Instead, consider using internal state or flags to control the focus restoration behavior without mutating the incoming props.
| settings.restoreFocusOnCleanUp = false | ||
| abortController.current?.abort() | ||
| } | ||
| }, |
Copilot
AI
Dec 4, 2025
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.
The useOnOutsideClick hook is called unconditionally on every render, even when allowOutsideClick is not enabled. This adds unnecessary event listeners and performance overhead when the feature isn't being used.
Consider conditionally calling the hook only when settings?.allowOutsideClick is true, or pass a disabled flag to control the hook's behavior. However, note that hooks cannot be called conditionally, so you may need to refactor useOnOutsideClick to accept a disabled parameter or handle the conditional logic internally.
| }, | |
| }, | |
| disabled: !settings?.allowOutsideClick, |
| settings?: FocusTrapHookSettings, | ||
| dependencies: React.DependencyList = [], | ||
| ): {containerRef: React.RefObject<HTMLElement | null>; initialFocusRef: React.RefObject<HTMLElement | null>} { | ||
| const [outsideClicked, setOutsideClicked] = React.useState(false) |
Copilot
AI
Dec 4, 2025
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.
The outsideClicked state is never reset, which can cause issues when the focus trap is re-enabled. If a user clicks outside once, sets outsideClicked to true, then disables and re-enables the trap, the flag remains true. This means that subsequent cleanups will incorrectly skip focus restoration even if the trap wasn't exited via an outside click the second time.
The state should be reset when the trap is disabled or when disabled changes from false to true.
| <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 |
Copilot
AI
Dec 4, 2025
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.
Typo in text: "Click to toggle focus trap to toggle" should be "Click the button to toggle the focus trap" or similar. The phrase "to toggle" is duplicated unnecessarily.
| 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 |
| /** | ||
| * 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 |
Copilot
AI
Dec 4, 2025
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.
The new allowOutsideClick functionality lacks test coverage. Since there are no existing tests for useFocusTrap in the __tests__ directory, and this introduces significant new behavior that could affect focus restoration, tests should be added to verify:
- Focus is not restored when clicking outside with
allowOutsideClick: true - Focus is properly restored when clicking outside with
allowOutsideClick: falseor undefined - The interaction between
allowOutsideClickandreturnFocusRef/restoreFocusOnCleanUp
Consider adding comprehensive tests for this hook to ensure the focus restoration logic works correctly in all scenarios.
| /** | ||
| * 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 |
Copilot
AI
Dec 4, 2025
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.
The documentation states that allowOutsideClick "should allow focus to escape the trap when clicking outside," but the implementation doesn't actually allow focus to escape - it only prevents focus restoration. The actual trap is aborted (line 106), but the wording "allow focus to escape" is misleading since it suggests the focus trap will remain active but allow outside clicks, when in reality it's being disabled.
Consider clarifying the documentation to say: "If true, clicking outside the trap container will disable the trap without restoring focus to the previously focused element."
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.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
Closes #https://github.com/github/primer/issues/5200
Changelog
New
Changed
Removed
Rollout strategy
Testing & Reviewing
Merge checklist