fix(build): harden radix ref cleanup patches

This commit is contained in:
777genius 2026-05-25 23:15:52 +03:00
parent a6dd0061a8
commit 4bf707c720
9 changed files with 968 additions and 16 deletions

View file

@ -33,6 +33,7 @@
"smoke:codex-runtime-install": "tsx scripts/smoke/codex-runtime-install.ts",
"prebuild": "node ./scripts/ci/verify-radix-presence-patch.mjs && tsx scripts/fetch-pricing-data.ts && pnpm --filter agent-teams-controller build && pnpm --filter agent-teams-mcp build",
"build": "node --max-old-space-size=8192 ./node_modules/electron-vite/bin/electron-vite.js build",
"postbuild": "node ./scripts/ci/verify-radix-renderer-bundle.mjs",
"stage-runtime": "node ./scripts/stage-runtime.mjs",
"clean:runtime": "node ./scripts/stage-runtime.mjs --clean",
"pack:mac": "node ./scripts/electron-builder/dist.mjs --mac",
@ -440,7 +441,12 @@
"patchedDependencies": {
"@radix-ui/react-presence@1.1.5": "patches/@radix-ui__react-presence@1.1.5.patch",
"@radix-ui/react-focus-scope@1.1.7": "patches/@radix-ui__react-focus-scope@1.1.7.patch",
"@radix-ui/react-dismissable-layer@1.1.11": "patches/@radix-ui__react-dismissable-layer@1.1.11.patch"
"@radix-ui/react-dismissable-layer@1.1.11": "patches/@radix-ui__react-dismissable-layer@1.1.11.patch",
"@radix-ui/react-popper@1.2.8": "patches/@radix-ui__react-popper@1.2.8.patch",
"@radix-ui/react-select@2.2.6": "patches/@radix-ui__react-select@2.2.6.patch",
"@radix-ui/react-tooltip@1.2.8": "patches/@radix-ui__react-tooltip@1.2.8.patch",
"@radix-ui/react-menu@2.1.16": "patches/@radix-ui__react-menu@2.1.16.patch",
"@radix-ui/react-checkbox@1.3.3": "patches/@radix-ui__react-checkbox@1.3.3.patch"
}
},
"knip": {

View file

@ -0,0 +1,178 @@
diff --git a/dist/index.js b/dist/index.js
index 6fb1a27897e543acf531704205f561f98eba0229..5180fb3fbd81c43ad539c3b2bf8047ee845f9602 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -59,6 +59,29 @@ var import_jsx_runtime = require("react/jsx-runtime");
var CHECKBOX_NAME = "Checkbox";
var [createCheckboxContext, createCheckboxScope] = (0, import_react_context.createContextScope)(CHECKBOX_NAME);
var [CheckboxProviderImpl, useCheckboxContext] = createCheckboxContext(CHECKBOX_NAME);
+function useGuardedNodeSetter(setNode) {
+ const nodeRef = React.useRef(null);
+ const nodeCleanupGenerationRef = React.useRef(0);
+ return React.useCallback((node) => {
+ const syncNode = (nextNode) => {
+ if (nodeRef.current === nextNode) return;
+ nodeRef.current = nextNode;
+ setNode(nextNode);
+ };
+ nodeCleanupGenerationRef.current += 1;
+ const cleanupGeneration = nodeCleanupGenerationRef.current;
+ if (node) {
+ syncNode(node);
+ return;
+ }
+ queueMicrotask(() => {
+ if (nodeCleanupGenerationRef.current !== cleanupGeneration) {
+ return;
+ }
+ syncNode(null);
+ });
+ }, [setNode]);
+}
function CheckboxProvider(props) {
const {
__scopeCheckbox,
@@ -82,6 +105,8 @@ function CheckboxProvider(props) {
});
const [control, setControl] = React.useState(null);
const [bubbleInput, setBubbleInput] = React.useState(null);
+ const setControlRef = useGuardedNodeSetter(setControl);
+ const setBubbleInputRef = useGuardedNodeSetter(setBubbleInput);
const hasConsumerStoppedPropagationRef = React.useRef(false);
const isFormControl = control ? !!form || !!control.closest("form") : (
// We set this to true by default so that events bubble to forms without JS (SSR)
@@ -92,7 +117,7 @@ function CheckboxProvider(props) {
disabled,
setChecked,
control,
- setControl,
+ setControl: setControlRef,
name,
form,
value,
@@ -101,7 +126,7 @@ function CheckboxProvider(props) {
defaultChecked: isIndeterminate(defaultChecked) ? false : defaultChecked,
isFormControl,
bubbleInput,
- setBubbleInput
+ setBubbleInput: setBubbleInputRef
};
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
CheckboxProviderImpl,
@@ -121,13 +146,13 @@ var CheckboxTrigger = React.forwardRef(
disabled,
checked,
required,
- setControl,
+ setControl: setControlRef,
setChecked,
hasConsumerStoppedPropagationRef,
isFormControl,
bubbleInput
} = useCheckboxContext(TRIGGER_NAME, __scopeCheckbox);
- const composedRefs = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, setControl);
+ const composedRefs = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, setControlRef);
const initialCheckedStateRef = React.useRef(checked);
React.useEffect(() => {
const form = control?.form;
@@ -250,9 +275,9 @@ var CheckboxBubbleInput = React.forwardRef(
value,
form,
bubbleInput,
- setBubbleInput
+ setBubbleInput: setBubbleInputRef
} = useCheckboxContext(BUBBLE_INPUT_NAME, __scopeCheckbox);
- const composedRefs = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, setBubbleInput);
+ const composedRefs = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, setBubbleInputRef);
const prevChecked = (0, import_react_use_previous.usePrevious)(checked);
const controlSize = (0, import_react_use_size.useSize)(control);
React.useEffect(() => {
diff --git a/dist/index.mjs b/dist/index.mjs
index 3f718f60b0032a25ad9386082aab9329bd6f5c7a..0688026b0d188d36dc0c4c036c9ededcc8e4f9fe 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -14,6 +14,29 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
var CHECKBOX_NAME = "Checkbox";
var [createCheckboxContext, createCheckboxScope] = createContextScope(CHECKBOX_NAME);
var [CheckboxProviderImpl, useCheckboxContext] = createCheckboxContext(CHECKBOX_NAME);
+function useGuardedNodeSetter(setNode) {
+ const nodeRef = React.useRef(null);
+ const nodeCleanupGenerationRef = React.useRef(0);
+ return React.useCallback((node) => {
+ const syncNode = (nextNode) => {
+ if (nodeRef.current === nextNode) return;
+ nodeRef.current = nextNode;
+ setNode(nextNode);
+ };
+ nodeCleanupGenerationRef.current += 1;
+ const cleanupGeneration = nodeCleanupGenerationRef.current;
+ if (node) {
+ syncNode(node);
+ return;
+ }
+ queueMicrotask(() => {
+ if (nodeCleanupGenerationRef.current !== cleanupGeneration) {
+ return;
+ }
+ syncNode(null);
+ });
+ }, [setNode]);
+}
function CheckboxProvider(props) {
const {
__scopeCheckbox,
@@ -37,6 +60,8 @@ function CheckboxProvider(props) {
});
const [control, setControl] = React.useState(null);
const [bubbleInput, setBubbleInput] = React.useState(null);
+ const setControlRef = useGuardedNodeSetter(setControl);
+ const setBubbleInputRef = useGuardedNodeSetter(setBubbleInput);
const hasConsumerStoppedPropagationRef = React.useRef(false);
const isFormControl = control ? !!form || !!control.closest("form") : (
// We set this to true by default so that events bubble to forms without JS (SSR)
@@ -47,7 +72,7 @@ function CheckboxProvider(props) {
disabled,
setChecked,
control,
- setControl,
+ setControl: setControlRef,
name,
form,
value,
@@ -56,7 +81,7 @@ function CheckboxProvider(props) {
defaultChecked: isIndeterminate(defaultChecked) ? false : defaultChecked,
isFormControl,
bubbleInput,
- setBubbleInput
+ setBubbleInput: setBubbleInputRef
};
return /* @__PURE__ */ jsx(
CheckboxProviderImpl,
@@ -76,13 +101,13 @@ var CheckboxTrigger = React.forwardRef(
disabled,
checked,
required,
- setControl,
+ setControl: setControlRef,
setChecked,
hasConsumerStoppedPropagationRef,
isFormControl,
bubbleInput
} = useCheckboxContext(TRIGGER_NAME, __scopeCheckbox);
- const composedRefs = useComposedRefs(forwardedRef, setControl);
+ const composedRefs = useComposedRefs(forwardedRef, setControlRef);
const initialCheckedStateRef = React.useRef(checked);
React.useEffect(() => {
const form = control?.form;
@@ -205,9 +230,9 @@ var CheckboxBubbleInput = React.forwardRef(
value,
form,
bubbleInput,
- setBubbleInput
+ setBubbleInput: setBubbleInputRef
} = useCheckboxContext(BUBBLE_INPUT_NAME, __scopeCheckbox);
- const composedRefs = useComposedRefs(forwardedRef, setBubbleInput);
+ const composedRefs = useComposedRefs(forwardedRef, setBubbleInputRef);
const prevChecked = usePrevious(checked);
const controlSize = useSize(control);
React.useEffect(() => {

View file

@ -0,0 +1,150 @@
diff --git a/dist/index.js b/dist/index.js
index 4d102d0cf4c9f82dcf13a384e5af0af36a4285d3..252229df648edad3752bd8db172ad5aa52a527de 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -113,10 +113,34 @@ var usePopperScope = (0, import_react_popper.createPopperScope)();
var useRovingFocusGroupScope = (0, import_react_roving_focus.createRovingFocusGroupScope)();
var [MenuProvider, useMenuContext] = createMenuContext(MENU_NAME);
var [MenuRootProvider, useMenuRootContext] = createMenuContext(MENU_NAME);
+function useGuardedNodeSetter(setNode) {
+ const nodeRef = React.useRef(null);
+ const nodeCleanupGenerationRef = React.useRef(0);
+ return React.useCallback((node) => {
+ const syncNode = (nextNode) => {
+ if (nodeRef.current === nextNode) return;
+ nodeRef.current = nextNode;
+ setNode(nextNode);
+ };
+ nodeCleanupGenerationRef.current += 1;
+ const cleanupGeneration = nodeCleanupGenerationRef.current;
+ if (node) {
+ syncNode(node);
+ return;
+ }
+ queueMicrotask(() => {
+ if (nodeCleanupGenerationRef.current !== cleanupGeneration) {
+ return;
+ }
+ syncNode(null);
+ });
+ }, [setNode]);
+}
var Menu = (props) => {
const { __scopeMenu, open = false, children, dir, onOpenChange, modal = true } = props;
const popperScope = usePopperScope(__scopeMenu);
const [content, setContent] = React.useState(null);
+ const setContentRef = useGuardedNodeSetter(setContent);
const isUsingKeyboardRef = React.useRef(false);
const handleOpenChange = (0, import_react_use_callback_ref.useCallbackRef)(onOpenChange);
const direction = (0, import_react_direction.useDirection)(dir);
@@ -141,7 +165,7 @@ var Menu = (props) => {
open,
onOpenChange: handleOpenChange,
content,
- onContentChange: setContent,
+ onContentChange: setContentRef,
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
MenuRootProvider,
{
@@ -653,6 +677,8 @@ var MenuSub = (props) => {
const popperScope = usePopperScope(__scopeMenu);
const [trigger, setTrigger] = React.useState(null);
const [content, setContent] = React.useState(null);
+ const setTriggerRef = useGuardedNodeSetter(setTrigger);
+ const setContentRef = useGuardedNodeSetter(setContent);
const handleOpenChange = (0, import_react_use_callback_ref.useCallbackRef)(onOpenChange);
React.useEffect(() => {
if (parentMenuContext.open === false) handleOpenChange(false);
@@ -665,7 +691,7 @@ var MenuSub = (props) => {
open,
onOpenChange: handleOpenChange,
content,
- onContentChange: setContent,
+ onContentChange: setContentRef,
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
MenuSubProvider,
{
@@ -673,7 +699,7 @@ var MenuSub = (props) => {
contentId: (0, import_react_id.useId)(),
triggerId: (0, import_react_id.useId)(),
trigger,
- onTriggerChange: setTrigger,
+ onTriggerChange: setTriggerRef,
children
}
)
diff --git a/dist/index.mjs b/dist/index.mjs
index 10eefb0533ee4fa16b7f0e42671969c2c4464835..680d56317917bff5bec1added766c553c5c00c52 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -46,10 +46,34 @@ var usePopperScope = createPopperScope();
var useRovingFocusGroupScope = createRovingFocusGroupScope();
var [MenuProvider, useMenuContext] = createMenuContext(MENU_NAME);
var [MenuRootProvider, useMenuRootContext] = createMenuContext(MENU_NAME);
+function useGuardedNodeSetter(setNode) {
+ const nodeRef = React.useRef(null);
+ const nodeCleanupGenerationRef = React.useRef(0);
+ return React.useCallback((node) => {
+ const syncNode = (nextNode) => {
+ if (nodeRef.current === nextNode) return;
+ nodeRef.current = nextNode;
+ setNode(nextNode);
+ };
+ nodeCleanupGenerationRef.current += 1;
+ const cleanupGeneration = nodeCleanupGenerationRef.current;
+ if (node) {
+ syncNode(node);
+ return;
+ }
+ queueMicrotask(() => {
+ if (nodeCleanupGenerationRef.current !== cleanupGeneration) {
+ return;
+ }
+ syncNode(null);
+ });
+ }, [setNode]);
+}
var Menu = (props) => {
const { __scopeMenu, open = false, children, dir, onOpenChange, modal = true } = props;
const popperScope = usePopperScope(__scopeMenu);
const [content, setContent] = React.useState(null);
+ const setContentRef = useGuardedNodeSetter(setContent);
const isUsingKeyboardRef = React.useRef(false);
const handleOpenChange = useCallbackRef(onOpenChange);
const direction = useDirection(dir);
@@ -74,7 +98,7 @@ var Menu = (props) => {
open,
onOpenChange: handleOpenChange,
content,
- onContentChange: setContent,
+ onContentChange: setContentRef,
children: /* @__PURE__ */ jsx(
MenuRootProvider,
{
@@ -586,6 +610,8 @@ var MenuSub = (props) => {
const popperScope = usePopperScope(__scopeMenu);
const [trigger, setTrigger] = React.useState(null);
const [content, setContent] = React.useState(null);
+ const setTriggerRef = useGuardedNodeSetter(setTrigger);
+ const setContentRef = useGuardedNodeSetter(setContent);
const handleOpenChange = useCallbackRef(onOpenChange);
React.useEffect(() => {
if (parentMenuContext.open === false) handleOpenChange(false);
@@ -598,7 +624,7 @@ var MenuSub = (props) => {
open,
onOpenChange: handleOpenChange,
content,
- onContentChange: setContent,
+ onContentChange: setContentRef,
children: /* @__PURE__ */ jsx(
MenuSubProvider,
{
@@ -606,7 +632,7 @@ var MenuSub = (props) => {
contentId: useId(),
triggerId: useId(),
trigger,
- onTriggerChange: setTrigger,
+ onTriggerChange: setTriggerRef,
children
}
)

View file

@ -0,0 +1,88 @@
diff --git a/dist/index.js b/dist/index.js
index 5cb1fd007d32021ad950d961dd4a0868576a2c0d..795a24fe0e75aaa042cad0a376a776f3e1436269 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -61,6 +61,29 @@ var ALIGN_OPTIONS = ["start", "center", "end"];
var POPPER_NAME = "Popper";
var [createPopperContext, createPopperScope] = (0, import_react_context.createContextScope)(POPPER_NAME);
var [PopperProvider, usePopperContext] = createPopperContext(POPPER_NAME);
+function useGuardedNodeSetter(setNode) {
+ const nodeRef = React.useRef(null);
+ const nodeCleanupGenerationRef = React.useRef(0);
+ return React.useCallback((node) => {
+ const syncNode = (nextNode) => {
+ if (nodeRef.current === nextNode) return;
+ nodeRef.current = nextNode;
+ setNode(nextNode);
+ };
+ nodeCleanupGenerationRef.current += 1;
+ const cleanupGeneration = nodeCleanupGenerationRef.current;
+ if (node) {
+ syncNode(node);
+ return;
+ }
+ queueMicrotask(() => {
+ if (nodeCleanupGenerationRef.current !== cleanupGeneration) {
+ return;
+ }
+ syncNode(null);
+ });
+ }, [setNode]);
+}
var Popper = (props) => {
const { __scopePopper, children } = props;
const [anchor, setAnchor] = React.useState(null);
@@ -108,7 +131,8 @@ var PopperContent = React.forwardRef(
} = props;
const context = usePopperContext(CONTENT_NAME, __scopePopper);
const [content, setContent] = React.useState(null);
- const composedRefs = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, (node) => setContent(node));
+ const setContentRef = useGuardedNodeSetter(setContent);
+ const composedRefs = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, setContentRef);
const [arrow, setArrow] = React.useState(null);
const arrowSize = (0, import_react_use_size.useSize)(arrow);
const arrowWidth = arrowSize?.width ?? 0;
diff --git a/dist/index.mjs b/dist/index.mjs
index 9f84984eab84e32d20d6c052217da0e3b8374b40..0ffdb6313708ab3113c0ce40fe4519b6b605deff 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -26,6 +26,29 @@ var ALIGN_OPTIONS = ["start", "center", "end"];
var POPPER_NAME = "Popper";
var [createPopperContext, createPopperScope] = createContextScope(POPPER_NAME);
var [PopperProvider, usePopperContext] = createPopperContext(POPPER_NAME);
+function useGuardedNodeSetter(setNode) {
+ const nodeRef = React.useRef(null);
+ const nodeCleanupGenerationRef = React.useRef(0);
+ return React.useCallback((node) => {
+ const syncNode = (nextNode) => {
+ if (nodeRef.current === nextNode) return;
+ nodeRef.current = nextNode;
+ setNode(nextNode);
+ };
+ nodeCleanupGenerationRef.current += 1;
+ const cleanupGeneration = nodeCleanupGenerationRef.current;
+ if (node) {
+ syncNode(node);
+ return;
+ }
+ queueMicrotask(() => {
+ if (nodeCleanupGenerationRef.current !== cleanupGeneration) {
+ return;
+ }
+ syncNode(null);
+ });
+ }, [setNode]);
+}
var Popper = (props) => {
const { __scopePopper, children } = props;
const [anchor, setAnchor] = React.useState(null);
@@ -73,7 +96,8 @@ var PopperContent = React.forwardRef(
} = props;
const context = usePopperContext(CONTENT_NAME, __scopePopper);
const [content, setContent] = React.useState(null);
- const composedRefs = useComposedRefs(forwardedRef, (node) => setContent(node));
+ const setContentRef = useGuardedNodeSetter(setContent);
+ const composedRefs = useComposedRefs(forwardedRef, setContentRef);
const [arrow, setArrow] = React.useState(null);
const arrowSize = useSize(arrow);
const arrowWidth = arrowSize?.width ?? 0;

View file

@ -0,0 +1,280 @@
diff --git a/dist/index.js b/dist/index.js
index dc37ac4a018a086c4244a09a67215dbaa9b4de65..fc80522666f91087ce1bce3a34844b17c74cdf6c 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -104,6 +104,29 @@ var [createSelectContext, createSelectScope] = (0, import_react_context.createCo
var usePopperScope = (0, import_react_popper.createPopperScope)();
var [SelectProvider, useSelectContext] = createSelectContext(SELECT_NAME);
var [SelectNativeOptionsProvider, useSelectNativeOptionsContext] = createSelectContext(SELECT_NAME);
+function useGuardedNodeSetter(setNode) {
+ const nodeRef = React.useRef(null);
+ const nodeCleanupGenerationRef = React.useRef(0);
+ return React.useCallback((node) => {
+ const syncNode = (nextNode) => {
+ if (nodeRef.current === nextNode) return;
+ nodeRef.current = nextNode;
+ setNode(nextNode);
+ };
+ nodeCleanupGenerationRef.current += 1;
+ const cleanupGeneration = nodeCleanupGenerationRef.current;
+ if (node) {
+ syncNode(node);
+ return;
+ }
+ queueMicrotask(() => {
+ if (nodeCleanupGenerationRef.current !== cleanupGeneration) {
+ return;
+ }
+ syncNode(null);
+ });
+ }, [setNode]);
+}
var Select = (props) => {
const {
__scopeSelect,
@@ -124,6 +147,8 @@ var Select = (props) => {
const popperScope = usePopperScope(__scopeSelect);
const [trigger, setTrigger] = React.useState(null);
const [valueNode, setValueNode] = React.useState(null);
+ const setTriggerRef = useGuardedNodeSetter(setTrigger);
+ const setValueNodeRef = useGuardedNodeSetter(setValueNode);
const [valueNodeHasChildren, setValueNodeHasChildren] = React.useState(false);
const direction = (0, import_react_direction.useDirection)(dir);
const [open, setOpen] = (0, import_react_use_controllable_state.useControllableState)({
@@ -148,9 +173,9 @@ var Select = (props) => {
required,
scope: __scopeSelect,
trigger,
- onTriggerChange: setTrigger,
+ onTriggerChange: setTriggerRef,
valueNode,
- onValueNodeChange: setValueNode,
+ onValueNodeChange: setValueNodeRef,
valueNodeHasChildren,
onValueNodeHasChildrenChange: setValueNodeHasChildren,
contentId: (0, import_react_id.useId)(),
@@ -366,11 +391,15 @@ var SelectContentImpl = React.forwardRef(
const context = useSelectContext(CONTENT_NAME, __scopeSelect);
const [content, setContent] = React.useState(null);
const [viewport, setViewport] = React.useState(null);
- const composedRefs = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, (node) => setContent(node));
+ const setContentRef = useGuardedNodeSetter(setContent);
+ const setViewportRef = useGuardedNodeSetter(setViewport);
+ const composedRefs = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, setContentRef);
const [selectedItem, setSelectedItem] = React.useState(null);
const [selectedItemText, setSelectedItemText] = React.useState(
null
);
+ const setSelectedItemRef = useGuardedNodeSetter(setSelectedItem);
+ const setSelectedItemTextRef = useGuardedNodeSetter(setSelectedItemText);
const getItems = useCollection(__scopeSelect);
const [isPositioned, setIsPositioned] = React.useState(false);
const firstValidItemFoundRef = React.useRef(false);
@@ -456,11 +485,11 @@ var SelectContentImpl = React.forwardRef(
const isFirstValidItem = !firstValidItemFoundRef.current && !disabled;
const isSelectedItem = context.value !== void 0 && context.value === value;
if (isSelectedItem || isFirstValidItem) {
- setSelectedItem(node);
+ setSelectedItemRef(node);
if (isFirstValidItem) firstValidItemFoundRef.current = true;
}
},
- [context.value]
+ [context.value, setSelectedItemRef]
);
const handleItemLeave = React.useCallback(() => content?.focus(), [content]);
const itemTextRefCallback = React.useCallback(
@@ -468,10 +497,10 @@ var SelectContentImpl = React.forwardRef(
const isFirstValidItem = !firstValidItemFoundRef.current && !disabled;
const isSelectedItem = context.value !== void 0 && context.value === value;
if (isSelectedItem || isFirstValidItem) {
- setSelectedItemText(node);
+ setSelectedItemTextRef(node);
}
},
- [context.value]
+ [context.value, setSelectedItemTextRef]
);
const SelectPosition = position === "popper" ? SelectPopperPosition : SelectItemAlignedPosition;
const popperContentProps = SelectPosition === SelectPopperPosition ? {
@@ -492,7 +521,7 @@ var SelectContentImpl = React.forwardRef(
scope: __scopeSelect,
content,
viewport,
- onViewportChange: setViewport,
+ onViewportChange: setViewportRef,
itemRefCallback,
selectedItem,
onItemLeave: handleItemLeave,
@@ -580,7 +609,9 @@ var SelectItemAlignedPosition = React.forwardRef((props, forwardedRef) => {
const contentContext = useSelectContentContext(CONTENT_NAME, __scopeSelect);
const [contentWrapper, setContentWrapper] = React.useState(null);
const [content, setContent] = React.useState(null);
- const composedRefs = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, (node) => setContent(node));
+ const setContentWrapperRef = useGuardedNodeSetter(setContentWrapper);
+ const setContentRef = useGuardedNodeSetter(setContent);
+ const composedRefs = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, setContentRef);
const getItems = useCollection(__scopeSelect);
const shouldExpandOnScrollRef = React.useRef(false);
const shouldRepositionRef = React.useRef(true);
@@ -709,7 +740,7 @@ var SelectItemAlignedPosition = React.forwardRef((props, forwardedRef) => {
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
"div",
{
- ref: setContentWrapper,
+ ref: setContentWrapperRef,
style: {
display: "flex",
flexDirection: "column",
@@ -971,9 +1002,10 @@ var SelectItemText = React.forwardRef(
const itemContext = useSelectItemContext(ITEM_TEXT_NAME, __scopeSelect);
const nativeOptionsContext = useSelectNativeOptionsContext(ITEM_TEXT_NAME, __scopeSelect);
const [itemTextNode, setItemTextNode] = React.useState(null);
+ const setItemTextNodeRef = useGuardedNodeSetter(setItemTextNode);
const composedRefs = (0, import_react_compose_refs.useComposedRefs)(
forwardedRef,
- (node) => setItemTextNode(node),
+ setItemTextNodeRef,
itemContext.onItemTextChange,
(node) => contentContext.itemTextRefCallback?.(node, itemContext.value, itemContext.disabled)
);
diff --git a/dist/index.mjs b/dist/index.mjs
index f9b94f39dfddef678ef3086354f8d7413ae27e52..4a53ec0d65aa051b95e3e8ea4aff4fdd52a17406 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -37,6 +37,29 @@ var [createSelectContext, createSelectScope] = createContextScope(SELECT_NAME, [
var usePopperScope = createPopperScope();
var [SelectProvider, useSelectContext] = createSelectContext(SELECT_NAME);
var [SelectNativeOptionsProvider, useSelectNativeOptionsContext] = createSelectContext(SELECT_NAME);
+function useGuardedNodeSetter(setNode) {
+ const nodeRef = React.useRef(null);
+ const nodeCleanupGenerationRef = React.useRef(0);
+ return React.useCallback((node) => {
+ const syncNode = (nextNode) => {
+ if (nodeRef.current === nextNode) return;
+ nodeRef.current = nextNode;
+ setNode(nextNode);
+ };
+ nodeCleanupGenerationRef.current += 1;
+ const cleanupGeneration = nodeCleanupGenerationRef.current;
+ if (node) {
+ syncNode(node);
+ return;
+ }
+ queueMicrotask(() => {
+ if (nodeCleanupGenerationRef.current !== cleanupGeneration) {
+ return;
+ }
+ syncNode(null);
+ });
+ }, [setNode]);
+}
var Select = (props) => {
const {
__scopeSelect,
@@ -57,6 +80,8 @@ var Select = (props) => {
const popperScope = usePopperScope(__scopeSelect);
const [trigger, setTrigger] = React.useState(null);
const [valueNode, setValueNode] = React.useState(null);
+ const setTriggerRef = useGuardedNodeSetter(setTrigger);
+ const setValueNodeRef = useGuardedNodeSetter(setValueNode);
const [valueNodeHasChildren, setValueNodeHasChildren] = React.useState(false);
const direction = useDirection(dir);
const [open, setOpen] = useControllableState({
@@ -81,9 +106,9 @@ var Select = (props) => {
required,
scope: __scopeSelect,
trigger,
- onTriggerChange: setTrigger,
+ onTriggerChange: setTriggerRef,
valueNode,
- onValueNodeChange: setValueNode,
+ onValueNodeChange: setValueNodeRef,
valueNodeHasChildren,
onValueNodeHasChildrenChange: setValueNodeHasChildren,
contentId: useId(),
@@ -299,11 +324,15 @@ var SelectContentImpl = React.forwardRef(
const context = useSelectContext(CONTENT_NAME, __scopeSelect);
const [content, setContent] = React.useState(null);
const [viewport, setViewport] = React.useState(null);
- const composedRefs = useComposedRefs(forwardedRef, (node) => setContent(node));
+ const setContentRef = useGuardedNodeSetter(setContent);
+ const setViewportRef = useGuardedNodeSetter(setViewport);
+ const composedRefs = useComposedRefs(forwardedRef, setContentRef);
const [selectedItem, setSelectedItem] = React.useState(null);
const [selectedItemText, setSelectedItemText] = React.useState(
null
);
+ const setSelectedItemRef = useGuardedNodeSetter(setSelectedItem);
+ const setSelectedItemTextRef = useGuardedNodeSetter(setSelectedItemText);
const getItems = useCollection(__scopeSelect);
const [isPositioned, setIsPositioned] = React.useState(false);
const firstValidItemFoundRef = React.useRef(false);
@@ -389,11 +418,11 @@ var SelectContentImpl = React.forwardRef(
const isFirstValidItem = !firstValidItemFoundRef.current && !disabled;
const isSelectedItem = context.value !== void 0 && context.value === value;
if (isSelectedItem || isFirstValidItem) {
- setSelectedItem(node);
+ setSelectedItemRef(node);
if (isFirstValidItem) firstValidItemFoundRef.current = true;
}
},
- [context.value]
+ [context.value, setSelectedItemRef]
);
const handleItemLeave = React.useCallback(() => content?.focus(), [content]);
const itemTextRefCallback = React.useCallback(
@@ -401,10 +430,10 @@ var SelectContentImpl = React.forwardRef(
const isFirstValidItem = !firstValidItemFoundRef.current && !disabled;
const isSelectedItem = context.value !== void 0 && context.value === value;
if (isSelectedItem || isFirstValidItem) {
- setSelectedItemText(node);
+ setSelectedItemTextRef(node);
}
},
- [context.value]
+ [context.value, setSelectedItemTextRef]
);
const SelectPosition = position === "popper" ? SelectPopperPosition : SelectItemAlignedPosition;
const popperContentProps = SelectPosition === SelectPopperPosition ? {
@@ -425,7 +454,7 @@ var SelectContentImpl = React.forwardRef(
scope: __scopeSelect,
content,
viewport,
- onViewportChange: setViewport,
+ onViewportChange: setViewportRef,
itemRefCallback,
selectedItem,
onItemLeave: handleItemLeave,
@@ -513,7 +542,9 @@ var SelectItemAlignedPosition = React.forwardRef((props, forwardedRef) => {
const contentContext = useSelectContentContext(CONTENT_NAME, __scopeSelect);
const [contentWrapper, setContentWrapper] = React.useState(null);
const [content, setContent] = React.useState(null);
- const composedRefs = useComposedRefs(forwardedRef, (node) => setContent(node));
+ const setContentWrapperRef = useGuardedNodeSetter(setContentWrapper);
+ const setContentRef = useGuardedNodeSetter(setContent);
+ const composedRefs = useComposedRefs(forwardedRef, setContentRef);
const getItems = useCollection(__scopeSelect);
const shouldExpandOnScrollRef = React.useRef(false);
const shouldRepositionRef = React.useRef(true);
@@ -642,7 +673,7 @@ var SelectItemAlignedPosition = React.forwardRef((props, forwardedRef) => {
children: /* @__PURE__ */ jsx(
"div",
{
- ref: setContentWrapper,
+ ref: setContentWrapperRef,
style: {
display: "flex",
flexDirection: "column",
@@ -904,9 +935,10 @@ var SelectItemText = React.forwardRef(
const itemContext = useSelectItemContext(ITEM_TEXT_NAME, __scopeSelect);
const nativeOptionsContext = useSelectNativeOptionsContext(ITEM_TEXT_NAME, __scopeSelect);
const [itemTextNode, setItemTextNode] = React.useState(null);
+ const setItemTextNodeRef = useGuardedNodeSetter(setItemTextNode);
const composedRefs = useComposedRefs(
forwardedRef,
- (node) => setItemTextNode(node),
+ setItemTextNodeRef,
itemContext.onItemTextChange,
(node) => contentContext.itemTextRefCallback?.(node, itemContext.value, itemContext.disabled)
);

View file

@ -0,0 +1,102 @@
diff --git a/dist/index.js b/dist/index.js
index 2d0d314a00082c458d2f551d715572cd1fa1b5c3..1954a52d7e25a2d733d57f2f32113af8a69a18bf 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -71,6 +71,29 @@ var PROVIDER_NAME = "TooltipProvider";
var DEFAULT_DELAY_DURATION = 700;
var TOOLTIP_OPEN = "tooltip.open";
var [TooltipProviderContextProvider, useTooltipProviderContext] = createTooltipContext(PROVIDER_NAME);
+function useGuardedNodeSetter(setNode) {
+ const nodeRef = React.useRef(null);
+ const nodeCleanupGenerationRef = React.useRef(0);
+ return React.useCallback((node) => {
+ const syncNode = (nextNode) => {
+ if (nodeRef.current === nextNode) return;
+ nodeRef.current = nextNode;
+ setNode(nextNode);
+ };
+ nodeCleanupGenerationRef.current += 1;
+ const cleanupGeneration = nodeCleanupGenerationRef.current;
+ if (node) {
+ syncNode(node);
+ return;
+ }
+ queueMicrotask(() => {
+ if (nodeCleanupGenerationRef.current !== cleanupGeneration) {
+ return;
+ }
+ syncNode(null);
+ });
+ }, [setNode]);
+}
var TooltipProvider = (props) => {
const {
__scopeTooltip,
@@ -128,6 +151,7 @@ var Tooltip = (props) => {
const providerContext = useTooltipProviderContext(TOOLTIP_NAME, props.__scopeTooltip);
const popperScope = usePopperScope(__scopeTooltip);
const [trigger, setTrigger] = React.useState(null);
+ const setTriggerRef = useGuardedNodeSetter(setTrigger);
const contentId = (0, import_react_id.useId)();
const openTimerRef = React.useRef(0);
const disableHoverableContent = disableHoverableContentProp ?? providerContext.disableHoverableContent;
@@ -185,7 +209,7 @@ var Tooltip = (props) => {
open,
stateAttribute,
trigger,
- onTriggerChange: setTrigger,
+ onTriggerChange: setTriggerRef,
onTriggerEnter: React.useCallback(() => {
if (providerContext.isOpenDelayedRef.current) handleDelayedOpen();
else handleOpen();
diff --git a/dist/index.mjs b/dist/index.mjs
index 568389bf3ce8123fa6de6d298878b32d613e25cf..a809f654d8b2d84be0f5747384a180a0864c7d44 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -24,6 +24,29 @@ var PROVIDER_NAME = "TooltipProvider";
var DEFAULT_DELAY_DURATION = 700;
var TOOLTIP_OPEN = "tooltip.open";
var [TooltipProviderContextProvider, useTooltipProviderContext] = createTooltipContext(PROVIDER_NAME);
+function useGuardedNodeSetter(setNode) {
+ const nodeRef = React.useRef(null);
+ const nodeCleanupGenerationRef = React.useRef(0);
+ return React.useCallback((node) => {
+ const syncNode = (nextNode) => {
+ if (nodeRef.current === nextNode) return;
+ nodeRef.current = nextNode;
+ setNode(nextNode);
+ };
+ nodeCleanupGenerationRef.current += 1;
+ const cleanupGeneration = nodeCleanupGenerationRef.current;
+ if (node) {
+ syncNode(node);
+ return;
+ }
+ queueMicrotask(() => {
+ if (nodeCleanupGenerationRef.current !== cleanupGeneration) {
+ return;
+ }
+ syncNode(null);
+ });
+ }, [setNode]);
+}
var TooltipProvider = (props) => {
const {
__scopeTooltip,
@@ -81,6 +104,7 @@ var Tooltip = (props) => {
const providerContext = useTooltipProviderContext(TOOLTIP_NAME, props.__scopeTooltip);
const popperScope = usePopperScope(__scopeTooltip);
const [trigger, setTrigger] = React.useState(null);
+ const setTriggerRef = useGuardedNodeSetter(setTrigger);
const contentId = useId();
const openTimerRef = React.useRef(0);
const disableHoverableContent = disableHoverableContentProp ?? providerContext.disableHoverableContent;
@@ -138,7 +162,7 @@ var Tooltip = (props) => {
open,
stateAttribute,
trigger,
- onTriggerChange: setTrigger,
+ onTriggerChange: setTriggerRef,
onTriggerEnter: React.useCallback(() => {
if (providerContext.isOpenDelayedRef.current) handleDelayedOpen();
else handleOpen();

View file

@ -48,15 +48,30 @@ overrides:
yaml: 2.9.0
patchedDependencies:
'@radix-ui/react-checkbox@1.3.3':
hash: 80a226b85c8e8a94674990c0eedf6d3709af6649df83bbed6f3ae930783e6d7d
path: patches/@radix-ui__react-checkbox@1.3.3.patch
'@radix-ui/react-dismissable-layer@1.1.11':
hash: c9a989c59554b1942cebb761daaaeb964304d965d236cb0bd2142c4d8f8032c3
path: patches/@radix-ui__react-dismissable-layer@1.1.11.patch
'@radix-ui/react-focus-scope@1.1.7':
hash: cce5af533d09e336a548ebb15555869267a2545df720cfe228aa0a98da80e829
path: patches/@radix-ui__react-focus-scope@1.1.7.patch
'@radix-ui/react-menu@2.1.16':
hash: a59dc5aa0792599fc395b6db50cae81f2bcdb76e9d28205b12ce88906d3aeaa0
path: patches/@radix-ui__react-menu@2.1.16.patch
'@radix-ui/react-popper@1.2.8':
hash: bab709aa37cbdb3023036cd85534ddb1400f2fccfe54bd6321fe405d5d199404
path: patches/@radix-ui__react-popper@1.2.8.patch
'@radix-ui/react-presence@1.1.5':
hash: afe90f800cfb3b1ce1a9c457772e2441a9202e1aa3f8658eb3b9613b3ba0ef7e
path: patches/@radix-ui__react-presence@1.1.5.patch
'@radix-ui/react-select@2.2.6':
hash: eea50c1407feb65af64720d0aadd2b534ca7743474d9204e0fc1d6b93e5f31f4
path: patches/@radix-ui__react-select@2.2.6.patch
'@radix-ui/react-tooltip@1.2.8':
hash: 92cb648a695f616d3b7222b90053cb36e162bab4303abf0fe39b517e1d9dd6b8
path: patches/@radix-ui__react-tooltip@1.2.8.patch
importers:
@ -166,7 +181,7 @@ importers:
version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-checkbox':
specifier: ^1.3.3
version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
version: 1.3.3(patch_hash=80a226b85c8e8a94674990c0eedf6d3709af6649df83bbed6f3ae930783e6d7d)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-collapsible':
specifier: ^1.1.12
version: 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@ -193,7 +208,7 @@ importers:
version: 1.1.5(patch_hash=afe90f800cfb3b1ce1a9c457772e2441a9202e1aa3f8658eb3b9613b3ba0ef7e)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-select':
specifier: ^2.2.6
version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
version: 2.2.6(patch_hash=eea50c1407feb65af64720d0aadd2b534ca7743474d9204e0fc1d6b93e5f31f4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-slot':
specifier: ^1.2.4
version: 1.2.4(@types/react@19.2.14)(react@19.2.4)
@ -202,7 +217,7 @@ importers:
version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-tooltip':
specifier: ^1.2.8
version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
version: 1.2.8(patch_hash=92cb648a695f616d3b7222b90053cb36e162bab4303abf0fe39b517e1d9dd6b8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@sentry/electron':
specifier: ^7.10.0
version: 7.10.0
@ -14468,7 +14483,7 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@radix-ui/react-checkbox@1.3.3(patch_hash=80a226b85c8e8a94674990c0eedf6d3709af6649df83bbed6f3ae930783e6d7d)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
@ -14522,7 +14537,7 @@ snapshots:
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-menu': 2.1.16(patch_hash=a59dc5aa0792599fc395b6db50cae81f2bcdb76e9d28205b12ce88906d3aeaa0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
@ -14585,7 +14600,7 @@ snapshots:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-menu': 2.1.16(patch_hash=a59dc5aa0792599fc395b6db50cae81f2bcdb76e9d28205b12ce88906d3aeaa0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
react: 19.2.4
@ -14617,7 +14632,7 @@ snapshots:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-dismissable-layer': 1.1.11(patch_hash=c9a989c59554b1942cebb761daaaeb964304d965d236cb0bd2142c4d8f8032c3)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-popper': 1.2.8(patch_hash=bab709aa37cbdb3023036cd85534ddb1400f2fccfe54bd6321fe405d5d199404)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-presence': 1.1.5(patch_hash=afe90f800cfb3b1ce1a9c457772e2441a9202e1aa3f8658eb3b9613b3ba0ef7e)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@ -14644,7 +14659,7 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@radix-ui/react-menu@2.1.16(patch_hash=a59dc5aa0792599fc395b6db50cae81f2bcdb76e9d28205b12ce88906d3aeaa0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@ -14655,7 +14670,7 @@ snapshots:
'@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-focus-scope': 1.1.7(patch_hash=cce5af533d09e336a548ebb15555869267a2545df720cfe228aa0a98da80e829)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-popper': 1.2.8(patch_hash=bab709aa37cbdb3023036cd85534ddb1400f2fccfe54bd6321fe405d5d199404)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-presence': 1.1.5(patch_hash=afe90f800cfb3b1ce1a9c457772e2441a9202e1aa3f8658eb3b9613b3ba0ef7e)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@ -14679,7 +14694,7 @@ snapshots:
'@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-focus-scope': 1.1.7(patch_hash=cce5af533d09e336a548ebb15555869267a2545df720cfe228aa0a98da80e829)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-popper': 1.2.8(patch_hash=bab709aa37cbdb3023036cd85534ddb1400f2fccfe54bd6321fe405d5d199404)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-presence': 1.1.5(patch_hash=afe90f800cfb3b1ce1a9c457772e2441a9202e1aa3f8658eb3b9613b3ba0ef7e)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@ -14693,7 +14708,7 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@radix-ui/react-popper@1.2.8(patch_hash=bab709aa37cbdb3023036cd85534ddb1400f2fccfe54bd6321fe405d5d199404)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@floating-ui/react-dom': 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@ -14766,7 +14781,7 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@radix-ui/react-select@2.2.6(patch_hash=eea50c1407feb65af64720d0aadd2b534ca7743474d9204e0fc1d6b93e5f31f4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/number': 1.1.1
'@radix-ui/primitive': 1.1.3
@ -14778,7 +14793,7 @@ snapshots:
'@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-focus-scope': 1.1.7(patch_hash=cce5af533d09e336a548ebb15555869267a2545df720cfe228aa0a98da80e829)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-popper': 1.2.8(patch_hash=bab709aa37cbdb3023036cd85534ddb1400f2fccfe54bd6321fe405d5d199404)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
@ -14825,14 +14840,14 @@ snapshots:
'@types/react': 19.2.14
'@types/react-dom': 19.2.3(@types/react@19.2.14)
'@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@radix-ui/react-tooltip@1.2.8(patch_hash=92cb648a695f616d3b7222b90053cb36e162bab4303abf0fe39b517e1d9dd6b8)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-dismissable-layer': 1.1.11(patch_hash=c9a989c59554b1942cebb761daaaeb964304d965d236cb0bd2142c4d8f8032c3)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-popper': 1.2.8(patch_hash=bab709aa37cbdb3023036cd85534ddb1400f2fccfe54bd6321fe405d5d199404)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-presence': 1.1.5(patch_hash=afe90f800cfb3b1ce1a9c457772e2441a9202e1aa3f8658eb3b9613b3ba0ef7e)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)

View file

@ -9,16 +9,60 @@ const patchChecks = [
{
packageName: '@radix-ui/react-presence',
requiredMarkers: ['nodeCleanupGenerationRef', 'syncNode(null)'],
forbiddenSnippets: ['setNode(node2);'],
},
{
packageName: '@radix-ui/react-focus-scope',
resolverFromPackage: '@radix-ui/react-dialog',
requiredMarkers: ['containerCleanupGenerationRef', 'syncContainer(null)'],
forbiddenSnippets: ['(node) => setContainer(node)'],
},
{
packageName: '@radix-ui/react-dismissable-layer',
resolverFromPackage: '@radix-ui/react-dialog',
requiredMarkers: ['nodeCleanupGenerationRef', 'syncNode(null)'],
forbiddenSnippets: ['(node2) => setNode(node2)'],
},
{
packageName: '@radix-ui/react-select',
requiredMarkers: ['useGuardedNodeSetter', 'setContentRef', 'setItemTextNodeRef'],
forbiddenSnippets: [
'(node) => setContent(node)',
'(node) => setItemTextNode(node)',
'onTriggerChange: setTrigger,',
'onValueNodeChange: setValueNode,',
'onViewportChange: setViewport,',
'ref: setContentWrapper,',
'setSelectedItem(node);',
'setSelectedItemText(node);',
],
},
{
packageName: '@radix-ui/react-popper',
resolverFromPackage: '@radix-ui/react-select',
requiredMarkers: ['useGuardedNodeSetter', 'setContentRef'],
forbiddenSnippets: ['(node) => setContent(node)'],
},
{
packageName: '@radix-ui/react-tooltip',
requiredMarkers: ['useGuardedNodeSetter', 'setTriggerRef'],
forbiddenSnippets: ['onTriggerChange: setTrigger,'],
},
{
packageName: '@radix-ui/react-menu',
resolverFromPackage: '@radix-ui/react-dropdown-menu',
requiredMarkers: ['useGuardedNodeSetter', 'setContentRef', 'setTriggerRef'],
forbiddenSnippets: ['onContentChange: setContent,', 'onTriggerChange: setTrigger,'],
},
{
packageName: '@radix-ui/react-checkbox',
requiredMarkers: ['useGuardedNodeSetter', 'setControlRef', 'setBubbleInputRef'],
forbiddenSnippets: [
'useComposedRefs(forwardedRef, setControl)',
'useComposedRefs(forwardedRef, setBubbleInput)',
'useComposedRefs)(forwardedRef, setControl)',
'useComposedRefs)(forwardedRef, setBubbleInput)',
],
},
];
@ -42,6 +86,14 @@ for (const check of patchChecks) {
if (missingMarkers.length > 0) {
missing.push(`${check.packageName}/${relativePath}: ${missingMarkers.join(', ')}`);
}
const forbiddenSnippets = check.forbiddenSnippets ?? [];
const presentForbiddenSnippets = forbiddenSnippets.filter((snippet) => source.includes(snippet));
if (presentForbiddenSnippets.length > 0) {
missing.push(
`${check.packageName}/${relativePath}: forbidden snippets still present: ${presentForbiddenSnippets.join(', ')}`
);
}
}
}

View file

@ -0,0 +1,81 @@
import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
const assetsDir = join(process.cwd(), 'out', 'renderer', 'assets');
const rendererBundles = readdirSync(assetsDir)
.filter((entry) => entry.endsWith('.js'))
.sort();
if (rendererBundles.length === 0) {
console.error('No renderer JavaScript bundles found under out/renderer/assets.');
process.exit(1);
}
const requiredMarkers = [
'nodeCleanupGenerationRef',
'syncNode(null)',
'useGuardedNodeSetter',
'setTriggerRef',
'setValueNodeRef',
'setContentRef',
'setViewportRef',
'setSelectedItemRef',
'setSelectedItemTextRef',
'setItemTextNodeRef',
'setControlRef',
'setBubbleInputRef',
];
const forbiddenSnippets = [
'(node) => setContent(node)',
'(node2) => setNode(node2)',
'(node) => setItemTextNode(node)',
'onContentChange: setContent,',
'onTriggerChange: setTrigger,',
'onValueNodeChange: setValueNode,',
'onViewportChange: setViewport,',
'ref: setContentWrapper,',
'setSelectedItem(node);',
'setSelectedItemText(node);',
'useComposedRefs(forwardedRef, setControl)',
'useComposedRefs(forwardedRef, setBubbleInput)',
'useComposedRefs)(forwardedRef, setControl)',
'useComposedRefs)(forwardedRef, setBubbleInput)',
];
const failures = [];
const bundleSources = new Map();
let combinedSource = '';
for (const bundleName of rendererBundles) {
const bundlePath = join(assetsDir, bundleName);
const source = readFileSync(bundlePath, 'utf8');
bundleSources.set(bundleName, source);
combinedSource += source;
}
const missingMarkers = requiredMarkers.filter((marker) => !combinedSource.includes(marker));
if (missingMarkers.length > 0) {
failures.push(`renderer bundles: missing markers: ${missingMarkers.join(', ')}`);
}
for (const [bundleName, source] of bundleSources) {
const presentForbiddenSnippets = forbiddenSnippets.filter((snippet) => source.includes(snippet));
if (presentForbiddenSnippets.length > 0) {
failures.push(
`${bundleName}: forbidden snippets still present: ${presentForbiddenSnippets.join(', ')}`
);
}
}
if (failures.length > 0) {
console.error(
[
'Renderer bundle was built without the complete Radix React 19 ref-cleanup guards.',
'',
...failures,
].join('\n')
);
process.exit(1);
}