@rivetjs/cdk 0.1.0-alpha.0 → 0.1.0-alpha.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.
- package/_adapters/selection.d.ts +13 -2
- package/_adapters/selection.js +39 -15
- package/button/index.jsx +5 -3
- package/collections/components.d.ts +36 -1
- package/collections/components.jsx +224 -16
- package/collections/nodes.d.ts +9 -1
- package/collections/nodes.js +26 -0
- package/collections/row-stamps.d.ts +17 -0
- package/collections/row-stamps.js +108 -0
- package/collections/server-dev.d.ts +1 -0
- package/collections/server-dev.js +2 -0
- package/collections/server-off.d.ts +1 -0
- package/collections/server-off.js +2 -0
- package/collections/server-on.d.ts +1 -0
- package/collections/server-on.jsx +10 -0
- package/drag-drop/directives/drag.d.ts +16 -0
- package/drag-drop/directives/drag.js +28 -0
- package/drag-drop/drag.jsx +6 -4
- package/drag-drop/lazy-refs.js +15 -4
- package/grid-list/behaviour.d.ts +6 -0
- package/grid-list/behaviour.js +25 -7
- package/grid-list/index.d.ts +3 -1
- package/grid-list/index.jsx +60 -24
- package/interactions/press.jsx +2 -1
- package/interactions/utils.d.ts +2 -3
- package/interactions/utils.js +8 -7
- package/listbox/behaviour.jsx +37 -14
- package/listbox/index.jsx +65 -29
- package/menu/index.jsx +38 -7
- package/package.json +4 -4
- package/portal/index.d.ts +8 -0
- package/portal/index.jsx +8 -1
- package/radio-group/index.jsx +4 -1
- package/selection/keyed.js +6 -16
- package/selection/selectable-collection.js +42 -17
- package/sidebar/index.jsx +5 -4
- package/state/menu.d.ts +3 -0
- package/state/menu.js +1 -0
- package/table/index.jsx +512 -168
- package/tree/index.jsx +26 -4
- package/utils/data-attrs.d.ts +4 -2
- package/utils/data-attrs.jsx +27 -3
- package/utils/index.d.ts +1 -15
- package/utils/index.js +5 -102
- package/utils/link-state.d.ts +6 -0
- package/utils/link-state.js +6 -0
- package/utils/links.d.ts +1 -6
- package/utils/links.jsx +2 -4
- package/utils/transitions.d.ts +15 -0
- package/utils/transitions.js +104 -0
- package/virtualizer/behaviour.d.ts +35 -4
- package/virtualizer/behaviour.js +131 -9
- package/virtualizer/collection-renderer.d.ts +62 -0
- package/virtualizer/collection-renderer.jsx +190 -0
- package/virtualizer/engine.d.ts +1 -1
- package/virtualizer/index.d.ts +1 -0
- package/virtualizer/index.jsx +1 -0
- package/virtualizer/list-layout.js +4 -2
- package/virtualizer/state.js +12 -2
- package/virtualizer/virtualizer.d.ts +8 -6
- package/virtualizer/virtualizer.jsx +4 -1
package/_adapters/selection.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SelectionManager, type FocusStrategy, type Key, type KeyboardDelegate, type MultipleSelectionState, type Orientation } from "./collections.js";
|
|
1
|
+
import { SelectionManager, type Collection, type FocusStrategy, type Key, type KeyboardDelegate, type LayoutDelegate, type MultipleSelectionState, type Orientation } from "./collections.js";
|
|
2
2
|
import { type PressEvent } from "./interactions.js";
|
|
3
3
|
export type LinkBehavior = "action" | "selection" | "override" | "none";
|
|
4
4
|
/** Static (serializable) options of a selectable collection + its items. */
|
|
@@ -27,6 +27,17 @@ export interface SelectableCollection extends MultipleSelectionState {
|
|
|
27
27
|
readonly onAction?: ((key: Key) => void) | undefined;
|
|
28
28
|
/** Sections with their own selection (RAC `MenuSection selectionMode`), by section key. */
|
|
29
29
|
readonly groups?: Readonly<Record<string, MultipleSelectionState>> | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Rivet: a virtualized collection (RAC `Virtualizer`, only a window of items is rendered): the handlers navigate its
|
|
32
|
+
* built collection (an accessor) instead of the rendered DOM, page keys use the virtualizer's layout delegate, focus
|
|
33
|
+
* waits for the focused item to render and the scroll position is the virtualizer's.
|
|
34
|
+
*/
|
|
35
|
+
readonly virtual?: VirtualCollection | undefined;
|
|
36
|
+
}
|
|
37
|
+
/** A virtualized collection's built collection and layout (see `SelectableCollection.virtual`). */
|
|
38
|
+
export interface VirtualCollection {
|
|
39
|
+
readonly collection: () => Collection;
|
|
40
|
+
readonly layoutDelegate?: LayoutDelegate | undefined;
|
|
30
41
|
}
|
|
31
42
|
/** The manager selecting `key`: its section's own (GroupSelectionManager) or the collection's. */
|
|
32
43
|
export declare function groupManager(list: SelectableCollection, root: HTMLElement, manager: SelectionManager, key: Key): SelectionManager;
|
|
@@ -40,7 +51,7 @@ export interface ItemPressHooks {
|
|
|
40
51
|
/** Called after the item performed its action (menu: close). */
|
|
41
52
|
onAction?(list: SelectableCollection, key: Key, root: HTMLElement): void;
|
|
42
53
|
}
|
|
43
|
-
/** Selection manager over the rendered collection under `root
|
|
54
|
+
/** Selection manager over the rendered collection under `root` (a virtualized collection: its built collection). */
|
|
44
55
|
export declare const managerOf: (list: MultipleSelectionState, root: HTMLElement) => SelectionManager;
|
|
45
56
|
/** Keyboard delegate of `root` (useSelectableList's default `ListKeyboardDelegate`). */
|
|
46
57
|
export declare function delegateOf(list: SelectableCollection, root: HTMLElement, manager: SelectionManager): KeyboardDelegate;
|
package/_adapters/selection.js
CHANGED
|
@@ -21,8 +21,8 @@ export function groupManager(list, root, manager, key) {
|
|
|
21
21
|
const group = section && root.contains(section) ? list.groups[section.getAttribute("data-key") ?? ""] : undefined;
|
|
22
22
|
return group ? new GroupSelectionManager(manager, group) : manager;
|
|
23
23
|
}
|
|
24
|
-
/** Selection manager over the rendered collection under `root
|
|
25
|
-
export const managerOf = (list, root) => new SelectionManager(domCollection(root), list);
|
|
24
|
+
/** Selection manager over the rendered collection under `root` (a virtualized collection: its built collection). */
|
|
25
|
+
export const managerOf = (list, root) => new SelectionManager(list.virtual?.collection() ?? domCollection(root), list);
|
|
26
26
|
/** Keyboard delegate of `root` (useSelectableList's default `ListKeyboardDelegate`). */
|
|
27
27
|
export function delegateOf(list, root, manager) {
|
|
28
28
|
return new ListKeyboardDelegate({
|
|
@@ -34,6 +34,7 @@ export function delegateOf(list, root, manager) {
|
|
|
34
34
|
layout: list.options.layout ?? "stack",
|
|
35
35
|
orientation: list.options.orientation ?? "vertical",
|
|
36
36
|
direction: dirOf(root),
|
|
37
|
+
layoutDelegate: list.virtual?.layoutDelegate,
|
|
37
38
|
});
|
|
38
39
|
}
|
|
39
40
|
const selectOnFocusOf = (list) => list.options.selectOnFocus ?? untrack(list.selectionBehavior) === "replace";
|
|
@@ -51,23 +52,43 @@ export function syncFocus(list, root, previous, force = false) {
|
|
|
51
52
|
const virtual = !!list.options.shouldUseVirtualFocus;
|
|
52
53
|
if (focused && key != null && (key !== previous || force)) {
|
|
53
54
|
const element = getItemElement(root, key);
|
|
54
|
-
if (element)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (modality === "keyboard" || force) {
|
|
61
|
-
scrollIntoView(root, element);
|
|
62
|
-
if (modality !== "virtual")
|
|
63
|
-
scrollIntoViewport(element, { containingElement: root });
|
|
64
|
-
}
|
|
65
|
-
}
|
|
55
|
+
if (element)
|
|
56
|
+
focusItem(virtual, root, element, force);
|
|
57
|
+
// Virtualized: the item renders once the virtualizer persists the new focused key (RA: useSelectableItem's
|
|
58
|
+
// effect after that render) — focus and scroll it in when it appears.
|
|
59
|
+
else if (list.virtual)
|
|
60
|
+
whenRendered(list, root, key, force);
|
|
66
61
|
}
|
|
67
62
|
// The focused key went away (item removed): focus the collection itself.
|
|
68
63
|
if (!virtual && focused && key == null && previous != null && root.isConnected)
|
|
69
64
|
focusSafely(root);
|
|
70
65
|
}
|
|
66
|
+
function focusItem(virtual, root, element, force) {
|
|
67
|
+
if (virtual)
|
|
68
|
+
moveVirtualFocus(element);
|
|
69
|
+
else if (document.activeElement !== element)
|
|
70
|
+
focusSafely(element);
|
|
71
|
+
const modality = getInteractionModality();
|
|
72
|
+
if (modality === "keyboard" || force) {
|
|
73
|
+
scrollIntoView(root, element);
|
|
74
|
+
if (modality !== "virtual")
|
|
75
|
+
scrollIntoViewport(element, { containingElement: root });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Virtualized collections: focus the item of `key` after the render that adds it (a microtask, then a few frames). */
|
|
79
|
+
function whenRendered(list, root, key, force) {
|
|
80
|
+
let frames = 0;
|
|
81
|
+
const attempt = () => {
|
|
82
|
+
if (untrack(list.focusedKey) !== key || !untrack(list.isFocused))
|
|
83
|
+
return;
|
|
84
|
+
const element = getItemElement(root, key);
|
|
85
|
+
if (element)
|
|
86
|
+
focusItem(!!list.options.shouldUseVirtualFocus, root, element, force);
|
|
87
|
+
else if (frames++ < 3)
|
|
88
|
+
requestAnimationFrame(attempt);
|
|
89
|
+
};
|
|
90
|
+
queueMicrotask(attempt);
|
|
91
|
+
}
|
|
71
92
|
// Typeahead (useTypeSelect): `./type-select.ts` (its own module, so a closed Select trigger's typeahead does not
|
|
72
93
|
// load this collection adapter).
|
|
73
94
|
export { typeSelectKeyDown, _resetTypeSelect } from "./type-select.js";
|
|
@@ -593,6 +614,9 @@ export function collectionPointerOver(list, e, root = e.currentTarget) {
|
|
|
593
614
|
const scrollPositions = new WeakMap();
|
|
594
615
|
/** scroll on the collection root (remembered, restored when focus re-enters). */
|
|
595
616
|
export function collectionScroll(list, e) {
|
|
617
|
+
// RA: not for a virtualized collection (the virtualizer owns its scroll position).
|
|
618
|
+
if (list.virtual)
|
|
619
|
+
return;
|
|
596
620
|
scrollPositions.set(list, { top: e.currentTarget.scrollTop, left: e.currentTarget.scrollLeft });
|
|
597
621
|
}
|
|
598
622
|
/** focusin on the collection root: item focus → focused key; entering → first / last / selected item. */
|
|
@@ -632,7 +656,7 @@ export function collectionFocusIn(list, e, root = e.currentTarget) {
|
|
|
632
656
|
// selection-follows-focus collection selects it, as RAC's first focus does.
|
|
633
657
|
if (selectOnFocus && modality !== "pointer" && manager.isEmpty && !manager.isDisabled(manager.focusedKey))
|
|
634
658
|
manager.replaceSelection(manager.focusedKey);
|
|
635
|
-
const saved = scrollPositions.get(list);
|
|
659
|
+
const saved = list.virtual ? undefined : scrollPositions.get(list);
|
|
636
660
|
if (saved) {
|
|
637
661
|
root.scrollTop = saved.top;
|
|
638
662
|
root.scrollLeft = saved.left;
|
package/button/index.jsx
CHANGED
|
@@ -149,10 +149,12 @@ export function Button(inProps) {
|
|
|
149
149
|
ref = mergeRefValues(ref, responderRef);
|
|
150
150
|
}
|
|
151
151
|
// RA useButton → useFocusable: FocusableContext (TooltipTrigger) props / handlers / ref reach the <button>.
|
|
152
|
-
// `isDisabled` is read
|
|
153
|
-
//
|
|
152
|
+
// `isDisabled` is never read here: an accessor / live one goes to the merge as an accessor (gated at event time and
|
|
153
|
+
// in the attribute binding). A read here would make the Button's enclosing region re-create it on its first change
|
|
154
|
+
// on a resumed page (Calendar page buttons dropped focus; a live `isDisabled` under a TooltipTrigger threw).
|
|
154
155
|
const focusable = useContext(FocusableContext);
|
|
155
|
-
const
|
|
156
|
+
const disabledAt = focusable ? liveAt(props, "isDisabled") : undefined;
|
|
157
|
+
const [focusableMerged, focusableRef, focusAttrs] = mergeFocusableContext(props, focusable, typeof disabledAt === "function" ? disabledAt : !!disabledAt);
|
|
156
158
|
props = focusableMerged;
|
|
157
159
|
ref = mergeRefValues(ref, focusableRef);
|
|
158
160
|
// Live props (P5): with a live prop the state / ARIA props are read through a narrow view (getters unread, read in
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Context } from "@rivetjs/reactivity";
|
|
2
2
|
import type { RefProp } from "@rivetjs/runtime/jsx-runtime";
|
|
3
|
+
import { type RowBox } from "./row-stamps.js";
|
|
3
4
|
import { BaseCollection, type CollectionNodeClass } from "./base.js";
|
|
4
5
|
import type { Collection as ICollection, Key, LayoutDelegate, Node, SelectionBehavior, SelectionMode } from "./types.js";
|
|
5
6
|
export type { CollectionNodeClass, CollectionNodeSpec } from "./base.js";
|
|
@@ -53,6 +54,14 @@ export declare function isCollectionProbe(): boolean;
|
|
|
53
54
|
* popovers). Server: built on first read. Browser: rebuilt when a signal the content reads changes.
|
|
54
55
|
*/
|
|
55
56
|
export declare function collectionOf<C extends BaseCollection<any> = BaseCollection<any>>(content: unknown, createCollection?: () => C): () => C;
|
|
57
|
+
/**
|
|
58
|
+
* Rivet: the built collection of a virtualized host's `content` (RAC: a collection inside a `Virtualizer` renders
|
|
59
|
+
* from its built collection, only the visible window has parts). One `collectionOf` computed per `instance` (a plain
|
|
60
|
+
* object the host creates in its body: serialized as data, so a resumed page rebuilds the computed from `content` on
|
|
61
|
+
* its first read). Created detached from the reading owner: the first read may be a binding that re-runs (the
|
|
62
|
+
* virtualizer's rows), which would dispose a computed it owns.
|
|
63
|
+
*/
|
|
64
|
+
export declare function builtCollectionOf<C extends BaseCollection<any> = BaseCollection<any>>(instance: object, content: unknown, createCollection?: () => C): C;
|
|
56
65
|
export interface CollectionBuilderProps<C extends BaseCollection<any>> {
|
|
57
66
|
/** The collection content (plain JSX children are lazy). */
|
|
58
67
|
content: unknown;
|
|
@@ -157,6 +166,11 @@ export interface CollectionRootProps {
|
|
|
157
166
|
idScope?: Key | undefined;
|
|
158
167
|
/** A set of keys for items that should always be persisted in the DOM (virtualizers). */
|
|
159
168
|
persistedKeys?: Set<Key> | null | undefined;
|
|
169
|
+
/**
|
|
170
|
+
* Rivet: the host's focused key (an accessor, e.g. the selection state's `focusedKey` signal): virtualizers keep it
|
|
171
|
+
* rendered (RAC `usePersistedKeys(focusedKey)`; a signal, not a set, so it resumes without a closure over the host state).
|
|
172
|
+
*/
|
|
173
|
+
focusedKey?: (() => Key | null) | undefined;
|
|
160
174
|
/** The scroll container (virtualizers). */
|
|
161
175
|
scrollRef?: unknown;
|
|
162
176
|
renderDropIndicator?: CollectionBranchProps["renderDropIndicator"];
|
|
@@ -172,6 +186,11 @@ export interface CollectionRenderer {
|
|
|
172
186
|
CollectionRoot: (props: CollectionRootProps) => unknown;
|
|
173
187
|
/** Renders the child items of a branch. */
|
|
174
188
|
CollectionBranch: (props: CollectionBranchProps) => unknown;
|
|
189
|
+
/**
|
|
190
|
+
* Rivet: the host forwards its scroll container's `scroll` events here (virtualizers: the engine follows the scroll
|
|
191
|
+
* position; RAC's useScrollView listens on `scrollRef` itself, a resumed page needs the host element's handler).
|
|
192
|
+
*/
|
|
193
|
+
onScroll?: ((event: Event) => void) | undefined;
|
|
175
194
|
}
|
|
176
195
|
/** A built collection, an accessor of one, or null (live mode). */
|
|
177
196
|
export type CollectionInput = ICollection<Node<unknown>> | (() => ICollection<Node<unknown>>) | null | undefined;
|
|
@@ -189,7 +208,23 @@ export declare function sameRender(a: Node<unknown> | null | undefined, b: Node<
|
|
|
189
208
|
* node renders the same (`sameRender`) and re-renders it when its render inputs changed, or when `depends` (read in the
|
|
190
209
|
* region) changes: a part whose content renders from its parent's value (TreeItemContent).
|
|
191
210
|
*/
|
|
192
|
-
export declare function renderNodeAt(collection: () => ICollection<Node<unknown>> | null | undefined, key: () => Key, depends?: () => unknown): unknown;
|
|
211
|
+
export declare function renderNodeAt(collection: () => ICollection<Node<unknown>> | null | undefined, key: () => Key | RowBox, depends?: () => unknown): unknown;
|
|
212
|
+
/**
|
|
213
|
+
* Server baseline of a resumed For over collection keys (ROW-RERENDER): `_regionState` with the stamps of the rows the server
|
|
214
|
+
* renders (`row-stamps.ts`) when the collection is an accessor whose build read signals (a static collection never rebuilds:
|
|
215
|
+
* null), recorded with the For's region; none (no slot) for the children of a section / table row (`parentType`). Called by the server and the browser alike (one `_regionState` slot per accessor
|
|
216
|
+
* collection, so the region's other slots keep their positions); the browser gets the server's value back when it
|
|
217
|
+
* re-creates that region (adopting the rows), null otherwise.
|
|
218
|
+
*/
|
|
219
|
+
export declare function serverRows(collection: CollectionInput, keys: () => readonly Key[], parentType?: string | undefined): string | null;
|
|
220
|
+
/**
|
|
221
|
+
* The keys of a resumed For over collection keys (`renderNodeAt` rows), given the server baseline (`serverRows`): an adopted
|
|
222
|
+
* server row keeps its key while its node renders like the server's (stamp in the baseline; then `sameRender` against the
|
|
223
|
+
* last node checked), else its key is boxed (`RowBox`, one box per key for good) — a key the adopted list does not know, so
|
|
224
|
+
* the For re-creates that row (rows read `rowKey(key())`). Rows the server did not render get a box too (created anyway).
|
|
225
|
+
* Without a baseline: `keys` itself.
|
|
226
|
+
*/
|
|
227
|
+
export declare function resumedKeys(collection: CollectionInput, base: string | null, keys: () => readonly Key[]): () => readonly (Key | RowBox)[];
|
|
193
228
|
/** RAC `DefaultCollectionRenderer`: built collections render node by node; live mode renders content in place. */
|
|
194
229
|
export declare const DefaultCollectionRenderer: CollectionRenderer;
|
|
195
230
|
/** RAC `CollectionRendererContext` (Virtualizer provides its own renderer). */
|
|
@@ -29,11 +29,13 @@
|
|
|
29
29
|
// Deviation: `dependencies` is accepted and ignored (signals re-render; no item cache to invalidate).
|
|
30
30
|
// Deviation: React hook names → Rivet (`useIsHidden` → `isHidden`, `usePersistedKeys` →
|
|
31
31
|
// `persistedKeys`, `useCachedChildren` → `collectionChildren`).
|
|
32
|
-
import { _observer, _reobserve, _scoped, computed, createContext, provide, runWithOwner, untrack, useContext, } from "@rivetjs/reactivity";
|
|
32
|
+
import { _observer, _regionState, _reobserve, _scoped, _trackReads, computed, createContext, provide, runWithOwner, untrack, useContext, } from "@rivetjs/reactivity";
|
|
33
33
|
import { For, Provider } from "@rivetjs/runtime/dom";
|
|
34
34
|
import { _rUnrecorded, resumableContext } from "@rivetjs/runtime/resume-format";
|
|
35
35
|
import { isSlot } from "@rivetjs/runtime/slot";
|
|
36
36
|
import { HiddenContext } from "./hidden.js";
|
|
37
|
+
import { domRowItem } from "#server";
|
|
38
|
+
import { rowKey, stampSet, stamp } from "./row-stamps.js";
|
|
37
39
|
import { hasLive } from "../utils/live.jsx";
|
|
38
40
|
import { mergeProps as mergeLive, splitProps } from "@rivetjs/runtime/props";
|
|
39
41
|
import { BaseCollection, buildCollection, collectionNodeClass, } from "./base.js";
|
|
@@ -93,8 +95,25 @@ export function buildCollectionFrom(content, createCollection) {
|
|
|
93
95
|
finally {
|
|
94
96
|
run.o = null;
|
|
95
97
|
}
|
|
96
|
-
|
|
98
|
+
const keyed = typeof content === "function" || (typeof content === "object" && content !== null);
|
|
99
|
+
try {
|
|
100
|
+
const built = buildCollection(scope.specs, createCollection);
|
|
101
|
+
if (keyed)
|
|
102
|
+
(lastBuilt ??= new WeakMap()).set(content, built);
|
|
103
|
+
return built;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
// A rebuild between two unbatched writes (an item moved from one `items` signal to another: added to the first, not
|
|
107
|
+
// yet removed from the second) sees a key twice; RAC never builds that state (React batches the renders). Keep the
|
|
108
|
+
// content's last collection: the build read every source, so the second write rebuilds it. A first build throws.
|
|
109
|
+
const last = keyed ? lastBuilt?.get(content) : undefined;
|
|
110
|
+
if (last !== undefined && error instanceof Error && error.message.startsWith("Duplicate key "))
|
|
111
|
+
return last;
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
97
114
|
}
|
|
115
|
+
/** The last collection each content built (`buildCollectionFrom`'s duplicate-key tolerance on rebuilds). */
|
|
116
|
+
let lastBuilt;
|
|
98
117
|
/**
|
|
99
118
|
* Rivet (server): evaluate `content` in build mode registering nothing, so the collection root in it (ListBox / Menu /
|
|
100
119
|
* …, `isCollectionProbe`) merges its own `id` into the `SelectableCollectionContext` `MergedId` (`contextProps`) before
|
|
@@ -158,6 +177,20 @@ export function isCollectionProbe() {
|
|
|
158
177
|
export function collectionOf(content, createCollection) {
|
|
159
178
|
return computed(() => buildCollectionFrom(content, createCollection));
|
|
160
179
|
}
|
|
180
|
+
let builtCollections;
|
|
181
|
+
/**
|
|
182
|
+
* Rivet: the built collection of a virtualized host's `content` (RAC: a collection inside a `Virtualizer` renders
|
|
183
|
+
* from its built collection, only the visible window has parts). One `collectionOf` computed per `instance` (a plain
|
|
184
|
+
* object the host creates in its body: serialized as data, so a resumed page rebuilds the computed from `content` on
|
|
185
|
+
* its first read). Created detached from the reading owner: the first read may be a binding that re-runs (the
|
|
186
|
+
* virtualizer's rows), which would dispose a computed it owns.
|
|
187
|
+
*/
|
|
188
|
+
export function builtCollectionOf(instance, content, createCollection) {
|
|
189
|
+
let built = (builtCollections ??= new WeakMap()).get(instance);
|
|
190
|
+
if (!built)
|
|
191
|
+
builtCollections.set(instance, (built = runWithOwner(null, () => collectionOf(content, createCollection))));
|
|
192
|
+
return built();
|
|
193
|
+
}
|
|
161
194
|
/**
|
|
162
195
|
* Builds a `Collection` from the `content` and passes it to `children` (RAC `CollectionBuilder`).
|
|
163
196
|
* Inside another builder's build pass it only renders the content (registers into that build).
|
|
@@ -173,10 +206,68 @@ export function CollectionBuilder(props) {
|
|
|
173
206
|
function evaluate(content) {
|
|
174
207
|
if (typeof content === "function" && (isSlot(content) || content.length === 0))
|
|
175
208
|
return evaluate(content());
|
|
176
|
-
if (Array.isArray(content))
|
|
209
|
+
if (Array.isArray(content)) {
|
|
210
|
+
if (!isServer && hasThunk(content)) {
|
|
211
|
+
const build = useContext(BuildContext);
|
|
212
|
+
if (build)
|
|
213
|
+
return evaluateInOrder(content, build.specs);
|
|
214
|
+
}
|
|
177
215
|
return content.map(evaluate);
|
|
216
|
+
}
|
|
178
217
|
return content;
|
|
179
218
|
}
|
|
219
|
+
// Build-pass order (browser). A children array is built eagerly: its parts register while JSX builds the array, and a
|
|
220
|
+
// `{() => …}` / `<Show>` / `<For>` child (an accessor) registers only when `evaluate` calls it, after its siblings. In
|
|
221
|
+
// build mode, parts return a per-spec mark (an empty comment: renders nothing; a node, so array copies — the dev
|
|
222
|
+
// runtime maps component output — keep it), so the array says where each eager part's spec belongs; an accessor's
|
|
223
|
+
// specs go where the accessor sits. Server: the build calls everything in document order already.
|
|
224
|
+
let marks;
|
|
225
|
+
/** The value a part returns in build mode: `null` on the server, a mark of its spec in the browser. */
|
|
226
|
+
const buildMark = (spec) => {
|
|
227
|
+
if (isServer)
|
|
228
|
+
return null;
|
|
229
|
+
// `globalThis.document`: the compiler rejects bare browser globals in a server-rendered module (RV2001).
|
|
230
|
+
const mark = globalThis.document.createComment("");
|
|
231
|
+
(marks ??= new WeakMap()).set(mark, spec);
|
|
232
|
+
return mark;
|
|
233
|
+
};
|
|
234
|
+
const isThunk = (value) => typeof value === "function" && (isSlot(value) || value.length === 0);
|
|
235
|
+
function hasThunk(content) {
|
|
236
|
+
for (const value of content)
|
|
237
|
+
if (isThunk(value) || (Array.isArray(value) && hasThunk(value)))
|
|
238
|
+
return true;
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
function evaluateInOrder(content, list) {
|
|
242
|
+
const listed = new Set(list);
|
|
243
|
+
const order = [];
|
|
244
|
+
const walk = (value) => {
|
|
245
|
+
if (Array.isArray(value))
|
|
246
|
+
for (const item of value)
|
|
247
|
+
walk(item);
|
|
248
|
+
else if (isThunk(value)) {
|
|
249
|
+
const start = list.length;
|
|
250
|
+
evaluate(value);
|
|
251
|
+
for (const spec of list.splice(start))
|
|
252
|
+
order.push(spec);
|
|
253
|
+
}
|
|
254
|
+
else if (value !== null && typeof value === "object") {
|
|
255
|
+
const spec = marks?.get(value);
|
|
256
|
+
if (spec && listed.has(spec))
|
|
257
|
+
order.push(spec);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
walk(content);
|
|
261
|
+
const placed = new Set(order);
|
|
262
|
+
let at = 0;
|
|
263
|
+
for (const spec of list)
|
|
264
|
+
if (!placed.has(spec))
|
|
265
|
+
list[at++] = spec;
|
|
266
|
+
list.length = at;
|
|
267
|
+
for (const spec of order)
|
|
268
|
+
list.push(spec);
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
180
271
|
/**
|
|
181
272
|
* Live (rendered) content of a host / branch / row, placed inside the caller's providers: slots run now; any other
|
|
182
273
|
* 0-arity function — a `{() => …}` render-function child or a DOM v2 structural accessor (`<For>` / `<Show>` /
|
|
@@ -276,7 +367,7 @@ export function createLeafComponent(CollectionNodeClass, render) {
|
|
|
276
367
|
const value = all.value ?? defaults?.value;
|
|
277
368
|
// RAC passes node.props as given: `id` only when the user (or an `items` row) supplied one.
|
|
278
369
|
const explicitId = resolveId(all, defaults, collectionCtx);
|
|
279
|
-
|
|
370
|
+
const spec = {
|
|
280
371
|
type: NodeClass,
|
|
281
372
|
id: explicitId ?? generatedId(build.parentKey, build.ids),
|
|
282
373
|
value: value,
|
|
@@ -286,8 +377,9 @@ export function createLeafComponent(CollectionNodeClass, render) {
|
|
|
286
377
|
props: rest,
|
|
287
378
|
ownProps: true,
|
|
288
379
|
render: (node) => render(withNode(all, live && !defaults, node.props, live, explicitId == null ? { value: node.value } : { id: node.key, value: node.value }), p.ref, node),
|
|
289
|
-
}
|
|
290
|
-
|
|
380
|
+
};
|
|
381
|
+
build.specs.push(spec);
|
|
382
|
+
return buildMark(spec);
|
|
291
383
|
}
|
|
292
384
|
const scope = useContext(CollectionScopeContext);
|
|
293
385
|
if (!scope) {
|
|
@@ -337,7 +429,7 @@ export function createBranchComponent(CollectionNodeClass, render, useChildren =
|
|
|
337
429
|
};
|
|
338
430
|
build.specs.push(spec);
|
|
339
431
|
provide(BuildContext, { specs: spec.children, parentKey: key, ids: { n: 0 }, run: build.run }, () => provide(CollectionItemContext, null, () => tracked(build, () => evaluate(() => useChildren(childProps(props, all, id))))));
|
|
340
|
-
return
|
|
432
|
+
return buildMark(spec);
|
|
341
433
|
}
|
|
342
434
|
const scope = useContext(CollectionScopeContext);
|
|
343
435
|
const key = id ?? generatedId(scope?.parentKey, scope?.ids ?? { n: 0 });
|
|
@@ -477,23 +569,136 @@ export function renderNodeAt(collection, key, depends) {
|
|
|
477
569
|
// of its region (a sort re-created the table header and body: focus lost) instead of keeping it like a client rebuild.
|
|
478
570
|
if (isServer)
|
|
479
571
|
return untrack(() => {
|
|
480
|
-
const n = collection()?.getItem(key()) ?? null;
|
|
572
|
+
const n = collection()?.getItem(rowKey(key())) ?? null;
|
|
573
|
+
// DOM-DATA servers: the row's data object (the For iterates keys), registered with the row (`domRowItem`).
|
|
574
|
+
if (n !== null)
|
|
575
|
+
domRowItem(n.value);
|
|
481
576
|
return n?.render ? n.render(n) : null;
|
|
482
577
|
});
|
|
483
|
-
const node = computed(() => collection()?.getItem(key()) ?? null, { equals: sameRender });
|
|
578
|
+
const node = computed(() => collection()?.getItem(rowKey(key())) ?? null, { equals: sameRender });
|
|
484
579
|
return serverNow(() => {
|
|
485
580
|
depends?.();
|
|
486
581
|
node();
|
|
487
582
|
// The current node (the cut-off computed keeps the first of equal ones; `depends` re-renders with the latest).
|
|
488
|
-
const n = untrack(() => collection()?.getItem(key()) ?? null);
|
|
583
|
+
const n = untrack(() => collection()?.getItem(rowKey(key())) ?? null);
|
|
489
584
|
return n?.render ? untrack(() => n.render(n)) : null;
|
|
490
585
|
});
|
|
491
586
|
}
|
|
492
|
-
|
|
587
|
+
/**
|
|
588
|
+
* Lazy collection rows (`resume.lazyRows`, LAZY-T6): a loop over container nodes — a section, a table's header / body (nodes
|
|
589
|
+
* whose children have children) — is not the lazy `For`; the rows below it are (a lazy row is one option / table row, not a
|
|
590
|
+
* whole table body). Read by the server's lazy rows only.
|
|
591
|
+
*/
|
|
592
|
+
function leafNodes(collection, parent) {
|
|
593
|
+
const c = resolveCollection(collection);
|
|
594
|
+
if (!c?.getChildren)
|
|
595
|
+
return true;
|
|
596
|
+
for (const n of parent ? c.getChildren(parent.key) : c) {
|
|
597
|
+
if (n.type === "content")
|
|
598
|
+
continue;
|
|
599
|
+
if (n.type === "section")
|
|
600
|
+
return false;
|
|
601
|
+
// No children (built nodes keep `firstChildKey`): no iterator per leaf (LAZY-COST-2: 1000 options ≈ 0.5 ms / render).
|
|
602
|
+
if (n.firstChildKey === null)
|
|
603
|
+
continue;
|
|
604
|
+
for (const child of c.getChildren(n.key)) {
|
|
605
|
+
for (const _ of c.getChildren(child.key))
|
|
606
|
+
return false;
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* The nodes a row of `n` renders (its stamp covers them): `n`, and a section's items / a table row's cells — a nested `For`
|
|
614
|
+
* inside the row (`CollectionBranch`) that keeps no baseline of its own (`serverRows`).
|
|
615
|
+
*/
|
|
616
|
+
function rowNodes(c, n) {
|
|
617
|
+
const row = [n];
|
|
618
|
+
if (n.firstChildKey != null && c.getChildren)
|
|
619
|
+
for (const child of c.getChildren(n.key))
|
|
620
|
+
if (n.type === "section" || child.type === "cell")
|
|
621
|
+
row.push(child);
|
|
622
|
+
return row;
|
|
623
|
+
}
|
|
624
|
+
/** The stamp of a row's nodes (`row-stamps.ts`). */
|
|
625
|
+
const rowStamp = (row) => stamp(row[0], row.length > 1 ? row.slice(1) : null);
|
|
626
|
+
/**
|
|
627
|
+
* Server baseline of a resumed For over collection keys (ROW-RERENDER): `_regionState` with the stamps of the rows the server
|
|
628
|
+
* renders (`row-stamps.ts`) when the collection is an accessor whose build read signals (a static collection never rebuilds:
|
|
629
|
+
* null), recorded with the For's region; none (no slot) for the children of a section / table row (`parentType`). Called by the server and the browser alike (one `_regionState` slot per accessor
|
|
630
|
+
* collection, so the region's other slots keep their positions); the browser gets the server's value back when it
|
|
631
|
+
* re-creates that region (adopting the rows), null otherwise.
|
|
632
|
+
*/
|
|
633
|
+
export function serverRows(collection, keys, parentType) {
|
|
634
|
+
// The rows of a section / a table row: covered by the stamp of the row holding them (`rowNodes`). Decided by the
|
|
635
|
+
// parent node's type alone (the same on the server and in the browser: the `_regionState` slot is positional).
|
|
636
|
+
if (typeof collection !== "function" || parentType === "section" || parentType === "item")
|
|
637
|
+
return null;
|
|
638
|
+
return _regionState(() => {
|
|
639
|
+
if (!isServer)
|
|
640
|
+
return null;
|
|
641
|
+
return untrack(() => {
|
|
642
|
+
const c = _trackReads(collection);
|
|
643
|
+
if (!c.signals.length || !c.value)
|
|
644
|
+
return null;
|
|
645
|
+
let out = "";
|
|
646
|
+
for (const k of keys()) {
|
|
647
|
+
const n = c.value.getItem(k);
|
|
648
|
+
out += n ? rowStamp(rowNodes(c.value, n)) : stamp(null);
|
|
649
|
+
}
|
|
650
|
+
return out || null;
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* The keys of a resumed For over collection keys (`renderNodeAt` rows), given the server baseline (`serverRows`): an adopted
|
|
656
|
+
* server row keeps its key while its node renders like the server's (stamp in the baseline; then `sameRender` against the
|
|
657
|
+
* last node checked), else its key is boxed (`RowBox`, one box per key for good) — a key the adopted list does not know, so
|
|
658
|
+
* the For re-creates that row (rows read `rowKey(key())`). Rows the server did not render get a box too (created anyway).
|
|
659
|
+
* Without a baseline: `keys` itself.
|
|
660
|
+
*/
|
|
661
|
+
export function resumedKeys(collection, base, keys) {
|
|
662
|
+
if (base === null || isServer)
|
|
663
|
+
return keys;
|
|
664
|
+
let set = null;
|
|
665
|
+
const boxes = new Map(),
|
|
666
|
+
// The last node (`rowNodes`: + its items / cells) found to render like the server's, per key.
|
|
667
|
+
seen = new Map();
|
|
668
|
+
return () => {
|
|
669
|
+
const list = keys(), c = resolveCollection(collection);
|
|
670
|
+
return untrack(() => {
|
|
671
|
+
set ??= stampSet(base);
|
|
672
|
+
let out = null;
|
|
673
|
+
for (let i = 0; i < list.length; i++) {
|
|
674
|
+
const k = list[i];
|
|
675
|
+
let box = boxes.get(k);
|
|
676
|
+
if (box === undefined) {
|
|
677
|
+
const n = c?.getItem(k) ?? null;
|
|
678
|
+
if (n === null)
|
|
679
|
+
continue;
|
|
680
|
+
const row = rowNodes(c, n), last = seen.get(k);
|
|
681
|
+
// Unchanged since the last check (`sameRender`, node by node): no stamp.
|
|
682
|
+
if (last !== undefined && last.length === row.length && row.every((x, j) => sameRender(last[j], x)))
|
|
683
|
+
continue;
|
|
684
|
+
if (set.has(rowStamp(row))) {
|
|
685
|
+
seen.set(k, row);
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
seen.delete(k);
|
|
689
|
+
boxes.set(k, (box = { k }));
|
|
690
|
+
}
|
|
691
|
+
(out ??= list.slice())[i] = box;
|
|
692
|
+
}
|
|
693
|
+
return out ?? list;
|
|
694
|
+
});
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function renderNodes(collection, parent, parentType) {
|
|
493
698
|
// Rows are keyed by primitive keys (stable across rebuilds) so an adopted (resumed) row survives a
|
|
494
699
|
// collection rebuild; a row re-renders only when its node's render inputs changed (`renderNodeAt`) and parts
|
|
495
|
-
// read live data by key.
|
|
496
|
-
const
|
|
700
|
+
// read live data by key. An adopted row whose node changed since the server render is re-created (`resumedKeys`).
|
|
701
|
+
const keys = () => {
|
|
497
702
|
const c = resolveCollection(collection);
|
|
498
703
|
const keys = [];
|
|
499
704
|
for (const n of parent ? c.getChildren(parent.key) : c)
|
|
@@ -501,7 +706,8 @@ function renderNodes(collection, parent) {
|
|
|
501
706
|
keys.push(n.key);
|
|
502
707
|
return keys;
|
|
503
708
|
};
|
|
504
|
-
|
|
709
|
+
const each = resumedKeys(collection, serverRows(collection, keys, parentType), keys);
|
|
710
|
+
return (<For each={each} by={(key) => key} lazy={() => leafNodes(collection, parent)}>
|
|
505
711
|
{(key) => renderNodeAt(() => resolveCollection(collection), key)}
|
|
506
712
|
</For>);
|
|
507
713
|
}
|
|
@@ -517,8 +723,10 @@ export const DefaultCollectionRenderer = {
|
|
|
517
723
|
},
|
|
518
724
|
CollectionBranch(props) {
|
|
519
725
|
const { collection, parent } = props;
|
|
520
|
-
|
|
521
|
-
|
|
726
|
+
// `parent` may be row data (`{ key }`, a resumable capture), not the node: its type comes from the collection.
|
|
727
|
+
const node = collection ? untrack(() => resolveCollection(collection)?.getItem(parent.key)) : null;
|
|
728
|
+
if (node)
|
|
729
|
+
return renderNodes(collection, parent, node.type);
|
|
522
730
|
return (parent.content?.() ?? null);
|
|
523
731
|
},
|
|
524
732
|
};
|
package/collections/nodes.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Collection, Node } from "./types.js";
|
|
1
|
+
import type { Collection, Key, Node } from "./types.js";
|
|
2
2
|
export declare function getChildNodes<T>(node: Node<T>, collection: Collection<Node<T>>): Iterable<Node<T>>;
|
|
3
3
|
export declare function getFirstItem<T>(iterable: Iterable<T>): T | undefined;
|
|
4
4
|
export declare function getNthItem<T>(iterable: Iterable<T>, index: number): T | undefined;
|
|
@@ -7,3 +7,11 @@ export declare function getLastItem<T>(iterable: Iterable<T>): T | undefined;
|
|
|
7
7
|
export declare function compareNodeOrder<T>(collection: Collection<Node<T>>, a: Node<T>, b: Node<T>): number;
|
|
8
8
|
/** Number of `item` nodes, descending into sections (cached per collection). */
|
|
9
9
|
export declare function getItemCount<T>(collection: Collection<Node<T>>): number;
|
|
10
|
+
/**
|
|
11
|
+
* Rivet: `[aria-posinset, aria-setsize]` of the item `key` in a virtualized collection (only a window of its items is
|
|
12
|
+
* in the DOM, so the browser cannot count them): its 1-based position among the items of its section and that
|
|
13
|
+
* section's item count; top-level items count among the top-level items. Headers, separators and loaders are not
|
|
14
|
+
* counted. RA useOption / useMenuItem use `node.index + 1` (a section's header counted) of `getItemCount` (every item
|
|
15
|
+
* of every section): equal without sections, per section (role `group`) with them. Cached per committed collection.
|
|
16
|
+
*/
|
|
17
|
+
export declare function itemPosition<T>(collection: Collection<Node<T>>, key: Key): readonly [number, number] | undefined;
|
package/collections/nodes.js
CHANGED
|
@@ -79,3 +79,29 @@ export function getItemCount(collection) {
|
|
|
79
79
|
countCache.set(collection, counter);
|
|
80
80
|
return counter;
|
|
81
81
|
}
|
|
82
|
+
const positionCache = new WeakMap();
|
|
83
|
+
/**
|
|
84
|
+
* Rivet: `[aria-posinset, aria-setsize]` of the item `key` in a virtualized collection (only a window of its items is
|
|
85
|
+
* in the DOM, so the browser cannot count them): its 1-based position among the items of its section and that
|
|
86
|
+
* section's item count; top-level items count among the top-level items. Headers, separators and loaders are not
|
|
87
|
+
* counted. RA useOption / useMenuItem use `node.index + 1` (a section's header counted) of `getItemCount` (every item
|
|
88
|
+
* of every section): equal without sections, per section (role `group`) with them. Cached per committed collection.
|
|
89
|
+
*/
|
|
90
|
+
export function itemPosition(collection, key) {
|
|
91
|
+
let cache = positionCache.get(collection);
|
|
92
|
+
if (!cache)
|
|
93
|
+
positionCache.set(collection, (cache = new Map()));
|
|
94
|
+
const hit = cache.get(key);
|
|
95
|
+
if (hit)
|
|
96
|
+
return hit;
|
|
97
|
+
const node = collection.getItem(key);
|
|
98
|
+
if (!node || node.type !== "item")
|
|
99
|
+
return undefined;
|
|
100
|
+
const parent = node.parentKey != null ? collection.getItem(node.parentKey) : null;
|
|
101
|
+
const keys = [];
|
|
102
|
+
for (const sibling of parent ? getChildNodes(parent, collection) : collection)
|
|
103
|
+
if (sibling.type === "item")
|
|
104
|
+
keys.push(sibling.key);
|
|
105
|
+
keys.forEach((k, i) => cache.set(k, [i + 1, keys.length]));
|
|
106
|
+
return cache.get(key);
|
|
107
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Key, Node } from "./types.js";
|
|
2
|
+
/** Characters per row stamp (24 bits). */
|
|
3
|
+
export declare const STAMP = 4;
|
|
4
|
+
/**
|
|
5
|
+
* The row stamp of a built node: a 24-bit hash of what `sameRender` compares (type, key, text, `rendered`, props, value), as
|
|
6
|
+
* 4 characters. A section's stamp covers its child nodes too (`children`): its rows are a For the section part creates in
|
|
7
|
+
* its body (ListBoxSection), which a resumed page never re-runs, so a changed child re-creates the section row.
|
|
8
|
+
*/
|
|
9
|
+
export declare function stamp(n: Node<unknown> | null | undefined, children?: Iterable<Node<unknown>> | null): string;
|
|
10
|
+
/** The set of stamps in a baseline string. */
|
|
11
|
+
export declare function stampSet(base: string): Set<string>;
|
|
12
|
+
/** A row key the browser re-created (its node changed since the server render): a fresh For key for the same collection key. */
|
|
13
|
+
export interface RowBox {
|
|
14
|
+
readonly k: Key;
|
|
15
|
+
}
|
|
16
|
+
/** The collection key of a `For` row key (`resumedKeys` boxes the keys of rows it re-creates). */
|
|
17
|
+
export declare const rowKey: (key: Key | RowBox) => Key;
|