@octanejs/aria 0.0.3 → 0.0.5

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 (93) hide show
  1. package/package.json +7 -5
  2. package/src/autocomplete/useAutocomplete.ts +656 -0
  3. package/src/collections/BaseCollection.ts +374 -0
  4. package/src/collections/CollectionBuilder.ts +343 -0
  5. package/src/collections/Document.ts +451 -0
  6. package/src/collections/Hidden.ts +84 -0
  7. package/src/collections/useCachedChildren.ts +103 -0
  8. package/src/components/Autocomplete.ts +124 -0
  9. package/src/components/Breadcrumbs.ts +170 -0
  10. package/src/components/Button.ts +209 -0
  11. package/src/components/Checkbox.ts +593 -0
  12. package/src/components/Collection.ts +301 -0
  13. package/src/components/ComboBox.ts +477 -0
  14. package/src/components/Dialog.ts +195 -0
  15. package/src/components/Disclosure.ts +322 -0
  16. package/src/components/DragAndDrop.ts +177 -0
  17. package/src/components/FieldError.ts +79 -0
  18. package/src/components/Form.ts +65 -0
  19. package/src/components/GridList.ts +1099 -0
  20. package/src/components/Group.ts +146 -0
  21. package/src/components/Header.ts +32 -0
  22. package/src/components/Heading.ts +45 -0
  23. package/src/components/Input.ts +139 -0
  24. package/src/components/Keyboard.ts +23 -0
  25. package/src/components/Label.ts +30 -0
  26. package/src/components/Link.ts +137 -0
  27. package/src/components/ListBox.ts +886 -0
  28. package/src/components/Menu.ts +685 -0
  29. package/src/components/Meter.ts +100 -0
  30. package/src/components/Modal.ts +352 -0
  31. package/src/components/NumberField.ts +221 -0
  32. package/src/components/OverlayArrow.ts +117 -0
  33. package/src/components/Popover.ts +385 -0
  34. package/src/components/ProgressBar.ts +112 -0
  35. package/src/components/RadioGroup.ts +542 -0
  36. package/src/components/SearchField.ts +208 -0
  37. package/src/components/Select.ts +505 -0
  38. package/src/components/SelectionIndicator.ts +52 -0
  39. package/src/components/Separator.ts +97 -0
  40. package/src/components/SharedElementTransition.ts +209 -0
  41. package/src/components/Slider.ts +472 -0
  42. package/src/components/Switch.ts +449 -0
  43. package/src/components/Tabs.ts +675 -0
  44. package/src/components/TagGroup.ts +443 -0
  45. package/src/components/Text.ts +27 -0
  46. package/src/components/TextArea.ts +92 -0
  47. package/src/components/TextField.ts +200 -0
  48. package/src/components/ToggleButton.ts +147 -0
  49. package/src/components/ToggleButtonGroup.ts +106 -0
  50. package/src/components/Toolbar.ts +74 -0
  51. package/src/components/Tooltip.ts +249 -0
  52. package/src/components/index.ts +302 -0
  53. package/src/components/useDragAndDrop.ts +164 -0
  54. package/src/components/utils.ts +521 -0
  55. package/src/intl/autocomplete/ar-AE.json +3 -0
  56. package/src/intl/autocomplete/bg-BG.json +3 -0
  57. package/src/intl/autocomplete/cs-CZ.json +3 -0
  58. package/src/intl/autocomplete/da-DK.json +3 -0
  59. package/src/intl/autocomplete/de-DE.json +3 -0
  60. package/src/intl/autocomplete/el-GR.json +3 -0
  61. package/src/intl/autocomplete/en-US.json +3 -0
  62. package/src/intl/autocomplete/es-ES.json +3 -0
  63. package/src/intl/autocomplete/et-EE.json +3 -0
  64. package/src/intl/autocomplete/fi-FI.json +3 -0
  65. package/src/intl/autocomplete/fr-FR.json +3 -0
  66. package/src/intl/autocomplete/he-IL.json +3 -0
  67. package/src/intl/autocomplete/hr-HR.json +3 -0
  68. package/src/intl/autocomplete/hu-HU.json +3 -0
  69. package/src/intl/autocomplete/index.ts +75 -0
  70. package/src/intl/autocomplete/it-IT.json +3 -0
  71. package/src/intl/autocomplete/ja-JP.json +3 -0
  72. package/src/intl/autocomplete/ko-KR.json +3 -0
  73. package/src/intl/autocomplete/lt-LT.json +3 -0
  74. package/src/intl/autocomplete/lv-LV.json +3 -0
  75. package/src/intl/autocomplete/nb-NO.json +3 -0
  76. package/src/intl/autocomplete/nl-NL.json +3 -0
  77. package/src/intl/autocomplete/pl-PL.json +3 -0
  78. package/src/intl/autocomplete/pt-BR.json +3 -0
  79. package/src/intl/autocomplete/pt-PT.json +3 -0
  80. package/src/intl/autocomplete/ro-RO.json +3 -0
  81. package/src/intl/autocomplete/ru-RU.json +3 -0
  82. package/src/intl/autocomplete/sk-SK.json +3 -0
  83. package/src/intl/autocomplete/sl-SI.json +3 -0
  84. package/src/intl/autocomplete/sr-SP.json +3 -0
  85. package/src/intl/autocomplete/sv-SE.json +3 -0
  86. package/src/intl/autocomplete/tr-TR.json +3 -0
  87. package/src/intl/autocomplete/uk-UA.json +3 -0
  88. package/src/intl/autocomplete/zh-CN.json +3 -0
  89. package/src/intl/autocomplete/zh-TW.json +3 -0
  90. package/src/stately/autocomplete/useAutocompleteState.ts +85 -0
  91. package/src/utils/animation.ts +149 -0
  92. package/src/utils/useLoadMoreSentinel.ts +78 -0
  93. package/src/utils/useViewportSize.ts +123 -0
@@ -0,0 +1,451 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/collections/Document.ts).
2
+ //
3
+ // GOVERNING ADAPTATION (docs/aria-migration-plan.md §2a; docs/aria-phase-4-handoff.md):
4
+ // upstream renders the hidden collection tree through a React portal into a
5
+ // hand-written FAKE Document whose host-config mutations (createElement /
6
+ // appendChild / insertBefore / removeChild) maintain a mutable fake node tree.
7
+ // octane's compiled templates write REAL DOM, so a fake document can never
8
+ // receive its writes. Instead, the hidden structural copy renders via octane
9
+ // `createPortal` into a DETACHED, never-attached real container owned by this
10
+ // Document, and the store is driven by:
11
+ //
12
+ // - REF REGISTRATION: each `<item>`/`<section>`-style placeholder element
13
+ // (valid HTMLUnknownElements) carries a ref that calls
14
+ // `Document.setElementProps(element, props, …)`, mapping the real element
15
+ // to an `ElementNode` in a `WeakMap<Element, ElementNode>` and updating its
16
+ // immutable CollectionNode copy-on-write (upstream's `setProps` cadence).
17
+ // - REAL-DOM WALKING: snapshots rebuild by walking the detached container in
18
+ // document order (the source of truth upstream's fake sibling links encode).
19
+ // Unregistered wrapper elements are transparent (descended through);
20
+ // `display: none` elements are excluded (upstream's Suspense-hide handling
21
+ // via the fake `style.display` setter).
22
+ // - MUTATION OBSERVER: upstream marks the store dirty synchronously inside
23
+ // the fake host-config calls. Real-DOM moves (keyed reorders of cached
24
+ // children) fire no refs, so a MutationObserver on the detached container
25
+ // is the structural dirty signal. Its delivery is a microtask after commit;
26
+ // `getCollection()` additionally drains `takeRecords()` so any render-time
27
+ // read observes a consistent snapshot. Prop updates still mark dirty
28
+ // synchronously through ref re-registration.
29
+ //
30
+ // Upstream semantics preserved: mutation batching with immutable BaseCollection
31
+ // snapshots, subscribe/getSnapshot for useSyncExternalStore (clone-to-notify,
32
+ // `inSubscription`/`queuedRender` bookkeeping ported verbatim), copy-on-write
33
+ // node cloning so untouched nodes keep identity across updates, generated
34
+ // `react-aria-N` keys, and the id-change error. The fake BaseNode sibling/index
35
+ // bookkeeping (insertBefore/appendChild/invalidateChildIndices) collapses away:
36
+ // the real DOM already stores order.
37
+ //
38
+ // SSR mirrors upstream's separate path: nodes are created during render, in
39
+ // order, into a lightweight parent/children tree (no DOM), and the collection
40
+ // is mutated IN PLACE (unfrozen) so the snapshot handed to the content render
41
+ // fills as registration proceeds. PHASE-8: SSR/hydration coverage is deferred;
42
+ // the path is ported structurally but not yet exercised by tests.
43
+ import type { Key, Node } from '@react-types/shared';
44
+ import { BaseCollection, CollectionNode, Mutable } from './BaseCollection';
45
+ import type { CollectionNodeClass } from './CollectionBuilder';
46
+
47
+ /**
48
+ * A mutable element entry owning an immutable CollectionNode which is copied
49
+ * on write. On the client it wraps a REAL placeholder element in the detached
50
+ * container; during SSR it is a lightweight render-phase tree node (upstream's
51
+ * fake ElementNode, minus the sibling/index bookkeeping the real DOM provides).
52
+ */
53
+ export class ElementNode<T> {
54
+ readonly type: string;
55
+ ownerDocument: Document<T, any>;
56
+ /** The real placeholder element this entry mirrors (client mode). */
57
+ element: Element | null = null;
58
+ /** Ordered children (SSR mode only — client order comes from the DOM). */
59
+ ssrChildren: ElementNode<T>[] | null = null;
60
+ node: CollectionNode<T> | null = null;
61
+ isMutated = true;
62
+
63
+ constructor(type: string, ownerDocument: Document<T, any>) {
64
+ this.type = type;
65
+ this.ownerDocument = ownerDocument;
66
+ }
67
+
68
+ /** SSR-only: children append in render order (octane SSR is single-pass ordered). */
69
+ appendChild(child: ElementNode<T>): void {
70
+ (this.ssrChildren ??= []).push(child);
71
+ }
72
+
73
+ /**
74
+ * Lazily gets a mutable instance of a Node. If the node has already
75
+ * been cloned during this update cycle, it just returns the existing one.
76
+ */
77
+ getMutableNode(): Mutable<CollectionNode<T>> {
78
+ if (!this.isMutated) {
79
+ this.node = this.node!.clone();
80
+ this.isMutated = true;
81
+ }
82
+
83
+ this.ownerDocument.markDirty(this);
84
+ return this.node as Mutable<CollectionNode<T>>;
85
+ }
86
+
87
+ setProps<E extends Element>(
88
+ obj: { [key: string]: any },
89
+ ref: any,
90
+ CollectionNodeClass: CollectionNodeClass<any>,
91
+ rendered?: any,
92
+ render?: (node: Node<T>) => any,
93
+ ): void {
94
+ let node: Mutable<CollectionNode<T>>;
95
+ let { value, textValue, id, ...props } = obj;
96
+ if (this.node == null) {
97
+ node = new CollectionNodeClass(id ?? `react-aria-${++this.ownerDocument.nodeId}`);
98
+ this.node = node;
99
+ this.ownerDocument.markDirty(this);
100
+ } else {
101
+ node = this.getMutableNode();
102
+ }
103
+
104
+ props.ref = ref;
105
+ node.props = props;
106
+ node.rendered = rendered;
107
+ node.render = render;
108
+ node.value = value;
109
+ if (obj['aria-label']) {
110
+ node['aria-label'] = obj['aria-label'];
111
+ }
112
+ node.textValue =
113
+ textValue ||
114
+ (typeof props.children === 'string' ? props.children : '') ||
115
+ obj['aria-label'] ||
116
+ '';
117
+ if (id != null && id !== node.key) {
118
+ throw new Error('Cannot change the id of an item');
119
+ }
120
+
121
+ if (props.colSpan != null) {
122
+ node.colSpan = props.colSpan;
123
+ }
124
+
125
+ this.ownerDocument.queueUpdate();
126
+ }
127
+ }
128
+
129
+ // Node-field assignment used by the walk: clone-on-write only when a computed
130
+ // structural field actually changed, so untouched nodes keep object identity
131
+ // across updates (the caching contract downstream renderers rely on).
132
+ interface WalkFields {
133
+ index: number;
134
+ level: number;
135
+ parentKey: Key | null;
136
+ prevKey: Key | null;
137
+ nextKey: Key | null;
138
+ hasChildNodes: boolean;
139
+ firstChildKey: Key | null;
140
+ lastChildKey: Key | null;
141
+ colIndex: number | null;
142
+ }
143
+
144
+ function fieldsDiffer<T>(node: CollectionNode<T>, f: WalkFields): boolean {
145
+ return (
146
+ node.index !== f.index ||
147
+ node.level !== f.level ||
148
+ node.parentKey !== f.parentKey ||
149
+ node.prevKey !== f.prevKey ||
150
+ node.nextKey !== f.nextKey ||
151
+ node.hasChildNodes !== f.hasChildNodes ||
152
+ node.firstChildKey !== f.firstChildKey ||
153
+ node.lastChildKey !== f.lastChildKey ||
154
+ node.colIndex !== f.colIndex
155
+ );
156
+ }
157
+
158
+ /**
159
+ * The binding-owned Document store. It owns the detached real container the
160
+ * hidden structural copy renders into, the Element → ElementNode registration
161
+ * map, and an immutable Collection instance which is lazily copied on write
162
+ * during updates.
163
+ */
164
+ export class Document<T, C extends BaseCollection<T> = BaseCollection<T>> {
165
+ ownerDocument: Document<T, C> = this;
166
+ isSSR = false;
167
+ nodeId = 0;
168
+ /** SSR double-render guard, ported verbatim (props identity → created node). */
169
+ nodesByProps: WeakMap<object, ElementNode<T>> = new WeakMap();
170
+
171
+ private rootElement: Element | null = null;
172
+ private observer: MutationObserver | null = null;
173
+ private nodesByElement: WeakMap<Element, ElementNode<T>> = new WeakMap();
174
+ private dirtyNodes: Set<ElementNode<T>> = new Set();
175
+ private structureDirty = false;
176
+ /** The first commit must freeze/commit the (possibly empty) initial collection. */
177
+ private needsCommit = true;
178
+
179
+ /** SSR-only render-phase tree root (client order comes from the real DOM). */
180
+ private ssrChildren: ElementNode<T>[] = [];
181
+
182
+ private collection: C;
183
+ private subscriptions: Set<() => void> = new Set();
184
+ private queuedRender = false;
185
+ private inSubscription = false;
186
+
187
+ constructor(collection: C) {
188
+ this.collection = collection;
189
+ }
190
+
191
+ /**
192
+ * The detached, never-attached real container the hidden structural copy
193
+ * portals into. Document order inside it is the collection's source of truth.
194
+ */
195
+ getRootElement(): Element | null {
196
+ if (this.rootElement === null && typeof document !== 'undefined') {
197
+ this.rootElement = document.createElement('div');
198
+ if (typeof MutationObserver !== 'undefined') {
199
+ this.observer = new MutationObserver(() => {
200
+ // Structural change with no ref re-fire (keyed reorder of cached
201
+ // children, removal of an unmounted subtree). Batch like any
202
+ // other mutation and notify subscribers.
203
+ this.structureDirty = true;
204
+ this.queueUpdate();
205
+ });
206
+ this.observer.observe(this.rootElement, {
207
+ childList: true,
208
+ subtree: true,
209
+ // Suspense/Activity hides content via inline `style.display =
210
+ // 'none'` (octane's hideActivityRange, same as React) — upstream
211
+ // models exactly this with the fake `style.display` setter:
212
+ // hidden elements remain in the Document but are removed from
213
+ // the Collection. Observe `style` so those subtrees drop out of
214
+ // the snapshot; the walk excludes `display: none` and nothing
215
+ // else (the `hidden` attribute is NOT a hide signal in either
216
+ // runtime, matching upstream).
217
+ attributes: true,
218
+ attributeFilter: ['style'],
219
+ });
220
+ }
221
+ }
222
+ return this.rootElement;
223
+ }
224
+
225
+ /** SSR-only: creates a render-phase tree node (upstream's `createElement`). */
226
+ createElement(type: string): ElementNode<T> {
227
+ return new ElementNode(type, this);
228
+ }
229
+
230
+ /** SSR-only: top-level nodes append in render order. */
231
+ appendChild(child: ElementNode<T>): void {
232
+ this.ssrChildren.push(child);
233
+ }
234
+
235
+ /**
236
+ * Ref-registration entry point (replaces upstream's fake-element `setProps`
237
+ * receiver): maps the real placeholder element to its ElementNode and
238
+ * applies the item's props to the immutable node copy-on-write.
239
+ */
240
+ setElementProps(
241
+ element: Element,
242
+ props: { [key: string]: any },
243
+ ref: any,
244
+ CollectionNodeClass: CollectionNodeClass<any>,
245
+ rendered?: any,
246
+ render?: (node: Node<T>) => any,
247
+ ): void {
248
+ let entry = this.nodesByElement.get(element);
249
+ if (entry === undefined) {
250
+ entry = new ElementNode(CollectionNodeClass.type, this);
251
+ entry.element = element;
252
+ this.nodesByElement.set(element, entry);
253
+ }
254
+ entry.setProps(props, ref, CollectionNodeClass, rendered, render);
255
+ }
256
+
257
+ markDirty(node: ElementNode<T>): void {
258
+ this.dirtyNodes.add(node);
259
+ }
260
+
261
+ private hasDirty(): boolean {
262
+ return this.dirtyNodes.size > 0 || this.structureDirty || this.needsCommit;
263
+ }
264
+
265
+ // Fold any not-yet-delivered MutationObserver records into the dirty state so
266
+ // render-time reads are consistent with the committed DOM (the async callback
267
+ // covers mutations nothing else reads synchronously).
268
+ private drainMutations(): void {
269
+ if (this.observer !== null && this.observer.takeRecords().length > 0) {
270
+ this.structureDirty = true;
271
+ }
272
+ }
273
+
274
+ /** Finalizes the collection update, updating all nodes and freezing the collection. */
275
+ getCollection(): C {
276
+ // If in a subscription update, return the existing collection.
277
+ // octane's useSyncExternalStore will call getCollection again during
278
+ // render, at which point all the updates will be complete.
279
+ if (this.inSubscription) {
280
+ return this.collection;
281
+ }
282
+
283
+ // Reset queuedRender to false when getCollection is called during render.
284
+ this.queuedRender = false;
285
+
286
+ this.updateCollection();
287
+ return this.collection;
288
+ }
289
+
290
+ updateCollection(): void {
291
+ this.drainMutations();
292
+ if (!this.hasDirty()) {
293
+ return;
294
+ }
295
+
296
+ // Rebuild membership from the current tree. In SSR the collection is
297
+ // mutated IN PLACE (unfrozen, identity-stable) so the snapshot handed to
298
+ // the content render fills as render-phase registration proceeds —
299
+ // upstream's `nextCollection === collection` SSR aliasing. On the client
300
+ // a clone is committed and frozen, and the old snapshot stays immutable.
301
+ let next: C = this.isSSR ? this.collection : this.collection.clone();
302
+ for (let key of [...next.getKeys()]) {
303
+ next.removeNode(key);
304
+ }
305
+
306
+ let topLevel = this.logicalChildren(this.isSSR ? null : this.getRootElement(), null);
307
+ this.walk(next, topLevel, null, 0);
308
+
309
+ next.commit(
310
+ topLevel[0]?.node?.key ?? null,
311
+ topLevel[topLevel.length - 1]?.node?.key ?? null,
312
+ this.isSSR,
313
+ );
314
+
315
+ this.collection = next;
316
+ for (let entry of this.dirtyNodes) {
317
+ entry.isMutated = false;
318
+ }
319
+ this.dirtyNodes.clear();
320
+ this.structureDirty = false;
321
+ this.needsCommit = false;
322
+ }
323
+
324
+ // The logical (registered) children of a container: registered elements in
325
+ // document order; unregistered wrapper elements are transparent and descended
326
+ // through; `display: none` subtrees are excluded (Suspense-hide parity).
327
+ private logicalChildren(
328
+ container: Element | null,
329
+ ssrParent: ElementNode<T> | null,
330
+ ): ElementNode<T>[] {
331
+ if (this.isSSR) {
332
+ return ssrParent === null ? this.ssrChildren : (ssrParent.ssrChildren ?? []);
333
+ }
334
+
335
+ let out: ElementNode<T>[] = [];
336
+ if (container === null) {
337
+ return out;
338
+ }
339
+ for (
340
+ let child = container.firstElementChild;
341
+ child !== null;
342
+ child = child.nextElementSibling
343
+ ) {
344
+ if ((child as HTMLElement).style?.display === 'none') {
345
+ continue;
346
+ }
347
+ let entry = this.nodesByElement.get(child);
348
+ if (entry !== undefined && entry.node !== null) {
349
+ out.push(entry);
350
+ } else {
351
+ out.push(...this.logicalChildren(child, null));
352
+ }
353
+ }
354
+ return out;
355
+ }
356
+
357
+ private walk(
358
+ collection: C,
359
+ entries: ElementNode<T>[],
360
+ parent: ElementNode<T> | null,
361
+ level: number,
362
+ ): void {
363
+ let prev: ElementNode<T> | null = null;
364
+ for (let i = 0; i < entries.length; i++) {
365
+ let entry = entries[i];
366
+ let children = this.logicalChildren(entry.element, entry);
367
+ let node = entry.node!;
368
+
369
+ // colIndex forward propagation, per upstream `updateNode`: a sibling
370
+ // following a colSpan/colIndex-bearing node gets an explicit colIndex.
371
+ let colIndex = node.colIndex;
372
+ let prevNode = prev?.node ?? null;
373
+ if (prevNode !== null && (prevNode.colSpan != null || prevNode.colIndex != null)) {
374
+ colIndex = (prevNode.colIndex ?? prevNode.index) + (prevNode.colSpan ?? 1);
375
+ }
376
+
377
+ let fields: WalkFields = {
378
+ index: i,
379
+ level,
380
+ parentKey: parent?.node?.key ?? null,
381
+ prevKey: prev?.node?.key ?? null,
382
+ nextKey: entries[i + 1]?.node?.key ?? null,
383
+ hasChildNodes: children.length > 0,
384
+ firstChildKey: children[0]?.node?.key ?? null,
385
+ lastChildKey: children[children.length - 1]?.node?.key ?? null,
386
+ colIndex,
387
+ };
388
+
389
+ if (entry.isMutated || fieldsDiffer(node, fields)) {
390
+ let mutable = entry.getMutableNode();
391
+ mutable.index = fields.index;
392
+ mutable.level = fields.level;
393
+ mutable.parentKey = fields.parentKey;
394
+ mutable.prevKey = fields.prevKey;
395
+ mutable.nextKey = fields.nextKey;
396
+ mutable.hasChildNodes = fields.hasChildNodes;
397
+ mutable.firstChildKey = fields.firstChildKey;
398
+ mutable.lastChildKey = fields.lastChildKey;
399
+ mutable.colIndex = fields.colIndex;
400
+ }
401
+
402
+ collection.addNode(entry.node!);
403
+
404
+ // Upstream level rule: only an `item` parent increases depth.
405
+ this.walk(collection, children, entry, level + (node.type === 'item' ? 1 : 0));
406
+ prev = entry;
407
+ }
408
+ }
409
+
410
+ queueUpdate(): void {
411
+ this.drainMutations();
412
+ if (!this.hasDirty() || this.queuedRender) {
413
+ return;
414
+ }
415
+
416
+ // Only trigger subscriptions once during an update, when the first item changes.
417
+ // useSyncExternalStore will call getCollection immediately, to check whether the
418
+ // snapshot changed. If so, a render is queued after the current commit finishes.
419
+ // We track whether getCollection is called in a subscription, and once it is
420
+ // called during render, we reset queuedRender back to false.
421
+ this.queuedRender = true;
422
+ this.inSubscription = true;
423
+
424
+ // Clone the collection to ensure the subscriber sees a changed snapshot and
425
+ // queues a render. getCollection will be called again during that render, at
426
+ // which point all the updates are complete and the rebuilt collection returns.
427
+ if (!this.isSSR) {
428
+ this.collection = this.collection.clone();
429
+ }
430
+
431
+ for (let fn of this.subscriptions) {
432
+ fn();
433
+ }
434
+
435
+ this.inSubscription = false;
436
+ }
437
+
438
+ subscribe(fn: () => void): () => boolean {
439
+ this.subscriptions.add(fn);
440
+ return (): boolean => this.subscriptions.delete(fn);
441
+ }
442
+
443
+ resetAfterSSR(): void {
444
+ if (this.isSSR) {
445
+ this.isSSR = false;
446
+ this.ssrChildren = [];
447
+ this.nodeId = 0;
448
+ this.needsCommit = true;
449
+ }
450
+ }
451
+ }
@@ -0,0 +1,84 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/collections/Hidden.tsx).
2
+ // octane adaptations (docs/aria-migration-plan.md §2a): upstream hides the
3
+ // structural copy inside a real `<template>` element (and monkey-patches
4
+ // HTMLTemplateElement for React hydration — dropped, React-specific). octane's
5
+ // hidden copy instead portals into a DETACHED, never-attached real container:
6
+ // the content renders and updates normally but is not in the document, so it is
7
+ // invisible and outside the accessible tree — the same "render structural copy
8
+ // only" contract. `CollectionBuilder` passes the Document's own detached root
9
+ // as `target` so ref registration and the real-DOM walk observe the copy;
10
+ // standalone `Hidden` uses a private detached container. During SSR the
11
+ // children render in place WITHOUT a portal (octane SSR has none): collection
12
+ // components take the render-phase SSR registration path and emit no HTML, and
13
+ // hideable components render null. `.tsx` → `.ts` (JSX → createElement), no
14
+ // forwardRef (octane refs are props), S()/subSlot component-slot convention.
15
+ import { createContext, createElement, createPortal, Fragment, useContext, useState } from 'octane';
16
+ import { useIsSSR } from '../ssr/SSRProvider';
17
+
18
+ import { S, subSlot } from '../internal';
19
+
20
+ export const HiddenContext = createContext<boolean>(false);
21
+
22
+ export function Hidden(props: { children: any; target?: Element | null }): any {
23
+ const slot = S('Hidden');
24
+ let isHidden = useContext(HiddenContext);
25
+ let isSSR = useIsSSR(subSlot(slot, 'ssr'));
26
+ // A private detached container for standalone use (never attached to the
27
+ // document). Client-only: SSR renders no portal.
28
+ let [ownContainer] = useState(
29
+ () => (typeof document !== 'undefined' ? document.createElement('div') : null),
30
+ subSlot(slot, 'container'),
31
+ );
32
+
33
+ if (isHidden) {
34
+ // Don't hide again if we are already hidden.
35
+ return props.children;
36
+ }
37
+
38
+ let children = createElement(HiddenContext.Provider, {
39
+ value: true,
40
+ children: props.children,
41
+ });
42
+
43
+ if (isSSR) {
44
+ // Render-phase SSR registration path: hidden collection content emits no
45
+ // HTML of its own (see useSSRCollectionNode), so nothing user-visible is
46
+ // serialized. PHASE-8: SSR/hydration coverage deferred.
47
+ return children;
48
+ }
49
+
50
+ // Upstream renders the structural copy inside a real in-DOM `<template>`
51
+ // element (children redirected into its inert `.content` fragment), so
52
+ // react-aria's serialized DOM contains an empty `<template></template>`
53
+ // marker. Our copy lives in the detached container instead, but we render
54
+ // the same inert placeholder so consumer-observable DOM (and the
55
+ // differential byte-compare) matches react-aria exactly.
56
+ return createElement(
57
+ Fragment,
58
+ null,
59
+ createElement('template', null),
60
+ createPortal(children, (props.target ?? ownContainer) as Element),
61
+ );
62
+ }
63
+
64
+ /** Creates a component that returns null if it is in a hidden subtree. */
65
+ export function createHideableComponent<T, P = {}>(
66
+ fn: (props: P, ref: any) => any,
67
+ ): (props: P & { ref?: any }) => any {
68
+ // octane adaptation: no forwardRef — the ref arrives as a normal prop and is
69
+ // forwarded to `fn` in upstream's (props, ref) positional shape.
70
+ let Wrapper = (props: P & { ref?: any }) => {
71
+ let isHidden = useContext(HiddenContext);
72
+ if (isHidden) {
73
+ return null;
74
+ }
75
+
76
+ return fn(props, (props as any).ref);
77
+ };
78
+ return Wrapper;
79
+ }
80
+
81
+ /** Returns whether the component is in a hidden subtree. */
82
+ export function useIsHidden(): boolean {
83
+ return useContext(HiddenContext);
84
+ }
@@ -0,0 +1,103 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/collections/useCachedChildren.ts).
2
+ // octane adaptations: `cloneElement` from 'octane' (descriptors carry
3
+ // .props/.key like React elements, so the caching walk is verbatim); React
4
+ // element types → `any`; public-hook slot threading (splitSlot/subSlot) with
5
+ // explicit dependency arrays.
6
+ import type { Key } from '@react-types/shared';
7
+ import { cloneElement, useMemo } from 'octane';
8
+
9
+ import { S, splitSlot, subSlot } from '../internal';
10
+
11
+ export interface CachedChildrenOptions<T> {
12
+ /** Item objects in the collection. */
13
+ items?: Iterable<T>;
14
+ /** The contents of the collection. */
15
+ children?: any | ((item: T) => any);
16
+ /** Values that should invalidate the item cache when using dynamic collections. */
17
+ dependencies?: ReadonlyArray<any>;
18
+ /** A scope to prepend to all child item ids to ensure they are unique. */
19
+ idScope?: Key;
20
+ /** Whether to add `id` and `value` props to all child items. */
21
+ addIdAndValue?: boolean;
22
+ }
23
+
24
+ /**
25
+ * Maps over a list of items and renders octane elements for them. Each rendered item is
26
+ * cached based on object identity, and keys are generated from the `key` or `id` property.
27
+ */
28
+ export function useCachedChildren<T>(props: CachedChildrenOptions<T>): any;
29
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
30
+ export function useCachedChildren<T>(
31
+ props: CachedChildrenOptions<T>,
32
+ slot: symbol | undefined,
33
+ ): any;
34
+ export function useCachedChildren(...args: any[]): any {
35
+ const [user, slotArg] = splitSlot(args);
36
+ const slot = slotArg ?? S('useCachedChildren');
37
+ const props = user[0] as CachedChildrenOptions<any>;
38
+ let { children, items, idScope, addIdAndValue, dependencies = [] } = props;
39
+
40
+ // In development, invalidate when the children function updates (e.g. HMR).
41
+ let childrenString = useMemo(
42
+ () =>
43
+ process.env.NODE_ENV !== 'production' && typeof children === 'function'
44
+ ? children.toString()
45
+ : undefined,
46
+ [children],
47
+ subSlot(slot, 'childrenString'),
48
+ );
49
+
50
+ // Invalidate the cache whenever dependencies change.
51
+ let cache = useMemo(
52
+ () => new WeakMap(),
53
+ [...dependencies, childrenString],
54
+ subSlot(slot, 'cache'),
55
+ );
56
+
57
+ return useMemo(
58
+ () => {
59
+ if (items && typeof children === 'function') {
60
+ let res: any[] = [];
61
+ for (let item of items) {
62
+ let cacheKey = isWeakKey(item) ? item : null;
63
+ let rendered = cacheKey ? cache.get(cacheKey) : null;
64
+ if (!rendered) {
65
+ rendered = children(item);
66
+ let id = rendered.props.id ?? item?.key ?? item?.id;
67
+ if (idScope != null && rendered.props.id == null && id != null) {
68
+ id = idScope + ':' + id;
69
+ }
70
+
71
+ // If no id is inferred from data, use the index as the key.
72
+ // An id will be generated by the collection document.
73
+ let key = id ?? res.length;
74
+
75
+ // Note: only works if wrapped Item passes through id...
76
+ rendered = cloneElement(rendered, addIdAndValue ? { key, id, value: item } : { key });
77
+ if (cacheKey) {
78
+ cache.set(cacheKey, rendered);
79
+ }
80
+ }
81
+ res.push(rendered);
82
+ }
83
+ return res;
84
+ } else if (typeof children !== 'function') {
85
+ return children;
86
+ }
87
+ },
88
+ [children, items, cache, idScope, addIdAndValue],
89
+ subSlot(slot, 'result'),
90
+ );
91
+ }
92
+
93
+ function isWeakKey(value: any): value is WeakKey {
94
+ switch (typeof value) {
95
+ case 'object':
96
+ return value != null;
97
+ case 'function':
98
+ case 'symbol':
99
+ return true;
100
+ default:
101
+ return false;
102
+ }
103
+ }