Skip to content

Commit 754eac8

Browse files
authored
Merge pull request #13321 from colinux/fix-combo-sections
Tech: réapplique le patch react-dom pour le combobox par sections
2 parents 2154f19 + 5d2eb7d commit 754eac8

5 files changed

Lines changed: 140 additions & 2 deletions

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Garde-fou contre le crash du perf-track dev de React 19.2 avec les sections
2+
// react-aria : `addObjectDiffToProperties` évalue un getter (`childNodes`) qui
3+
// lève une exception sur les nœuds de collection, gelant le composant à la
4+
// sélection / frappe. Ce comportement est neutralisé par le patch
5+
// `patches/react-dom@<version>.patch`. Ce test reproduit la MÊME structure que
6+
// ComboBox.tsx (AriaComboBox + Virtualizer + Collection de sections) et échoue
7+
// si le patch n'est plus appliqué (typiquement après un bump de react-dom dont
8+
// la clé `patchedDependencies` n'a pas suivi).
9+
import './process-env-shim';
10+
import { suite, test, expect, beforeEach, afterEach } from 'vitest';
11+
import { userEvent, page } from '@vitest/browser/context';
12+
import { useState } from 'react';
13+
import { createRoot, type Root } from 'react-dom/client';
14+
import {
15+
ComboBox as AriaComboBox,
16+
ListBox,
17+
ListBoxItem,
18+
ListBoxSection,
19+
Header,
20+
Popover,
21+
Input,
22+
Button,
23+
Collection,
24+
Virtualizer,
25+
ListLayout
26+
} from 'react-aria-components';
27+
28+
type Item = { label: string; value: string };
29+
type Section = { label: string; items: Item[] };
30+
31+
function buildSections(): Section[] {
32+
return [
33+
{
34+
label: 'Préfectures',
35+
items: [
36+
{ label: 'Préfecture de Paris', value: '75' },
37+
{ label: 'Préfecture du Rhône', value: '69' }
38+
]
39+
},
40+
{
41+
label: 'Ministères',
42+
items: [
43+
{ label: "Ministère de l'Intérieur", value: 'interieur' },
44+
{ label: 'Ministère de la Justice', value: 'justice' }
45+
]
46+
}
47+
];
48+
}
49+
50+
function ComboBoxWithSections() {
51+
const [selectedKey, setSelectedKey] = useState<string | null>(null);
52+
const sections = buildSections();
53+
54+
return (
55+
<AriaComboBox
56+
aria-label="Démarches"
57+
menuTrigger="focus"
58+
selectedKey={selectedKey}
59+
onSelectionChange={(key) =>
60+
setSelectedKey(key == null ? null : String(key))
61+
}
62+
shouldFocusWrap
63+
>
64+
<Input aria-label="Démarches" />
65+
<Button>open</Button>
66+
<Popover>
67+
<Virtualizer layout={ListLayout}>
68+
<ListBox style={{ height: 300, width: 300 }}>
69+
<Collection items={sections}>
70+
{(section) => (
71+
<ListBoxSection id={section.label}>
72+
<Header>{section.label}</Header>
73+
<Collection items={section.items}>
74+
{(item) => (
75+
<ListBoxItem id={item.value}>{item.label}</ListBoxItem>
76+
)}
77+
</Collection>
78+
</ListBoxSection>
79+
)}
80+
</Collection>
81+
</ListBox>
82+
</Virtualizer>
83+
</Popover>
84+
</AriaComboBox>
85+
);
86+
}
87+
88+
suite('ComboBox sections (React 19 perf-track)', () => {
89+
let container: HTMLDivElement;
90+
let root: Root;
91+
const errors: unknown[] = [];
92+
const onError = (e: ErrorEvent) => errors.push(e.error ?? e.message);
93+
94+
beforeEach(() => {
95+
errors.length = 0;
96+
window.addEventListener('error', onError);
97+
container = document.createElement('div');
98+
document.body.appendChild(container);
99+
root = createRoot(container);
100+
});
101+
102+
afterEach(() => {
103+
window.removeEventListener('error', onError);
104+
root.unmount();
105+
container.remove();
106+
});
107+
108+
test('typing in a sectioned combobox does not freeze the component', async () => {
109+
root.render(<ComboBoxWithSections />);
110+
111+
const input = page.getByRole('combobox', { name: 'Démarches' });
112+
await userEvent.click(input);
113+
114+
// La frappe filtre la collection des sections, donc déclenche un re-render
115+
// que le perf-track dev de React diffe (et donc l'évaluation des getters).
116+
await userEvent.type(input, 'Rhô');
117+
118+
// laisser passer les passive effects (le perf-track tourne après le commit)
119+
await new Promise((resolve) => setTimeout(resolve, 300));
120+
121+
// sans le patch : crash « childNodes is not supported » → input gelé sur « R »
122+
await expect.element(input).toHaveValue('Rhô');
123+
expect(
124+
errors,
125+
`erreurs non capturées: ${errors.map(String).join('; ')}`
126+
).toEqual([]);
127+
});
128+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Shim pour les tests vitest en mode browser : react-aria référence
2+
// `process.env.NODE_ENV` (non défini dans le navigateur). Importé en premier
3+
// pour s'exécuter avant le chargement des dépendances react-aria.
4+
const g = globalThis as unknown as {
5+
process?: { env: Record<string, string> };
6+
};
7+
g.process ??= { env: {} };
8+
g.process.env ??= {};
9+
g.process.env.NODE_ENV ??= 'development';

bun.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,6 @@
139139
},
140140
"patchedDependencies": {
141141
"@hotwired/turbo@7.3.0": "patches/@hotwired%2Fturbo@7.3.0.patch",
142-
"react-dom@19.2.4": "patches/react-dom@19.2.4.patch"
142+
"react-dom@19.2.7": "patches/react-dom@19.2.7.patch"
143143
}
144144
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
diff --git a/cjs/react-dom-client.development.js b/cjs/react-dom-client.development.js
2-
index fa853b3a79413c8ddc553441ee259d4b34f59d69..0ccc4c8e33a401e4ea954e291c97c0983232ab36 100644
2+
index 8c860aca7db6a1ca74788209d100f10ae5f085fa..6e6ee0a9ec6aeaa1f2791b23e13a9f3d2a349095 100644
33
--- a/cjs/react-dom-client.development.js
44
+++ b/cjs/react-dom-client.development.js
55
@@ -3965,8 +3965,13 @@

0 commit comments

Comments
 (0)