Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
diff --git a/dist/index.js b/dist/index.js
index 5ba7a95bc7a29605f32aa010a14224ad87bf6589..699fc222db43d83ac2e3218c0aa5c9db46dc697d 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -39,7 +39,9 @@ module.exports = __toCommonJS(index_exports);
var React = __toESM(require("react"));
function setRef(ref, value) {
if (typeof ref === "function") {
- return ref(value);
+ if (value !== null) {
+ return ref(value);
+ }
} else if (ref !== null && ref !== void 0) {
ref.current = value;
}
diff --git a/dist/index.mjs b/dist/index.mjs
index 7dd9172a7228d70a25f708d0fd577cfc69ea15a8..d8a2f56ecb5951410e1a5748d9a8db56eb966351 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -2,7 +2,9 @@
import * as React from "react";
function setRef(ref, value) {
if (typeof ref === "function") {
- return ref(value);
+ if (value !== null) {
+ return ref(value);
+ }
} else if (ref !== null && ref !== void 0) {
ref.current = value;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
diff --git a/dist/index.js b/dist/index.js
index deae02992529c4daef1710bb7394a94edeea11f5..cb1ae63b7067854f3277ceb9a7cdb66735768e0e 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -78,10 +78,10 @@ var Radio = React.forwardRef(
form,
...radioProps
} = props;
- const [button, setButton] = React.useState(null);
- const composedRefs = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, (node) => setButton(node));
+ const button = React.useRef(null);
+ const composedRefs = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, button);
const hasConsumerStoppedPropagationRef = React.useRef(false);
- const isFormControl = button ? form || !!button.closest("form") : true;
+ const isFormControl = button.current ? form || !!button.current.closest("form") : true;
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RadioProvider, { scope: __scopeRadio, checked, disabled, children: [
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
import_react_primitive.Primitive.button,
@@ -107,7 +107,7 @@ var Radio = React.forwardRef(
isFormControl && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
RadioBubbleInput,
{
- control: button,
+ control: button.current,
bubbles: !hasConsumerStoppedPropagationRef.current,
name,
value,
diff --git a/dist/index.mjs b/dist/index.mjs
index 46bb67f279d7e0a06cb177ad6a8b3de12745d4a5..808c57a8f43de24c89df3cfa3cf6b0f582ddf8e6 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -37,10 +37,10 @@ var Radio = React.forwardRef(
form,
...radioProps
} = props;
- const [button, setButton] = React.useState(null);
- const composedRefs = useComposedRefs(forwardedRef, (node) => setButton(node));
+ const button = React.useRef(null);
+ const composedRefs = useComposedRefs(forwardedRef, button);
const hasConsumerStoppedPropagationRef = React.useRef(false);
- const isFormControl = button ? form || !!button.closest("form") : true;
+ const isFormControl = button.current ? form || !!button.current.closest("form") : true;
return /* @__PURE__ */ jsxs(RadioProvider, { scope: __scopeRadio, checked, disabled, children: [
/* @__PURE__ */ jsx(
Primitive.button,
@@ -66,7 +66,7 @@ var Radio = React.forwardRef(
isFormControl && /* @__PURE__ */ jsx(
RadioBubbleInput,
{
- control: button,
+ control: button.current,
bubbles: !hasConsumerStoppedPropagationRef.current,
name,
value,
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
"@radix-ui/react-dropdown-menu": "2.1.16",
"@radix-ui/react-navigation-menu": "1.2.14",
"@radix-ui/react-popover": "1.1.15",
"@radix-ui/react-radio-group": "1.3.8",
"@radix-ui/react-radio-group": "patch:@radix-ui/react-radio-group@npm%3A1.3.8#./.yarn/patches/@radix-ui-react-radio-group-npm-1.3.8-6cdda6336a.patch",
Comment thread
david-cho-lerat-sonarsource marked this conversation as resolved.
"@radix-ui/react-slot": "1.2.4",
"@radix-ui/react-toggle-group": "1.1.11",
"@radix-ui/react-tooltip": "1.2.8",
Expand All @@ -144,6 +144,7 @@
"sonner": "2.0.7"
},
"resolutions": {
"@radix-ui/react-compose-refs": "patch:@radix-ui/react-compose-refs@npm%3A1.1.2#./.yarn/patches/@radix-ui-react-compose-refs-npm-1.1.2-f0371f8267.patch",
Comment thread
david-cho-lerat-sonarsource marked this conversation as resolved.
"ast-types": "patch:ast-types@npm%3A0.16.1#./.yarn/patches/ast-types-npm-0.16.1-596f974e68.patch",
"axe-core": "4.11.1"
}
Expand Down
21 changes: 15 additions & 6 deletions package.json.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,29 @@

## peerDependencies

- react : Core runtime required by all Echoes React components.
- react-dom : Required for DOM rendering and portals used by some components.
- react-intl : Required for components relying on internationalization.
- react-router-dom : Required by components integrating with routing/navigation context.
- @emotion/react : Required styling runtime for Emotion-based components.
- @emotion/styled : Required for Emotion styled-component APIs used in the library.

## Dependencies

- @mantine/core : Used as a base some components (Select, MultiSelect), currently in v6 because v7 only supports React 18. We had to create a patch to prevent some TS error with React 18, we will be able to drop it once we upgrade to Mantine v7.
- @mantine/hooks : Used as a base some components (Select, MultiSelect), currently in v6 because v7 only supports React 18
- @mantine/core : Used as a base for some components (Select, MultiSelect). We maintain a patch to add `withExpandedAttribute` on `Combobox.Target` in Select so the trigger exposes the expected `aria-expanded` state for accessibility and tests.
- @mantine/hooks : Used with `@mantine/core` as a base for some components (Select, MultiSelect).
- Sonner : Used as the base component for Toast notifications
- @radix-ui/react-radio-group (patch): Replaces an internal callback-ref + `useState` pattern with `useRef` to avoid ref-driven render loops (notably with React 19) that can cause maximum update depth errors.

## DevDependencies

- @testing-library/react : Used for testing, must match the version of React so we are stuck with v12 until we upgrade to React 18
- @testing-library/react : Used for component and interaction testing. It should stay aligned with our React major version.

- @emotion/cache : peer dependencies of Mantine v6
- @emotion/serialize : peer dependencies of Mantine v6
- @emotion/utils : peer dependencies of Mantine v6
- @emotion/cache : Used by Emotion/Mantine styling internals.
- @emotion/serialize : Used by Emotion style serialization internals.
- @emotion/utils : Used by Emotion runtime/style utilities.

## Resolutions

- @radix-ui/react-compose-refs (patch): Prevents forwarding `null` to function refs. This avoids repeated ref teardown/setup feedback loops that can retrigger state updates.
- ast-types: Transitive dependency of Storybook. It is apparently incompatible with typescript 5.4+, but we've never been impacted (why?). With the bump to storybook 9, it requires a patch not to fail ts-check. See [this issue](https://github.com/benjamn/ast-types/issues/948)
55 changes: 38 additions & 17 deletions src/common/helpers/useForwardedRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { type ForwardedRef, useRef, useState } from 'react';
import { type ForwardedRef, useCallback, useRef, useState } from 'react';

/**
* This hook may be used to intercept a forwarded ref, providing a local ref
Expand All @@ -35,16 +35,29 @@ import { type ForwardedRef, useRef, useState } from 'react';
*/
export function useForwardedRefWithState<T>(forwardedRef: ForwardedRef<T>) {
const [ref, setRef] = useState<T | null>(null);
const previousElementRef = useRef<T | null>(null);

const setForwardedRef = (element: T | null) => {
if (typeof forwardedRef === 'function') {
forwardedRef(element);
} else if (forwardedRef) {
forwardedRef.current = element;
}
const setForwardedRef = useCallback(
(element: T | null) => {
// React 19 can invoke function refs with null during detach/reattach cycles.
// Skip null for function refs to avoid commit-phase state update feedback loops.
if (typeof forwardedRef === 'function') {
if (element !== null) {
forwardedRef(element);
}
} else if (forwardedRef) {
forwardedRef.current = element;
}

setRef(element);
};
if (element === null || previousElementRef.current === element) {
return;
}

previousElementRef.current = element;
setRef(element);
},
[forwardedRef],
);

return [ref, setForwardedRef] as const;
}
Expand All @@ -66,14 +79,22 @@ export function useForwardedRefWithState<T>(forwardedRef: ForwardedRef<T>) {
export function useForwardedRef<T>(forwardedRef: ForwardedRef<T>) {
const ref = useRef<T | null>(null);

const setForwardedRef = (element: T | null) => {
if (typeof forwardedRef === 'function') {
forwardedRef(element);
} else if (forwardedRef) {
forwardedRef.current = element;
}
ref.current = element;
};
const setForwardedRef = useCallback(
(element: T | null) => {
if (typeof forwardedRef === 'function') {
if (element !== null) {
forwardedRef(element);
}
} else if (forwardedRef) {
forwardedRef.current = element;
}

if (element !== null) {
ref.current = element;
}
},
[forwardedRef],
);

return [ref, setForwardedRef] as const;
}
43 changes: 42 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3523,6 +3523,19 @@ __metadata:
languageName: node
linkType: hard

"@radix-ui/react-compose-refs@patch:@radix-ui/react-compose-refs@npm%3A1.1.2#./.yarn/patches/@radix-ui-react-compose-refs-npm-1.1.2-f0371f8267.patch::locator=%40sonarsource%2Fechoes-react%40workspace%3A.":
version: 1.1.2
resolution: "@radix-ui/react-compose-refs@patch:@radix-ui/react-compose-refs@npm%3A1.1.2#./.yarn/patches/@radix-ui-react-compose-refs-npm-1.1.2-f0371f8267.patch::version=1.1.2&hash=dc9d5d&locator=%40sonarsource%2Fechoes-react%40workspace%3A."
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/7bb2f8147fa61a50ab9b692dcecbd265c1e4452f266916378befb1d3d7a7195f8dbd2f0b7fdd48ae664252bfeac83d760f128ca207eff4266dd1313badd2c4b2
languageName: node
linkType: hard

"@radix-ui/react-context@npm:1.1.2":
version: 1.1.2
resolution: "@radix-ui/react-context@npm:1.1.2"
Expand Down Expand Up @@ -3913,6 +3926,34 @@ __metadata:
languageName: node
linkType: hard

"@radix-ui/react-radio-group@patch:@radix-ui/react-radio-group@npm%3A1.3.8#./.yarn/patches/@radix-ui-react-radio-group-npm-1.3.8-6cdda6336a.patch::locator=%40sonarsource%2Fechoes-react%40workspace%3A.":
version: 1.3.8
resolution: "@radix-ui/react-radio-group@patch:@radix-ui/react-radio-group@npm%3A1.3.8#./.yarn/patches/@radix-ui-react-radio-group-npm-1.3.8-6cdda6336a.patch::version=1.3.8&hash=ecd358&locator=%40sonarsource%2Fechoes-react%40workspace%3A."
dependencies:
"@radix-ui/primitive": "npm:1.1.3"
"@radix-ui/react-compose-refs": "npm:1.1.2"
"@radix-ui/react-context": "npm:1.1.2"
"@radix-ui/react-direction": "npm:1.1.1"
"@radix-ui/react-presence": "npm:1.1.5"
"@radix-ui/react-primitive": "npm:2.1.3"
"@radix-ui/react-roving-focus": "npm:1.1.11"
"@radix-ui/react-use-controllable-state": "npm:1.2.2"
"@radix-ui/react-use-previous": "npm:1.1.1"
"@radix-ui/react-use-size": "npm:1.1.1"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 10/2c3334ffc762ddc423910a56131ebd23574c1eca308b837bcfefea6d766a6cdfc8f4771769a50a2011d44c29156cd1c825d47cc1dacef57f9a811b4b86cd3060
languageName: node
linkType: hard

"@radix-ui/react-roving-focus@npm:1.1.11":
version: 1.1.11
resolution: "@radix-ui/react-roving-focus@npm:1.1.11"
Expand Down Expand Up @@ -4689,7 +4730,7 @@ __metadata:
"@radix-ui/react-dropdown-menu": "npm:2.1.16"
"@radix-ui/react-navigation-menu": "npm:1.2.14"
"@radix-ui/react-popover": "npm:1.1.15"
"@radix-ui/react-radio-group": "npm:1.3.8"
"@radix-ui/react-radio-group": "patch:@radix-ui/react-radio-group@npm%3A1.3.8#./.yarn/patches/@radix-ui-react-radio-group-npm-1.3.8-6cdda6336a.patch"
"@radix-ui/react-slot": "npm:1.2.4"
"@radix-ui/react-toggle-group": "npm:1.1.11"
"@radix-ui/react-tooltip": "npm:1.2.8"
Expand Down
Loading