@samitouri / QOS-React-1 / commits / a1124489a5

[DOM] Treat omitted Fragment Event listener options same as `capture: false` (#37251)

`FragmentInstance` tracks its event listeners so they can be applied to children added later, and matches them by a normalized options identity. Omitted options currently normalize to a different identity than an explicit `false` or `{capture: false}`, even though both mean `capture: false` per the `EventTarget` contract, where listener identity is the tuple of type, callback, and capture flag. As a result, a listener added without an options argument cannot be removed with an explicit capture-false value (or the reverse). This change normalizes omitted options to the same capture-false identity as `false` and `{capture: false}`. The first commit adds a test to the FragmentRef suite characterizing the current behavior; the second commit contains the fix and the updated assertions. --------- Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>

Leo committed Aug 26, 2026 at 19:09 UTC a1124489a5e8f81e16ac957699a60038b965f502
2 files changed +42 -1
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+1 -1
@@ -3152,7 +3152,7 @@ function normalizeListenerOptions(
3152 opts: ?EventListenerOptionsOrUseCapture,
3153 ): string {
3154 if (opts == null) {
3155 - return '0';
3155 + return 'c=0';
3156 }
3157
3158 if (typeof opts === 'boolean') {
packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
+41
@@ -832,6 +832,47 @@ describe('FragmentRefs', () => {
832 expect(logs).toEqual([]);
833 });
834
835 + // @gate enableFragmentRefs
836 + it('matches listeners by their normalized capture flag', async () => {
837 + const fragmentRef = React.createRef();
838 + const childRef = React.createRef();
839 + const root = ReactDOMClient.createRoot(container);
840 + const logs = [];
841 +
842 + function addedWithOmittedOptions() {
843 + logs.push('addedWithOmittedOptions');
844 + }
845 +
846 + function addedWithCaptureFalse() {
847 + logs.push('addedWithCaptureFalse');
848 + }
849 +
850 + await act(() => {
851 + root.render(
852 + <Fragment ref={fragmentRef}>
853 + <div ref={childRef}>child</div>
854 + </Fragment>,
855 + );
856 + });
857 +
858 + fragmentRef.current.addEventListener('click', addedWithOmittedOptions);
859 + fragmentRef.current.addEventListener('click', addedWithCaptureFalse, {
860 + capture: false,
861 + });
862 +
863 + // Omitted options and an explicit capture: false are the same
864 + // EventTarget listener identity, so each removal should match.
865 + fragmentRef.current.removeEventListener(
866 + 'click',
867 + addedWithOmittedOptions,
868 + false,
869 + );
870 + fragmentRef.current.removeEventListener('click', addedWithCaptureFalse);
871 +
872 + childRef.current.click();
873 + expect(logs).toEqual([]);
874 + });
875 +
876 // @gate enableFragmentRefs
877 it('adds and removes event listeners from children with multiple fragments', async () => {
878 const fragmentRef = React.createRef();