@octanejs/aria 0.0.4 → 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.
- package/package.json +7 -5
- package/src/autocomplete/useAutocomplete.ts +656 -0
- package/src/collections/BaseCollection.ts +374 -0
- package/src/collections/CollectionBuilder.ts +343 -0
- package/src/collections/Document.ts +451 -0
- package/src/collections/Hidden.ts +84 -0
- package/src/collections/useCachedChildren.ts +103 -0
- package/src/components/Autocomplete.ts +124 -0
- package/src/components/Breadcrumbs.ts +170 -0
- package/src/components/Button.ts +209 -0
- package/src/components/Checkbox.ts +593 -0
- package/src/components/Collection.ts +301 -0
- package/src/components/ComboBox.ts +477 -0
- package/src/components/Dialog.ts +195 -0
- package/src/components/Disclosure.ts +322 -0
- package/src/components/DragAndDrop.ts +177 -0
- package/src/components/FieldError.ts +79 -0
- package/src/components/Form.ts +65 -0
- package/src/components/GridList.ts +1099 -0
- package/src/components/Group.ts +146 -0
- package/src/components/Header.ts +32 -0
- package/src/components/Heading.ts +45 -0
- package/src/components/Input.ts +139 -0
- package/src/components/Keyboard.ts +23 -0
- package/src/components/Label.ts +30 -0
- package/src/components/Link.ts +137 -0
- package/src/components/ListBox.ts +886 -0
- package/src/components/Menu.ts +685 -0
- package/src/components/Meter.ts +100 -0
- package/src/components/Modal.ts +352 -0
- package/src/components/NumberField.ts +221 -0
- package/src/components/OverlayArrow.ts +117 -0
- package/src/components/Popover.ts +385 -0
- package/src/components/ProgressBar.ts +112 -0
- package/src/components/RadioGroup.ts +542 -0
- package/src/components/SearchField.ts +208 -0
- package/src/components/Select.ts +505 -0
- package/src/components/SelectionIndicator.ts +52 -0
- package/src/components/Separator.ts +97 -0
- package/src/components/SharedElementTransition.ts +209 -0
- package/src/components/Slider.ts +472 -0
- package/src/components/Switch.ts +449 -0
- package/src/components/Tabs.ts +675 -0
- package/src/components/TagGroup.ts +443 -0
- package/src/components/Text.ts +27 -0
- package/src/components/TextArea.ts +92 -0
- package/src/components/TextField.ts +200 -0
- package/src/components/ToggleButton.ts +147 -0
- package/src/components/ToggleButtonGroup.ts +106 -0
- package/src/components/Toolbar.ts +74 -0
- package/src/components/Tooltip.ts +249 -0
- package/src/components/index.ts +302 -0
- package/src/components/useDragAndDrop.ts +164 -0
- package/src/components/utils.ts +521 -0
- package/src/intl/autocomplete/ar-AE.json +3 -0
- package/src/intl/autocomplete/bg-BG.json +3 -0
- package/src/intl/autocomplete/cs-CZ.json +3 -0
- package/src/intl/autocomplete/da-DK.json +3 -0
- package/src/intl/autocomplete/de-DE.json +3 -0
- package/src/intl/autocomplete/el-GR.json +3 -0
- package/src/intl/autocomplete/en-US.json +3 -0
- package/src/intl/autocomplete/es-ES.json +3 -0
- package/src/intl/autocomplete/et-EE.json +3 -0
- package/src/intl/autocomplete/fi-FI.json +3 -0
- package/src/intl/autocomplete/fr-FR.json +3 -0
- package/src/intl/autocomplete/he-IL.json +3 -0
- package/src/intl/autocomplete/hr-HR.json +3 -0
- package/src/intl/autocomplete/hu-HU.json +3 -0
- package/src/intl/autocomplete/index.ts +75 -0
- package/src/intl/autocomplete/it-IT.json +3 -0
- package/src/intl/autocomplete/ja-JP.json +3 -0
- package/src/intl/autocomplete/ko-KR.json +3 -0
- package/src/intl/autocomplete/lt-LT.json +3 -0
- package/src/intl/autocomplete/lv-LV.json +3 -0
- package/src/intl/autocomplete/nb-NO.json +3 -0
- package/src/intl/autocomplete/nl-NL.json +3 -0
- package/src/intl/autocomplete/pl-PL.json +3 -0
- package/src/intl/autocomplete/pt-BR.json +3 -0
- package/src/intl/autocomplete/pt-PT.json +3 -0
- package/src/intl/autocomplete/ro-RO.json +3 -0
- package/src/intl/autocomplete/ru-RU.json +3 -0
- package/src/intl/autocomplete/sk-SK.json +3 -0
- package/src/intl/autocomplete/sl-SI.json +3 -0
- package/src/intl/autocomplete/sr-SP.json +3 -0
- package/src/intl/autocomplete/sv-SE.json +3 -0
- package/src/intl/autocomplete/tr-TR.json +3 -0
- package/src/intl/autocomplete/uk-UA.json +3 -0
- package/src/intl/autocomplete/zh-CN.json +3 -0
- package/src/intl/autocomplete/zh-TW.json +3 -0
- package/src/stately/autocomplete/useAutocompleteState.ts +85 -0
- package/src/utils/animation.ts +149 -0
- package/src/utils/useLoadMoreSentinel.ts +78 -0
- 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
|
+
}
|