@jsenv/navi 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/jsenv_navi.js +22959 -0
  2. package/index.js +66 -16
  3. package/package.json +23 -11
  4. package/src/actions.js +50 -26
  5. package/src/browser_integration/browser_integration.js +31 -6
  6. package/src/browser_integration/via_history.js +42 -9
  7. package/src/components/action_execution/render_actionable_component.jsx +6 -4
  8. package/src/components/action_execution/use_action.js +51 -282
  9. package/src/components/action_execution/use_execute_action.js +106 -92
  10. package/src/components/action_execution/use_run_on_mount.js +9 -0
  11. package/src/components/action_renderer.jsx +21 -32
  12. package/src/components/demos/0_button_demo.html +574 -103
  13. package/src/components/demos/10_column_reordering_debug.html +277 -0
  14. package/src/components/demos/11_table_selection_debug.html +432 -0
  15. package/src/components/demos/1_checkbox_demo.html +579 -202
  16. package/src/components/demos/2_input_textual_demo.html +81 -138
  17. package/src/components/demos/3_radio_demo.html +0 -2
  18. package/src/components/demos/4_select_demo.html +19 -23
  19. package/src/components/demos/6_tablist_demo.html +77 -0
  20. package/src/components/demos/7_table_selection_demo.html +176 -0
  21. package/src/components/demos/8_table_fixed_headers_demo.html +584 -0
  22. package/src/components/demos/9_table_column_drag_demo.html +325 -0
  23. package/src/components/demos/action/0_button_demo.html +2 -4
  24. package/src/components/demos/action/1_input_text_demo.html +643 -222
  25. package/src/components/demos/action/3_details_demo.html +146 -115
  26. package/src/components/demos/action/4_input_checkbox_demo.html +442 -322
  27. package/src/components/demos/action/5_input_checkbox_state_demo.html +270 -0
  28. package/src/components/demos/action/6_checkbox_list_demo.html +304 -72
  29. package/src/components/demos/action/7_radio_list_demo.html +310 -170
  30. package/src/components/demos/action/{8_editable_text_demo.html → 8_editable_demo.html} +65 -76
  31. package/src/components/demos/action/9_link_demo.html +84 -62
  32. package/src/components/demos/ui_transition/0_action_renderer_ui_transition_demo.html +695 -0
  33. package/src/components/demos/ui_transition/1_nested_ui_transition_demo.html +429 -0
  34. package/src/components/demos/ui_transition/2_height_transition_test.html +295 -0
  35. package/src/components/details/details.jsx +62 -64
  36. package/src/components/edition/editable.jsx +186 -0
  37. package/src/components/field/README.md +247 -0
  38. package/src/components/{input → field}/button.jsx +151 -130
  39. package/src/components/field/checkbox_list.jsx +184 -0
  40. package/src/components/{collect_form_element_values.js → field/collect_form_element_values.js} +7 -4
  41. package/src/components/{input → field}/field_css.js +4 -1
  42. package/src/components/field/form.jsx +211 -0
  43. package/src/components/{input → field}/input.jsx +1 -0
  44. package/src/components/{input → field}/input_checkbox.jsx +132 -155
  45. package/src/components/{input → field}/input_radio.jsx +135 -46
  46. package/src/components/field/input_textual.jsx +418 -0
  47. package/src/components/field/label.jsx +32 -0
  48. package/src/components/field/radio_list.jsx +182 -0
  49. package/src/components/{input → field}/select.jsx +17 -32
  50. package/src/components/field/use_action_events.js +132 -0
  51. package/src/components/field/use_form_events.js +55 -0
  52. package/src/components/field/use_ui_state_controller.js +506 -0
  53. package/src/components/item_tracker/README.md +461 -0
  54. package/src/components/item_tracker/use_isolated_item_tracker.jsx +209 -0
  55. package/src/components/item_tracker/use_isolated_item_tracker_demo.html +148 -0
  56. package/src/components/item_tracker/use_isolated_item_tracker_demo.jsx +460 -0
  57. package/src/components/item_tracker/use_item_tracker.jsx +143 -0
  58. package/src/components/item_tracker/use_item_tracker_demo.html +207 -0
  59. package/src/components/item_tracker/use_item_tracker_demo.jsx +216 -0
  60. package/src/components/keyboard_shortcuts/active_keyboard_shortcuts.jsx +87 -0
  61. package/src/components/keyboard_shortcuts/aria_key_shortcuts.js +61 -0
  62. package/src/components/keyboard_shortcuts/keyboard_key_meta.js +17 -0
  63. package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +371 -0
  64. package/src/components/link/link.jsx +65 -102
  65. package/src/components/link/link_with_icon.jsx +52 -0
  66. package/src/components/loader/loader_background.jsx +85 -64
  67. package/src/components/loader/rectangle_loading.jsx +38 -19
  68. package/src/components/route.jsx +8 -4
  69. package/src/components/selection/selection.jsx +1583 -0
  70. package/src/components/svg/font_sized_svg.jsx +45 -0
  71. package/src/components/svg/icon_and_text.jsx +21 -0
  72. package/src/components/svg/svg_mask_overlay.jsx +105 -0
  73. package/src/components/table/drag/table_drag.jsx +506 -0
  74. package/src/components/table/resize/table_resize.jsx +650 -0
  75. package/src/components/table/resize/table_size.js +43 -0
  76. package/src/components/table/selection/table_selection.js +106 -0
  77. package/src/components/table/selection/table_selection.jsx +203 -0
  78. package/src/components/table/sticky/sticky_group.js +354 -0
  79. package/src/components/table/sticky/table_sticky.js +25 -0
  80. package/src/components/table/sticky/table_sticky.jsx +501 -0
  81. package/src/components/table/table.jsx +721 -0
  82. package/src/components/table/table_css.js +211 -0
  83. package/src/components/table/table_ui.jsx +49 -0
  84. package/src/components/table/use_cells_and_columns.js +90 -0
  85. package/src/components/table/use_object_array_to_cells.js +46 -0
  86. package/src/components/table/z_indexes.js +23 -0
  87. package/src/components/tablist/tablist.jsx +99 -0
  88. package/src/components/text/overflow.jsx +15 -0
  89. package/src/components/text/text_and_count.jsx +28 -0
  90. package/src/components/ui_transition.jsx +128 -0
  91. package/src/components/use_auto_focus.js +58 -7
  92. package/src/components/use_batch_during_render.js +33 -0
  93. package/src/components/use_debounce_true.js +7 -7
  94. package/src/components/use_dependencies_diff.js +35 -0
  95. package/src/components/use_focus_group.js +4 -3
  96. package/src/components/use_initial_value.js +8 -34
  97. package/src/components/use_signal_sync.js +1 -1
  98. package/src/components/use_stable_callback.js +68 -0
  99. package/src/components/use_state_array.js +16 -9
  100. package/src/docs/actions.md +22 -0
  101. package/src/notes.md +33 -12
  102. package/src/route/route.js +97 -47
  103. package/src/store/resource_graph.js +2 -1
  104. package/src/store/tests/{resource_graph_dependencies.test.js → resource_graph_dependencies.test_manual.js} +13 -13
  105. package/src/utils/is_signal.js +20 -0
  106. package/src/utils/stringify_for_display.js +4 -23
  107. package/src/validation/constraints/confirm_constraint.js +14 -0
  108. package/src/validation/constraints/create_unique_value_constraint.js +27 -0
  109. package/src/validation/constraints/native_constraints.js +313 -0
  110. package/src/validation/constraints/readonly_constraint.js +36 -0
  111. package/src/validation/constraints/single_space_constraint.js +13 -0
  112. package/src/validation/custom_constraint_validation.js +599 -0
  113. package/src/validation/custom_message.js +18 -0
  114. package/src/validation/demos/browser_style.png +0 -0
  115. package/src/validation/demos/form_validation_demo.html +142 -0
  116. package/src/validation/demos/form_validation_demo_preact.html +87 -0
  117. package/src/validation/demos/form_validation_native_popover_demo.html +168 -0
  118. package/src/validation/demos/form_validation_vs_native_demo.html +172 -0
  119. package/src/validation/demos/validation_message_demo.html +203 -0
  120. package/src/validation/hooks/use_constraints.js +23 -0
  121. package/src/validation/hooks/use_custom_validation_ref.js +73 -0
  122. package/src/validation/hooks/use_validation_message.js +19 -0
  123. package/src/validation/validation_message.js +741 -0
  124. package/src/components/editable_text/editable_text.jsx +0 -96
  125. package/src/components/form.jsx +0 -144
  126. package/src/components/input/checkbox_list.jsx +0 -294
  127. package/src/components/input/field.jsx +0 -61
  128. package/src/components/input/input_textual.jsx +0 -338
  129. package/src/components/input/radio_list.jsx +0 -283
  130. package/src/components/input/use_form_event.js +0 -20
  131. package/src/components/input/use_on_change.js +0 -12
  132. package/src/components/selection/selection.js +0 -5
  133. package/src/components/selection/selection_context.jsx +0 -262
  134. package/src/components/shortcut/shortcut_context.jsx +0 -390
  135. package/src/components/use_action_events.js +0 -37
  136. package/src/utils/iterable_weak_set.js +0 -62
  137. /package/src/components/demos/action/{11_nested_shortcuts_demo.html → 11_nested_shortcuts_demo.xhtml} +0 -0
  138. /package/src/components/{shortcut → keyboard_shortcuts}/os.js +0 -0
  139. /package/src/route/{route.test.html → route.xtest.html} +0 -0
@@ -0,0 +1,461 @@
1
+ # Item Tracker
2
+
3
+ A Preact hook system for tracking dynamic lists, designed to prevent infinite re-renders while enabling component composition similar to native HTML elements.
4
+
5
+ ## The Problem
6
+
7
+ In React/Preact, you can wrap elements into components while maintaining the same compositional flexibility:
8
+
9
+ ```jsx
10
+ // Native HTML
11
+ <select>
12
+ <option>Eastern</option>
13
+ <option>Central</option>
14
+ <option disabled>Mountain</option>
15
+ <option className="highlighted">Pacific</option>
16
+ </select>
17
+
18
+ // Component abstraction - same flexibility
19
+ <TimezoneSelect>
20
+ <TimezoneOption value="est">Eastern</TimezoneOption>
21
+ <TimezoneOption value="cst">Central</TimezoneOption>
22
+ <TimezoneOption value="mst" disabled>Mountain</TimezoneOption>
23
+ <TimezoneOption value="pst" className="highlighted">Pacific</TimezoneOption>
24
+ </TimezoneSelect>
25
+ ```
26
+
27
+ However, when building components that need to coordinate between children, the parent component needs to know about its children's properties.
28
+
29
+ Consider a table where column definitions need to be shared with table cells:
30
+
31
+ ```jsx
32
+ // Desired API - clean composition
33
+ <Table>
34
+ <colgroup>
35
+ <Col id="name" width="200px" sortable />
36
+ <Col id="email" width="300px" resizable />
37
+ <Col id="status" width="100px" />
38
+ </colgroup>
39
+ <tbody>
40
+ <tr>
41
+ <Cell column="name">{user.name}</Cell>
42
+ <Cell column="email">{user.email}</Cell>
43
+ <Cell column="status">{user.status}</Cell>
44
+ </tr>
45
+ </tbody>
46
+ </Table>
47
+ ```
48
+
49
+ The challenge: `<Cell>` components need access to column configuration defined by `<Col>` components, but they're not in a parent-child relationship.
50
+
51
+ ## Alternative Approaches
52
+
53
+ Most libraries use configuration objects instead of components:
54
+
55
+ ```jsx
56
+ <Table
57
+ columns={[
58
+ { id: "name", width: "200px", sortable: true },
59
+ { id: "email", width: "300px", resizable: true },
60
+ { id: "status", width: "100px" },
61
+ ]}
62
+ data={tableData}
63
+ />
64
+ ```
65
+
66
+ This approach works but requires complex APIs when you need to customize individual columns:
67
+
68
+ ```jsx
69
+ <Table
70
+ columns={columns}
71
+ getColumnProps={(column, index) => {
72
+ if (column.id === "email") return { className: "email-column" };
73
+ return {};
74
+ }}
75
+ cellRenderers={{
76
+ email: (value) => value.toLowerCase(),
77
+ }}
78
+ />
79
+ ```
80
+
81
+ With component composition, the same customization is more straightforward:
82
+
83
+ ```jsx
84
+ <Table>
85
+ <Colgroup>
86
+ <Col id="name" width="200px" sortable />
87
+ <Col id="email" width="300px" resizable className="email-column" />
88
+ <Col id="status" width="100px" />
89
+ </Colgroup>
90
+ <Tbody>
91
+ <Tr>
92
+ <TableCell column="name">{user.name}</TableCell>
93
+ <TableCell column="email">{user.email.toLowerCase()}</TableCell>
94
+ <TableCell column="status">{user.status}</TableCell>
95
+ </Tr>
96
+ </Tbody>
97
+ </Table>
98
+ ```
99
+
100
+ The problem is: how does the parent component discover its children's properties without causing infinite re-renders?
101
+
102
+ ## Our Solution
103
+
104
+ The Item Tracker provides hooks that enable component composition while preventing infinite re-renders. It offers two approaches depending on your component structure:
105
+
106
+ **For library authors**, the system provides hooks to build components with clean APIs:
107
+
108
+ ```jsx
109
+ // Internal implementation
110
+ function Table({ children }) {
111
+ const ColTrackerProvider = useItemTrackerProvider();
112
+
113
+ return (
114
+ <ColTrackerProvider>
115
+ <Table>
116
+ <TableHeaders /> {/* Reads column data */}
117
+ <Tbody>{children}</Tbody>
118
+ </Table>
119
+ </ColTrackerProvider>
120
+ );
121
+ }
122
+
123
+ function Col({ id, width, sortable, ...props }) {
124
+ const colIndex = useTrackItem({ id, width, sortable, ...props });
125
+ return <col style={{ width }} />;
126
+ }
127
+
128
+ function TableCell({ column, children }) {
129
+ const columns = useTrackedItems();
130
+ const colData = columns.find((col) => col.id === column);
131
+ return <td style={{ width: colData.width }}>{children}</td>;
132
+ }
133
+ ```
134
+
135
+ **For end users**, this enables clean, component-based APIs:
136
+
137
+ ```jsx
138
+ // User-facing API
139
+ <Table>
140
+ <Colgroup>
141
+ <Col id="name" width="200px" sortable className="name-col" />
142
+ <Col id="email" width="300px" resizable />
143
+ <Col id="status" width="100px" />
144
+ </Colgroup>
145
+ <Tbody>
146
+ <Tr>
147
+ <TableCell column="name">{user.name}</TableCell>
148
+ <TableCell column="email">{user.email}</TableCell>
149
+ <TableCell column="status">{user.status}</TableCell>
150
+ </Tr>
151
+ </Tbody>
152
+ </Table>
153
+ ```
154
+
155
+ Key features:
156
+
157
+ - **Prevents infinite re-renders** through ref-based item registration
158
+ - **Enables true composition** - items can be registered anywhere in the component tree
159
+ - **No configuration objects** - each component configures itself through props
160
+ - **Dynamic lists** - supports adding, removing, and reordering items
161
+
162
+ ## Two Systems for Different Use Cases
163
+
164
+ We provide two complementary systems:
165
+
166
+ ### 1. Simple Item Tracker (Colocated)
167
+
168
+ For scenarios where registration and consumption happen in the same component tree:
169
+
170
+ ```jsx
171
+ import { createItemTracker } from "./use_item_tracker.jsx";
172
+
173
+ const [useRowTrackerProvider, useRegisterRow, useRow, useRows] =
174
+ createItemTracker();
175
+
176
+ function App() {
177
+ const RowTrackerProvider = useRowTrackerProvider();
178
+
179
+ return (
180
+ <RowTrackerProvider>
181
+ <table>
182
+ <tbody>
183
+ {rows.map((data) => (
184
+ <TableRow key={data.id} data={data}>
185
+ <TableCell column="name" />
186
+ <TableCell column="email" />
187
+ </TableRow>
188
+ ))}
189
+ </tbody>
190
+ </table>
191
+ </RowTrackerProvider>
192
+ );
193
+ }
194
+
195
+ function TableRow({ data, children }) {
196
+ const rowIndex = useRegisterRow(data);
197
+ return (
198
+ <RowContext.Provider value={rowIndex}>
199
+ <tr>{children}</tr>
200
+ </RowContext.Provider>
201
+ );
202
+ }
203
+
204
+ function TableCell({ column }) {
205
+ const rowIndex = useContext(RowContext);
206
+ const rowData = useRow(rowIndex);
207
+ return <td>{rowData[column]}</td>;
208
+ }
209
+ ```
210
+
211
+ ### 2. Isolated Item Tracker (Separated Trees)
212
+
213
+ For complex scenarios where registration and consumption are in completely separate component trees:
214
+
215
+ ```jsx
216
+ import { createIsolatedItemTracker } from "./use_item_tracker_isolated.jsx";
217
+
218
+ const [useColumnTrackerProviders, useRegisterColumn, useColumn, useColumns] =
219
+ createIsolatedItemTracker();
220
+
221
+ function App() {
222
+ const [ColumnProducerProvider, ColumnConsumerProvider] =
223
+ useColumnTrackerProviders();
224
+
225
+ return (
226
+ <div>
227
+ {/* Producer tree: Registers column data */}
228
+ <Table>
229
+ <Colgroup>
230
+ <ColumnProducerProvider>
231
+ {columns.map((col) => (
232
+ <Col key={col.id} {...col} />
233
+ ))}
234
+ </ColumnProducerProvider>
235
+ </Colgroup>
236
+ {/* Table content */}
237
+ </Table>
238
+
239
+ {/* Consumer tree: Reads column data */}
240
+ <ColumnConsumerProvider>
241
+ <TableControls />
242
+ <ColumnSummary />
243
+ </ColumnConsumerProvider>
244
+ </div>
245
+ );
246
+ }
247
+
248
+ function Col({ id, label, width, sortable }) {
249
+ const columnIndex = useRegisterColumn({ id, label, width, sortable });
250
+ return <col style={{ width }} />;
251
+ }
252
+
253
+ function TableControls() {
254
+ const columns = useColumns(); // Access all columns
255
+ return (
256
+ <div>
257
+ {columns.map((col, index) => (
258
+ <ColumnToggle key={col.id} columnIndex={index} />
259
+ ))}
260
+ </div>
261
+ );
262
+ }
263
+
264
+ function ColumnToggle({ columnIndex }) {
265
+ const column = useColumn(columnIndex); // Access specific column
266
+ return (
267
+ <button>
268
+ Toggle {column.label} ({column.width})
269
+ </button>
270
+ );
271
+ }
272
+ ```
273
+
274
+ ## Key Architecture Benefits
275
+
276
+ ### ✅ No Infinite Re-renders
277
+
278
+ **Producer side uses refs** - Item registration doesn't trigger re-renders:
279
+
280
+ ```jsx
281
+ // This is safe - no state updates during render
282
+ const index = useRegisterItem(data);
283
+ ```
284
+
285
+ **Consumer side uses state** - Only consumers re-render when data changes:
286
+
287
+ ```jsx
288
+ // Only this component re-renders when items change
289
+ const items = useTrackedItems();
290
+ ```
291
+
292
+ ### ✅ True Component Composition
293
+
294
+ Register items anywhere in the producer tree:
295
+
296
+ ```jsx
297
+ <ColumnProducerProvider>
298
+ <div>
299
+ <SomeWrapper>
300
+ <Col id="name" width="200px" />
301
+ </SomeWrapper>
302
+ </div>
303
+ <AnotherComponent>
304
+ <Col id="email" width="300px" />
305
+ </AnotherComponent>
306
+ </ColumnProducerProvider>
307
+ ```
308
+
309
+ Consume items anywhere in the consumer tree:
310
+
311
+ ```jsx
312
+ <ColumnConsumerProvider>
313
+ <Header>
314
+ <ColumnSummary /> {/* Reads all columns */}
315
+ </Header>
316
+ <Sidebar>
317
+ <ColumnFilter columnIndex={0} /> {/* Reads specific column */}
318
+ </Sidebar>
319
+ </ColumnConsumerProvider>
320
+ ```
321
+
322
+ ### ✅ Controlled Synchronization
323
+
324
+ The isolated tracker synchronizes data at controlled moments:
325
+
326
+ - After producer tree renders completely
327
+ - Before consumer tree renders
328
+ - No intermediate state or partial updates
329
+
330
+ ### ✅ Handles Dynamic Lists
331
+
332
+ Add, remove, and reorder items without breaking:
333
+
334
+ ```jsx
335
+ // Columns can be added/removed dynamically
336
+ {
337
+ dynamicColumns.map((col) => <ColumnDefinition key={col.id} {...col} />);
338
+ }
339
+ ```
340
+
341
+ ## When to Use Which System
342
+
343
+ ### Use Simple Item Tracker when:
344
+
345
+ - Registration and consumption happen in the same component tree
346
+ - Parent-child relationships exist between producers and consumers
347
+ - You need a straightforward, lightweight solution
348
+
349
+ **Examples:**
350
+
351
+ - Table rows registering themselves for cell access
352
+ - Navigation items registering for keyboard navigation
353
+ - Form fields registering for validation
354
+
355
+ ### Use Isolated Item Tracker when:
356
+
357
+ - Registration and consumption happen in separate component trees
358
+ - No parent-child relationship exists between producers and consumers
359
+ - You need complex synchronization between distant components
360
+
361
+ **Examples:**
362
+
363
+ - HTML table colgroup → tbody communication
364
+ - Sidebar filters reading main content structure
365
+ - Toolbar controls accessing editor content
366
+ - Dashboard widgets reading data source definitions
367
+
368
+ ## HTML Table Use Case (Primary Motivation)
369
+
370
+ The isolated tracker was specifically designed for HTML table structures:
371
+
372
+ ```jsx
373
+ <Table>
374
+ {/* PRODUCER: Column definitions register metadata */}
375
+ <ColumnProducerProvider>
376
+ <Colgroup>
377
+ <Col
378
+ id="name"
379
+ label="Full Name"
380
+ width="200px"
381
+ sortable={true}
382
+ filterable={true}
383
+ />
384
+ <Col
385
+ id="email"
386
+ label="Email Address"
387
+ width="300px"
388
+ sortable={true}
389
+ filterable={false}
390
+ />
391
+ </Colgroup>
392
+ </ColumnProducerProvider>
393
+
394
+ {/* CONSUMER: Table cells read column metadata */}
395
+ <ColumnConsumerProvider>
396
+ <Thead>
397
+ <Tr>
398
+ <TableCell columnIndex={0} /> {/* Reads name column */}
399
+ <TableCell columnIndex={1} /> {/* Reads email column */}
400
+ </Tr>
401
+ </Thead>
402
+ <Tbody>
403
+ {data.map((row) => (
404
+ <Tr key={row.id}>
405
+ <TableCell columnIndex={0} value={row.name} />
406
+ <TableCell columnIndex={1} value={row.email} />
407
+ </Tr>
408
+ ))}
409
+ </Tbody>
410
+ </ColumnConsumerProvider>
411
+ </Table>
412
+ ```
413
+
414
+ This enables:
415
+
416
+ - **Semantic HTML structure** - Proper `<colgroup>` usage
417
+ - **Column metadata sharing** - Headers and cells access the same data
418
+ - **Dynamic column management** - Add/remove columns without breaking
419
+ - **Rich interactions** - Sorting, filtering, resizing based on column config
420
+ - **Accessibility** - ARIA attributes based on column metadata
421
+
422
+ ## API Reference
423
+
424
+ ### Simple Item Tracker
425
+
426
+ ```jsx
427
+ const [
428
+ useItemTrackerProvider, // () => Provider component
429
+ useTrackItem, // (data) => index
430
+ useTrackedItem, // (index) => data
431
+ useTrackedItems, // () => data[]
432
+ ] = createItemTracker();
433
+ ```
434
+
435
+ ### Isolated Item Tracker
436
+
437
+ ```jsx
438
+ const [
439
+ useItemTrackerProviders, // () => [ProducerProvider, ConsumerProvider]
440
+ useRegisterItem, // (data) => index (use in producer)
441
+ useTrackedItem, // (index) => data (use in consumer)
442
+ useTrackedItems, // () => data[] (use in consumer)
443
+ ] = createIsolatedItemTracker();
444
+ ```
445
+
446
+ ## Performance Characteristics
447
+
448
+ - **Producer registration**: O(1) - No re-renders, direct ref updates
449
+ - **Consumer access**: O(1) - Direct array access by index
450
+ - **Synchronization**: O(n) - One-time copy from refs to state
451
+ - **Memory**: O(n) - Stores one copy in refs, one copy in state during sync
452
+
453
+ ## Browser Support
454
+
455
+ Requires modern JavaScript features:
456
+
457
+ - ES Modules
458
+ - Preact/React hooks
459
+ - `useLayoutEffect` for synchronization timing
460
+
461
+ Compatible with all modern browsers and Node.js environments.
@@ -0,0 +1,209 @@
1
+ // https://github.com/reach/reach-ui/tree/b3d94d22811db6b5c0f272b9a7e2e3c1bb4699ae/packages/descendants
2
+ // https://github.com/pacocoursey/use-descendants/tree/master
3
+
4
+ /*
5
+ * Item Tracker Isolated System - A Preact hook for tracking dynamic lists without infinite re-renders
6
+ *
7
+ * USE CASE:
8
+ * This is specifically designed for scenarios where item registration and usage are SEPARATE,
9
+ * such as HTML tables with colgroup elements where colgroup registers columns and tbody uses them.
10
+ *
11
+ * For simpler cases where item definition and usage are colocated (same component tree),
12
+ * prefer useItemTracker instead.
13
+ *
14
+ * SOLUTION ARCHITECTURE:
15
+ * This system uses a Producer/Consumer pattern with separate context trees:
16
+ *
17
+ * 1. PRODUCER SIDE (ref-based, no re-renders):
18
+ * - ItemProducerProvider: Manages item registration without causing re-renders
19
+ * - useTrackItem: Registers individual items using refs
20
+ * - Items are stored in a mutable array via useRef
21
+ *
22
+ * 2. CONSUMER SIDE (state-based, re-renders when needed):
23
+ * - ItemConsumerProvider: Manages reactive state for consumers
24
+ * - useTrackedItems/useTrackedItem: Read the tracked items with reactivity
25
+ * - State is synchronized from producer side at controlled intervals
26
+ *
27
+ * RENDER ORDER REQUIREMENT:
28
+ * Producer MUST render before Consumer in the React tree for proper synchronization.
29
+ */
30
+
31
+ import { createContext } from "preact";
32
+ import {
33
+ useContext,
34
+ useLayoutEffect,
35
+ useMemo,
36
+ useRef,
37
+ useState,
38
+ } from "preact/hooks";
39
+
40
+ import { compareTwoJsValues } from "../../utils/compare_two_js_values.js";
41
+
42
+ export const createIsolatedItemTracker = () => {
43
+ // Producer contexts (ref-based, no re-renders)
44
+ const ProducerTrackerContext = createContext();
45
+ const ProducerItemCountRefContext = createContext();
46
+ const ProducerListRenderIdContext = createContext();
47
+
48
+ // Consumer contexts (state-based, re-renders)
49
+ const ConsumerItemsContext = createContext();
50
+
51
+ const useIsolatedItemTrackerProvider = () => {
52
+ const itemsRef = useRef([]);
53
+ const items = itemsRef.current;
54
+ const itemCountRef = useRef();
55
+ const pendingFlushRef = useRef(false);
56
+ const producerIsRenderingRef = useRef(false);
57
+
58
+ const itemTracker = useMemo(() => {
59
+ const registerItem = (index, value) => {
60
+ const hasValue = index in items;
61
+ if (hasValue) {
62
+ const currentValue = items[index];
63
+ if (compareTwoJsValues(currentValue, value)) {
64
+ return;
65
+ }
66
+ }
67
+
68
+ items[index] = value;
69
+
70
+ if (producerIsRenderingRef.current) {
71
+ // Consumer will sync after producer render completes
72
+ return;
73
+ }
74
+
75
+ pendingFlushRef.current = true;
76
+ };
77
+
78
+ const getProducerItem = (itemIndex) => {
79
+ return items[itemIndex];
80
+ };
81
+
82
+ const ItemProducerProvider = ({ children }) => {
83
+ items.length = 0;
84
+ itemCountRef.current = 0;
85
+ pendingFlushRef.current = false;
86
+ producerIsRenderingRef.current = true;
87
+ const listRenderId = {};
88
+
89
+ useLayoutEffect(() => {
90
+ producerIsRenderingRef.current = false;
91
+ });
92
+
93
+ // CRITICAL: Sync consumer state on subsequent renders
94
+ const renderedOnce = useRef(false);
95
+ useLayoutEffect(() => {
96
+ if (!renderedOnce.current) {
97
+ renderedOnce.current = true;
98
+ return;
99
+ }
100
+ pendingFlushRef.current = true;
101
+ itemTracker.flushToConsumers();
102
+ }, [listRenderId]);
103
+
104
+ return (
105
+ <ProducerItemCountRefContext.Provider value={itemCountRef}>
106
+ <ProducerListRenderIdContext.Provider value={listRenderId}>
107
+ <ProducerTrackerContext.Provider value={itemTracker}>
108
+ {children}
109
+ </ProducerTrackerContext.Provider>
110
+ </ProducerListRenderIdContext.Provider>
111
+ </ProducerItemCountRefContext.Provider>
112
+ );
113
+ };
114
+
115
+ const ItemConsumerProvider = ({ children }) => {
116
+ const [consumerItems, setConsumerItems] = useState(items);
117
+
118
+ const flushToConsumers = () => {
119
+ if (!pendingFlushRef.current) {
120
+ return;
121
+ }
122
+ const itemsCopy = [...items];
123
+ pendingFlushRef.current = false;
124
+ setConsumerItems(itemsCopy);
125
+ };
126
+ itemTracker.flushToConsumers = flushToConsumers;
127
+
128
+ useLayoutEffect(() => {
129
+ flushToConsumers();
130
+ });
131
+
132
+ return (
133
+ <ConsumerItemsContext.Provider value={consumerItems}>
134
+ {children}
135
+ </ConsumerItemsContext.Provider>
136
+ );
137
+ };
138
+
139
+ return {
140
+ pendingFlushRef,
141
+ registerItem,
142
+ getProducerItem,
143
+ ItemProducerProvider,
144
+ ItemConsumerProvider,
145
+ };
146
+ }, []);
147
+
148
+ const { ItemProducerProvider, ItemConsumerProvider } = itemTracker;
149
+
150
+ return [ItemProducerProvider, ItemConsumerProvider, items];
151
+ };
152
+
153
+ // Hook for producers to register items (ref-based, no re-renders)
154
+ const useTrackIsolatedItem = (data) => {
155
+ const listRenderId = useContext(ProducerListRenderIdContext);
156
+ const itemCountRef = useContext(ProducerItemCountRefContext);
157
+ const itemTracker = useContext(ProducerTrackerContext);
158
+ const listRenderIdRef = useRef();
159
+ const itemIndexRef = useRef();
160
+ const dataRef = useRef();
161
+ const prevListRenderId = listRenderIdRef.current;
162
+
163
+ useLayoutEffect(() => {
164
+ if (itemTracker.pendingFlushRef.current) {
165
+ itemTracker.flushToConsumers();
166
+ }
167
+ });
168
+
169
+ if (prevListRenderId === listRenderId) {
170
+ const itemIndex = itemIndexRef.current;
171
+ itemTracker.registerItem(itemIndex, data);
172
+ dataRef.current = data;
173
+ return itemIndex;
174
+ }
175
+
176
+ listRenderIdRef.current = listRenderId;
177
+ const itemCount = itemCountRef.current;
178
+ const itemIndex = itemCount;
179
+ itemCountRef.current = itemIndex + 1;
180
+ itemIndexRef.current = itemIndex;
181
+ dataRef.current = data;
182
+ itemTracker.registerItem(itemIndex, data);
183
+ return itemIndex;
184
+ };
185
+
186
+ const useTrackedIsolatedItem = (itemIndex) => {
187
+ const items = useTrackedIsolatedItems();
188
+ const item = items[itemIndex];
189
+ return item;
190
+ };
191
+
192
+ // Hooks for consumers to read items (state-based, re-renders)
193
+ const useTrackedIsolatedItems = () => {
194
+ const consumerItems = useContext(ConsumerItemsContext);
195
+ if (!consumerItems) {
196
+ throw new Error(
197
+ "useTrackedIsolatedItems must be used within <ItemConsumerProvider />",
198
+ );
199
+ }
200
+ return consumerItems;
201
+ };
202
+
203
+ return [
204
+ useIsolatedItemTrackerProvider,
205
+ useTrackIsolatedItem,
206
+ useTrackedIsolatedItem,
207
+ useTrackedIsolatedItems,
208
+ ];
209
+ };