Skip to content

Commit a3a286c

Browse files
committed
fix(ui): patch radix focus scope ref loop
1 parent 7c3c852 commit a3a286c

4 files changed

Lines changed: 103 additions & 6 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,8 @@
353353
"cpu-features"
354354
],
355355
"patchedDependencies": {
356-
"@radix-ui/react-presence@1.1.5": "patches/@radix-ui__react-presence@1.1.5.patch"
356+
"@radix-ui/react-presence@1.1.5": "patches/@radix-ui__react-presence@1.1.5.patch",
357+
"@radix-ui/react-focus-scope@1.1.7": "patches/@radix-ui__react-focus-scope@1.1.7.patch"
357358
}
358359
},
359360
"knip": {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
diff --git a/dist/index.js b/dist/index.js
2+
index c91ae9196280060974778cbb1164839d5610e7d0..a2dd82afe79d7d0a6640e983166b4b205686dae9 100644
3+
--- a/dist/index.js
4+
+++ b/dist/index.js
5+
@@ -58,7 +58,13 @@ var FocusScope = React.forwardRef((props, forwardedRef) => {
6+
const onMountAutoFocus = (0, import_react_use_callback_ref.useCallbackRef)(onMountAutoFocusProp);
7+
const onUnmountAutoFocus = (0, import_react_use_callback_ref.useCallbackRef)(onUnmountAutoFocusProp);
8+
const lastFocusedElementRef = React.useRef(null);
9+
- const composedRefs = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, (node) => setContainer(node));
10+
+ const containerRef = React.useRef(null);
11+
+ const setContainerRef = React.useCallback((node) => {
12+
+ if (containerRef.current === node) return;
13+
+ containerRef.current = node;
14+
+ setContainer(node);
15+
+ }, []);
16+
+ const composedRefs = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, setContainerRef);
17+
const focusScope = React.useRef({
18+
paused: false,
19+
pause() {
20+
diff --git a/dist/index.mjs b/dist/index.mjs
21+
index e39d5c9105b3f8060d037bf5490843d20d1c859a..70781360acc81bff33c36b8ebd8d6b278df58450 100644
22+
--- a/dist/index.mjs
23+
+++ b/dist/index.mjs
24+
@@ -22,7 +22,13 @@ var FocusScope = React.forwardRef((props, forwardedRef) => {
25+
const onMountAutoFocus = useCallbackRef(onMountAutoFocusProp);
26+
const onUnmountAutoFocus = useCallbackRef(onUnmountAutoFocusProp);
27+
const lastFocusedElementRef = React.useRef(null);
28+
- const composedRefs = useComposedRefs(forwardedRef, (node) => setContainer(node));
29+
+ const containerRef = React.useRef(null);
30+
+ const setContainerRef = React.useCallback((node) => {
31+
+ if (containerRef.current === node) return;
32+
+ containerRef.current = node;
33+
+ setContainer(node);
34+
+ }, []);
35+
+ const composedRefs = useComposedRefs(forwardedRef, setContainerRef);
36+
const focusScope = React.useRef({
37+
paused: false,
38+
pause() {

pnpm-lock.yaml

Lines changed: 8 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React, { act } from 'react';
2+
import { createRoot } from 'react-dom/client';
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4+
5+
import {
6+
Dialog,
7+
DialogContent,
8+
DialogDescription,
9+
DialogTitle,
10+
} from '@renderer/components/ui/dialog';
11+
12+
describe('DialogContent FocusScope integration', () => {
13+
let host: HTMLDivElement;
14+
let root: ReturnType<typeof createRoot>;
15+
16+
beforeEach(() => {
17+
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
18+
host = document.createElement('div');
19+
document.body.appendChild(host);
20+
root = createRoot(host);
21+
});
22+
23+
afterEach(() => {
24+
act(() => {
25+
root.unmount();
26+
});
27+
document.body.innerHTML = '';
28+
vi.unstubAllGlobals();
29+
});
30+
31+
it('keeps the Radix focus scope stable while an open dialog rerenders', () => {
32+
const renderDialog = (label: string): void => {
33+
root.render(
34+
<Dialog open>
35+
<DialogContent>
36+
<DialogTitle>{label}</DialogTitle>
37+
<DialogDescription>Provider model settings</DialogDescription>
38+
<button type="button">Focusable action</button>
39+
</DialogContent>
40+
</Dialog>
41+
);
42+
};
43+
44+
expect(() => {
45+
act(() => {
46+
renderDialog('Create team');
47+
});
48+
act(() => {
49+
renderDialog('Create team updated');
50+
});
51+
}).not.toThrow();
52+
53+
expect(document.body.textContent).toContain('Create team updated');
54+
});
55+
});

0 commit comments

Comments
 (0)