@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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// 64 JSON / HTML-safe characters (no quote, backslash, `<`, `&`).
|
|
2
|
+
const DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
3
|
+
/** Characters per row stamp (24 bits). */
|
|
4
|
+
export const STAMP = 4;
|
|
5
|
+
let h = 0, budget = 0;
|
|
6
|
+
const ch = (c) => void (h = Math.imul(h ^ c, 16777619));
|
|
7
|
+
const str = (s) => {
|
|
8
|
+
for (let i = 0; i < s.length; i++)
|
|
9
|
+
h = Math.imul(h ^ s.charCodeAt(i), 16777619);
|
|
10
|
+
ch(0);
|
|
11
|
+
};
|
|
12
|
+
const isPlain = (v) => {
|
|
13
|
+
const proto = Object.getPrototypeOf(v);
|
|
14
|
+
return proto === Object.prototype || proto === null;
|
|
15
|
+
};
|
|
16
|
+
// Data the server and the browser see alike (resume-decoded values): primitives, arrays, plain objects, dates. Functions
|
|
17
|
+
// (slots, render functions, handlers: the server and browser compile them differently) and class instances count by kind only;
|
|
18
|
+
// nesting below `depth` by kind and length. `budget` bounds the work per node (a tree node's value holds its subtree).
|
|
19
|
+
function feed(v, depth) {
|
|
20
|
+
if (--budget < 0)
|
|
21
|
+
return;
|
|
22
|
+
switch (typeof v) {
|
|
23
|
+
case "string":
|
|
24
|
+
(ch(1), str(v));
|
|
25
|
+
return;
|
|
26
|
+
case "number":
|
|
27
|
+
case "bigint":
|
|
28
|
+
(ch(2), str(String(v)));
|
|
29
|
+
return;
|
|
30
|
+
case "boolean":
|
|
31
|
+
ch(v ? 3 : 4);
|
|
32
|
+
return;
|
|
33
|
+
case "undefined":
|
|
34
|
+
ch(5);
|
|
35
|
+
return;
|
|
36
|
+
case "function":
|
|
37
|
+
ch(6);
|
|
38
|
+
return;
|
|
39
|
+
case "object":
|
|
40
|
+
break;
|
|
41
|
+
default:
|
|
42
|
+
ch(7);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (v === null)
|
|
46
|
+
return ch(8);
|
|
47
|
+
if (Array.isArray(v)) {
|
|
48
|
+
(ch(9), str(String(v.length)));
|
|
49
|
+
if (depth > 0)
|
|
50
|
+
for (let i = 0; i < v.length; i++)
|
|
51
|
+
feed(v[i], depth - 1);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (v instanceof Date)
|
|
55
|
+
return (ch(10), str(String(v.getTime())));
|
|
56
|
+
if (!isPlain(v))
|
|
57
|
+
return ch(11);
|
|
58
|
+
ch(12);
|
|
59
|
+
if (depth > 0)
|
|
60
|
+
for (const k in v)
|
|
61
|
+
(str(k), feed(v[k], depth - 1));
|
|
62
|
+
}
|
|
63
|
+
function node(n) {
|
|
64
|
+
const live = n.constructor.liveProps;
|
|
65
|
+
(str(n.type), feed(n.key, 0), feed(n.textValue, 0), feed(n["aria-label"], 0), feed(n.rendered, 0));
|
|
66
|
+
const props = n.props;
|
|
67
|
+
if (props)
|
|
68
|
+
for (const k in props)
|
|
69
|
+
if (!live?.includes(k))
|
|
70
|
+
(str(k), feed(props[k], 2));
|
|
71
|
+
const value = n.value;
|
|
72
|
+
if (live && value !== null && typeof value === "object" && isPlain(value)) {
|
|
73
|
+
ch(12);
|
|
74
|
+
for (const k in value)
|
|
75
|
+
if (!live.includes(k))
|
|
76
|
+
(str(k), feed(value[k], 2));
|
|
77
|
+
}
|
|
78
|
+
else
|
|
79
|
+
feed(value, 3);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The row stamp of a built node: a 24-bit hash of what `sameRender` compares (type, key, text, `rendered`, props, value), as
|
|
83
|
+
* 4 characters. A section's stamp covers its child nodes too (`children`): its rows are a For the section part creates in
|
|
84
|
+
* its body (ListBoxSection), which a resumed page never re-runs, so a changed child re-creates the section row.
|
|
85
|
+
*/
|
|
86
|
+
export function stamp(n, children) {
|
|
87
|
+
h = 0x811c9dc5;
|
|
88
|
+
if (n) {
|
|
89
|
+
budget = 256;
|
|
90
|
+
node(n);
|
|
91
|
+
if (children)
|
|
92
|
+
for (const child of children)
|
|
93
|
+
((budget = 256), node(child));
|
|
94
|
+
}
|
|
95
|
+
h ^= h >>> 15;
|
|
96
|
+
h = Math.imul(h, 0x2c1b3c6d);
|
|
97
|
+
h ^= h >>> 12;
|
|
98
|
+
return DIGITS[h & 63] + DIGITS[(h >>> 6) & 63] + DIGITS[(h >>> 12) & 63] + DIGITS[(h >>> 18) & 63];
|
|
99
|
+
}
|
|
100
|
+
/** The set of stamps in a baseline string. */
|
|
101
|
+
export function stampSet(base) {
|
|
102
|
+
const set = new Set();
|
|
103
|
+
for (let i = 0; i + STAMP <= base.length; i += STAMP)
|
|
104
|
+
set.add(base.slice(i, i + STAMP));
|
|
105
|
+
return set;
|
|
106
|
+
}
|
|
107
|
+
/** The collection key of a `For` row key (`resumedKeys` boxes the keys of rows it re-creates). */
|
|
108
|
+
export const rowKey = (key) => (typeof key === "object" ? key.k : key);
|
|
@@ -3,3 +3,4 @@ export declare const slotProbing = false;
|
|
|
3
3
|
export declare function probeSlots(_values: readonly (readonly [Context<any>, unknown])[], _content: unknown): void;
|
|
4
4
|
export declare const idScoping = false;
|
|
5
5
|
export declare function scopedIdUpdaters(): Map<string, unknown> | undefined;
|
|
6
|
+
export declare function domRowItem(_item: unknown): void;
|
|
@@ -3,3 +3,4 @@ export declare const slotProbing = false;
|
|
|
3
3
|
export declare function probeSlots(_values: readonly (readonly [Context<any>, unknown])[], _content: unknown): void;
|
|
4
4
|
export declare const idScoping = false;
|
|
5
5
|
export declare function scopedIdUpdaters(): Map<string, unknown> | undefined;
|
|
6
|
+
export declare function domRowItem(_item: unknown): void;
|
|
@@ -74,3 +74,13 @@ export function scopedIdUpdaters() {
|
|
|
74
74
|
scopedIds.set(scope, (map = new Map()));
|
|
75
75
|
return map;
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* DOM-backed loader data (DOM-DATA, `resume: { domData: true }` servers): a built collection row renders `item` (the
|
|
79
|
+
* node's `value`, the user's data object) although its `For` iterates keys. `@rivetjs/runtime/server-resume` installs the
|
|
80
|
+
* hook only in domData builds (`Symbol.for("rivet.domRowItem")`): the row then registers that object, so the item's
|
|
81
|
+
* strings the row shows as an element's whole text / a plain attribute travel as DOM paths. Elsewhere: nothing.
|
|
82
|
+
*/
|
|
83
|
+
const DOM_ROW_ITEM = Symbol.for("rivet.domRowItem");
|
|
84
|
+
export function domRowItem(item) {
|
|
85
|
+
globalThis[DOM_ROW_ITEM]?.(item);
|
|
86
|
+
}
|
|
@@ -91,6 +91,12 @@ export interface DragSetup<T = any> {
|
|
|
91
91
|
config?: DragDropConfig | null;
|
|
92
92
|
/** `DirectionalityContext` value where the `<Drag>` rendered (else the document's). */
|
|
93
93
|
dir?: Directionality | null;
|
|
94
|
+
/**
|
|
95
|
+
* Rivet: the `<Drag>` element's `ref()`. The instance is destroyed once it lets go of the element: a client unmount
|
|
96
|
+
* (`bindRef`'s cleanup) or, on a resumed page, the end of the server element's region (a row a `For` drops, a
|
|
97
|
+
* re-created region: the revived ref resets).
|
|
98
|
+
*/
|
|
99
|
+
element?: () => HTMLElement | undefined;
|
|
94
100
|
}
|
|
95
101
|
/** Element that can be moved inside a `DropList` container (Angular `CdkDrag`). */
|
|
96
102
|
export declare class DragInstance<T = any> {
|
|
@@ -112,6 +118,7 @@ export declare class DragInstance<T = any> {
|
|
|
112
118
|
private _dragDropRegistry;
|
|
113
119
|
private _destroyed;
|
|
114
120
|
private _disposeSync;
|
|
121
|
+
private _disposeRelease;
|
|
115
122
|
private _containerBeforeStarted;
|
|
116
123
|
private _movedSubscription;
|
|
117
124
|
private _handles;
|
|
@@ -163,6 +170,15 @@ export declare class DragInstance<T = any> {
|
|
|
163
170
|
setFreeDragPosition(value: Point): void;
|
|
164
171
|
/** Rivet: the props of a lazily created (bare) instance arrived with its own handler. */
|
|
165
172
|
_setInputs(setup: DragSetup<T>): void;
|
|
173
|
+
/**
|
|
174
|
+
* Rivet: destroy the instance once `element` (the `<Drag>`'s `ref()`, `DragSetup.element`) stops holding this
|
|
175
|
+
* element (Angular `ngOnDestroy`). A server-rendered `<Drag>` runs no component code on a resumed page, so its lazily
|
|
176
|
+
* created instance had no destroy hook: a row a resumed `For` dropped stayed in the registry (`_dragInstances`, the
|
|
177
|
+
* document `touchmove` listener), its list's items and its sync effects. The resume runtime resets a revived ref when
|
|
178
|
+
* the region of its server element dies; client renders reset it on unmount (`bindRef`), like `drag.tsx`'s cleanup.
|
|
179
|
+
* False: destroyed (the element already left).
|
|
180
|
+
*/
|
|
181
|
+
_releaseWith(element: (() => HTMLElement | undefined) | undefined): boolean;
|
|
166
182
|
/** Rivet: sync what the DOM holds (handles, container state) before the ref sees a pointer down. */
|
|
167
183
|
_beforePointerDown(): void;
|
|
168
184
|
/** Angular `ngOnDestroy`. */
|
|
@@ -56,6 +56,7 @@ export class DragInstance {
|
|
|
56
56
|
_dragDropRegistry;
|
|
57
57
|
_destroyed = false;
|
|
58
58
|
_disposeSync = null;
|
|
59
|
+
_disposeRelease = null;
|
|
59
60
|
_containerBeforeStarted = null;
|
|
60
61
|
_movedSubscription = null;
|
|
61
62
|
_handles = [];
|
|
@@ -224,6 +225,32 @@ export class DragInstance {
|
|
|
224
225
|
this._syncReactiveInputs();
|
|
225
226
|
this._syncHostClasses();
|
|
226
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Rivet: destroy the instance once `element` (the `<Drag>`'s `ref()`, `DragSetup.element`) stops holding this
|
|
230
|
+
* element (Angular `ngOnDestroy`). A server-rendered `<Drag>` runs no component code on a resumed page, so its lazily
|
|
231
|
+
* created instance had no destroy hook: a row a resumed `For` dropped stayed in the registry (`_dragInstances`, the
|
|
232
|
+
* document `touchmove` listener), its list's items and its sync effects. The resume runtime resets a revived ref when
|
|
233
|
+
* the region of its server element dies; client renders reset it on unmount (`bindRef`), like `drag.tsx`'s cleanup.
|
|
234
|
+
* False: destroyed (the element already left).
|
|
235
|
+
*/
|
|
236
|
+
_releaseWith(element) {
|
|
237
|
+
if (element && !this._disposeRelease && !this._destroyed) {
|
|
238
|
+
const release = (this._disposeRelease = createRoot((dispose) => {
|
|
239
|
+
let held = false;
|
|
240
|
+
effect(() => {
|
|
241
|
+
if (element() === this.element)
|
|
242
|
+
held = true;
|
|
243
|
+
// Never held: a ref that already let go (its region died before this handler ran, a replayed press).
|
|
244
|
+
else if (held || !this.element.isConnected)
|
|
245
|
+
untrack(() => this.destroy());
|
|
246
|
+
});
|
|
247
|
+
return dispose;
|
|
248
|
+
}).value);
|
|
249
|
+
if (this._destroyed)
|
|
250
|
+
release();
|
|
251
|
+
}
|
|
252
|
+
return !this._destroyed;
|
|
253
|
+
}
|
|
227
254
|
/** Rivet: sync what the DOM holds (handles, container state) before the ref sees a pointer down. */
|
|
228
255
|
_beforePointerDown() {
|
|
229
256
|
this._syncHandles();
|
|
@@ -241,6 +268,7 @@ export class DragInstance {
|
|
|
241
268
|
}
|
|
242
269
|
this._dragDropRegistry.removeDirectiveNode(this.element);
|
|
243
270
|
this._disposeSync?.();
|
|
271
|
+
this._disposeRelease?.();
|
|
244
272
|
this._containerBeforeStarted?.unsubscribe();
|
|
245
273
|
this._movedSubscription?.unsubscribe();
|
|
246
274
|
this._dragRef.dispose();
|
package/drag-drop/drag.jsx
CHANGED
|
@@ -47,7 +47,10 @@ export function Drag(props) {
|
|
|
47
47
|
onDropped: props.onDropped,
|
|
48
48
|
onMoved: props.onMoved,
|
|
49
49
|
};
|
|
50
|
-
|
|
50
|
+
// Client render: set once rendered. Resumed page: revived holding the server element, reset when its region dies;
|
|
51
|
+
// the instance (created by the first press) is destroyed with it (`DragInstance._releaseWith`).
|
|
52
|
+
const element = ref();
|
|
53
|
+
const setup = { inputs, config, dir, element };
|
|
51
54
|
// Read every prop up front: an element with a spread resumes as one binding over all its
|
|
52
55
|
// attributes, and reading `props` there would capture the whole props object (children included).
|
|
53
56
|
const dom = globalDOMProps(props, OWNED);
|
|
@@ -75,8 +78,6 @@ export function Drag(props) {
|
|
|
75
78
|
dragHostEvent(event, setup);
|
|
76
79
|
onTouchStart?.(event);
|
|
77
80
|
};
|
|
78
|
-
// Client render only: on the server the ref stays empty.
|
|
79
|
-
const element = ref();
|
|
80
81
|
const node = elementType === "li" ? (<li {...dom} ref={element} id={id} class={className} class:cdk-drag-disabled={isDisabled()} data-disabled={isDisabled() ? "true" : undefined} data-rv-drag="" data-rv-drag-self-handle={selfHandle} data-rv-free-position={transform() ? "" : undefined} style={style} style:transform={transform()} onMouseDown={mouseDown} onTouchStart={touchStart} onDragStart={host}>
|
|
81
82
|
{children}
|
|
82
83
|
</li>) : elementType === "tr" ? (<tr {...dom} ref={element} id={id} class={className} class:cdk-drag-disabled={isDisabled()} data-disabled={isDisabled() ? "true" : undefined} data-rv-drag="" data-rv-drag-self-handle={selfHandle} data-rv-free-position={transform() ? "" : undefined} style={style} style:transform={transform()} onMouseDown={mouseDown} onTouchStart={touchStart} onDragStart={host}>
|
|
@@ -91,7 +92,8 @@ export function Drag(props) {
|
|
|
91
92
|
{children}
|
|
92
93
|
</div>);
|
|
93
94
|
// Client-rendered: join a list that is being dragged over (Angular `addItem` on construction) and
|
|
94
|
-
// destroy the instance with the element (`ngOnDestroy`). Server-rendered drags
|
|
95
|
+
// destroy the instance with the element (`ngOnDestroy`). Server-rendered drags (resumed page, no component code):
|
|
96
|
+
// the instance goes when the revived element ref resets (its region died), `DragInstance._releaseWith`.
|
|
95
97
|
// (`getOwner()` is set on the server too: the element ref is what tells a client render apart.)
|
|
96
98
|
queueMicrotask(() => {
|
|
97
99
|
const el = element();
|
package/drag-drop/lazy-refs.js
CHANGED
|
@@ -6,14 +6,17 @@
|
|
|
6
6
|
// Cold presses: the loader replays a `mousedown` / `touchstart` once the component chunks are in
|
|
7
7
|
// (async). The replayed event is forwarded with its age: the ref counts the drag-start delay from the
|
|
8
8
|
// original press and cancels the pending sequence when the press was released meanwhile
|
|
9
|
-
// (`DragRef._pointerDownFromHost`).
|
|
10
|
-
//
|
|
9
|
+
// (`DragRef._pointerDownFromHost`). A replay is a press handled after its dispatch ended
|
|
10
|
+
// (`eventPhase` NONE), however soon: preloaded / merged chunks replay it within a few ms, and its
|
|
11
|
+
// `mouseup` / `touchend` (no resumed handler, so never held by the loader) reached no listener.
|
|
12
|
+
// An item whose `<DropList>` chunk is still loading waits for the list to register before it
|
|
13
|
+
// handles the press (so it never starts in standalone mode).
|
|
11
14
|
import { DragInstance } from "./directives/drag.js";
|
|
12
15
|
import { DropListInstance } from "./directives/drop-list.js";
|
|
13
16
|
import { DRAG_ATTR, closestDropListElement, dragOf, dropListOf, drags, dropLists, isWake, wake, whenRegistered } from "./directives/registry.js";
|
|
14
17
|
import { handleElements } from "./directives/drag-handle.js";
|
|
15
18
|
import { toggleNativeDragInteractions } from "./dom/styling.js";
|
|
16
|
-
/** A press handled this much later than it happened (ms)
|
|
19
|
+
/** A press handled this much later than it happened (ms) is treated as replayed even during its dispatch. */
|
|
17
20
|
const REPLAYED_AFTER = 50;
|
|
18
21
|
/** Handler of a `<Drag>` element (`mousedown`, `touchstart`, `dragstart` + the wake event). */
|
|
19
22
|
export function dragHostEvent(event, setup) {
|
|
@@ -32,6 +35,9 @@ export function dragHostEvent(event, setup) {
|
|
|
32
35
|
if (instance)
|
|
33
36
|
instance._setInputs(setup);
|
|
34
37
|
const drag = instance ?? new DragInstance(element, setup);
|
|
38
|
+
// Dies with its element's `ref()` (a resumed row dropped, a client unmount): see `DragInstance._releaseWith`.
|
|
39
|
+
if (!drag._releaseWith(setup.element))
|
|
40
|
+
return;
|
|
35
41
|
// The wake event, or a native `dragstart`: the ref's own listener prevents it when needed, except
|
|
36
42
|
// for the `dragstart` that created the ref (its listener was added during the dispatch).
|
|
37
43
|
if (event.type === "dragstart") {
|
|
@@ -41,7 +47,12 @@ export function dragHostEvent(event, setup) {
|
|
|
41
47
|
}
|
|
42
48
|
drag._beforePointerDown();
|
|
43
49
|
const age = performance.now() - event.timeStamp;
|
|
44
|
-
|
|
50
|
+
// Replayed: its dispatch is over (the loader's replay, `whenRegistered` above), or handled late. Age alone missed a
|
|
51
|
+
// replay faster than REPLAYED_AFTER: the press released before the code loaded then kept its sequence, and the next
|
|
52
|
+
// move started a drag of an element the replayed click may already have removed (KANBAN-BUILD 2026-10-19: ui-docs
|
|
53
|
+
// `showcase-kanban`, "Cannot read properties of null (reading 'insertBefore')" in `_startDragSequence`).
|
|
54
|
+
const replayed = event.eventPhase === Event.NONE || age > REPLAYED_AFTER;
|
|
55
|
+
drag._dragRef._pointerDownFromHost(event, replayed ? Math.max(age, Number.MIN_VALUE) : 0);
|
|
45
56
|
}
|
|
46
57
|
/** Handler of a `<DropList>` element (`mousedown`, `touchstart` and the wake event): creates its instance. */
|
|
47
58
|
export function dropListHostEvent(event, setup) {
|
package/grid-list/behaviour.d.ts
CHANGED
|
@@ -160,6 +160,12 @@ export interface GridListItemAria extends SelectableItemStates {
|
|
|
160
160
|
}
|
|
161
161
|
/** Focus the row (focusMode "child": its first tabbable child) — react-aria's item `focus()`. */
|
|
162
162
|
export declare function focusRow(props: AriaGridListItemOptions, row: HTMLElement | null): void;
|
|
163
|
+
/**
|
|
164
|
+
* useGridListItem (virtualized) `aria-rowindex` of `key`: `node.index + 1`, or with sections its 1-based position
|
|
165
|
+
* among the collection's non-section keys. Rivet: the section scan is cached per committed collection (a virtualized
|
|
166
|
+
* GridList's rows each ask; upstream rescans per row).
|
|
167
|
+
*/
|
|
168
|
+
export declare function gridListRowIndex(collection: Collection<Node<unknown>>, key: Key, index?: number): number;
|
|
163
169
|
/**
|
|
164
170
|
* keydown (capture) on the row: Left / Right move between focusable children (arrow
|
|
165
171
|
* keyboardNavigationBehavior, RTL aware) and back to the row; tree expand / collapse; Up / Down
|
package/grid-list/behaviour.js
CHANGED
|
@@ -336,15 +336,33 @@ function gridListItemAttrs(props, state) {
|
|
|
336
336
|
};
|
|
337
337
|
if (props.focusMode === "child" && props.allowsArrowNavigation && d.shared.keyboardNavigationBehavior === "tab")
|
|
338
338
|
attrs.tabIndex = -1;
|
|
339
|
-
if (props.isVirtualized)
|
|
340
|
-
|
|
341
|
-
const nodes = [...collection];
|
|
342
|
-
attrs["aria-rowindex"] = nodes.find((n) => n.type === "section")
|
|
343
|
-
? [...collection.getKeys()].filter((key) => collection.getItem(key)?.type !== "section").findIndex((key) => key === node.key) + 1
|
|
344
|
-
: node.index + 1;
|
|
345
|
-
}
|
|
339
|
+
if (props.isVirtualized)
|
|
340
|
+
attrs["aria-rowindex"] = gridListRowIndex(state.collection, node.key, node.index);
|
|
346
341
|
return { ...attrs, ...d.treeAttrs };
|
|
347
342
|
}
|
|
343
|
+
const rowIndexes = new WeakMap();
|
|
344
|
+
/**
|
|
345
|
+
* useGridListItem (virtualized) `aria-rowindex` of `key`: `node.index + 1`, or with sections its 1-based position
|
|
346
|
+
* among the collection's non-section keys. Rivet: the section scan is cached per committed collection (a virtualized
|
|
347
|
+
* GridList's rows each ask; upstream rescans per row).
|
|
348
|
+
*/
|
|
349
|
+
export function gridListRowIndex(collection, key, index = collection.getItem(key)?.index ?? 0) {
|
|
350
|
+
let keys = rowIndexes.get(collection);
|
|
351
|
+
if (keys === undefined) {
|
|
352
|
+
keys = null;
|
|
353
|
+
for (const n of collection)
|
|
354
|
+
if (n.type === "section") {
|
|
355
|
+
keys = new Map();
|
|
356
|
+
for (const k of collection.getKeys())
|
|
357
|
+
if (collection.getItem(k)?.type !== "section")
|
|
358
|
+
keys.set(k, keys.size + 1);
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
if (collection.isFrozen !== false)
|
|
362
|
+
rowIndexes.set(collection, keys);
|
|
363
|
+
}
|
|
364
|
+
return keys ? (keys.get(key) ?? 0) : index + 1;
|
|
365
|
+
}
|
|
348
366
|
/** Attributes of the grid cell inside the row. */
|
|
349
367
|
const gridListCellAttrs = () => ({ role: "gridcell", "aria-colindex": 1 });
|
|
350
368
|
function handleTreeExpansionKeys(e, state, node, hasChildRows, direction, activeElement, row, allowsArrowNavigation) {
|
package/grid-list/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Signal } from "@rivetjs/reactivity";
|
|
2
2
|
import { type CollectionProps } from "../collections/components.jsx";
|
|
3
3
|
import { type ListState } from "../collections/list-state.js";
|
|
4
|
-
import type { DisabledBehavior, Key, KeyboardDelegate, Node, Orientation, Selection, SelectionBehavior, SelectionMode } from "../collections/types.js";
|
|
4
|
+
import type { Collection as ICollection, DisabledBehavior, Key, KeyboardDelegate, Node, Orientation, Selection, SelectionBehavior, SelectionMode } from "../collections/types.js";
|
|
5
5
|
import { type DOMRenderProps } from "../utils/render.js";
|
|
6
6
|
import { type GlobalDOMAttributes } from "../utils/dom-props.js";
|
|
7
7
|
import { type LinkDOMProps } from "../utils/links.jsx";
|
|
@@ -100,6 +100,8 @@ export interface GridListInternal {
|
|
|
100
100
|
version: () => unknown;
|
|
101
101
|
/** Autocomplete filter (SelectableCollectionContext): rows it returns false for do not render. */
|
|
102
102
|
filter?: ((textValue: string, node: Node<any>) => boolean) | undefined;
|
|
103
|
+
/** Rivet: the built collection of a virtualized GridList (rows read their `aria-rowindex` from it). */
|
|
104
|
+
collection?: (() => ICollection<Node<any>>) | undefined;
|
|
103
105
|
}
|
|
104
106
|
/** The GridList a part renders in (null outside a GridList, and inside a row's content). */
|
|
105
107
|
export declare const GridListInternalContext: import("@rivetjs/reactivity").Context<GridListInternal | null>;
|
package/grid-list/index.jsx
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
// @react-aria/gridlist port (`./behaviour.ts`).
|
|
7
7
|
//
|
|
8
8
|
// Rivet:
|
|
9
|
-
// -
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
9
|
+
// - Rows render from the built collection (below; `data-key` on the row); the handlers read it per event
|
|
10
|
+
// (`listStateView(state, shared)` → `listCollection(state)`). Resumed pages ship no item model: the collection is
|
|
11
|
+
// rebuilt from the content, the grid and its rows resume from serializable state only (`createListState` signals +
|
|
12
|
+
// keys + plain item props).
|
|
13
13
|
// - Row attributes are computed per row from a one-node collection of the row's own props
|
|
14
14
|
// (`rowCollection(spec)`) inside a `computed` (selection / focus / disabled reads are reactive);
|
|
15
15
|
// handlers rebuild the row node from the DOM collection per event (`eventNode`).
|
|
@@ -19,8 +19,13 @@
|
|
|
19
19
|
// `class` / `style` / `children` function asks for render props.
|
|
20
20
|
// - `className` → `class` (default `rv-GridList`, `rv-GridListItem`, `rv-GridListSection`,
|
|
21
21
|
// `rv-GridListHeader`, `rv-GridListLoadingIndicator`).
|
|
22
|
+
// - The GridList builds its collection (`computed(buildCollectionFrom(content))` in the body; LAZY-ROWS T3, 2026-10-19:
|
|
23
|
+
// always, virtualized only before): rows render from it through the renderer's keyed `For` (`renderNodeAt`, lazy-row
|
|
24
|
+
// eligible), handlers navigate it; a resumed page rebuilds it from the content on first need. Inside a
|
|
25
|
+
// `<CollectionVirtualizer layout={ListLayout | GridLayout}>` (RAC Virtualizer) rows render as views, the grid element
|
|
26
|
+
// is the scroll container (scroll forwarded to the renderer), the focused row is persisted, rows get `aria-rowindex`.
|
|
22
27
|
// Deviation: `isEmpty` is known for `items` collections and, when `renderEmptyState` is set, for static
|
|
23
|
-
// children (built
|
|
28
|
+
// children (the built collection's size); static children without `renderEmptyState` count as non-empty.
|
|
24
29
|
// Deviation: react-aria `useHasTabbableChild` (empty state with a tabbable child) is not tracked: an
|
|
25
30
|
// empty grid is always tabbable.
|
|
26
31
|
// Deviation: the description / section heading ids (react-aria `useSlotId`) are always set on the
|
|
@@ -32,7 +37,7 @@
|
|
|
32
37
|
import { computed, createContext, onCleanup, signal, untrack, useContext } from "@rivetjs/reactivity";
|
|
33
38
|
import { Show, clientEffect, ref as createRef } from "@rivetjs/runtime/dom";
|
|
34
39
|
import { buildCollection, HeaderNode, ItemNode, LoaderNode, SectionNode } from "../collections/base.js";
|
|
35
|
-
import { Collection, CollectionRendererContext, DefaultCollectionRenderer,
|
|
40
|
+
import { Collection, CollectionRendererContext, DefaultCollectionRenderer, buildCollectionFrom, createBranchComponent, createLeafComponent, } from "../collections/components.jsx";
|
|
36
41
|
import { createListState, listSelectionManager } from "../collections/list-state.js";
|
|
37
42
|
import { ListKeyboardDelegate, searchCollator } from "../selection/list-keyboard-delegate.js";
|
|
38
43
|
import {} from "../utils/render.js";
|
|
@@ -52,7 +57,7 @@ import { SharedElementContext } from "../shared-element-transition/transition.js
|
|
|
52
57
|
import { loadMoreSentinel } from "../utils/load-more-sentinel.jsx";
|
|
53
58
|
import { SelectableCollectionContext } from "../autocomplete/contexts.js";
|
|
54
59
|
import { eventNode, isFn, ringOf, rowCollection, rowSpec, slotRef, withDisabled } from "./row.js";
|
|
55
|
-
import { gridListBlur, gridListAutoFocus, gridListFocus, gridListHighlightDescription, gridListItemClick, gridListItemDoubleClick, gridListItemDragStart, gridListItemFocus, gridListItemKeyDown, gridListItemKeyDownCapture, gridListItemKeyUp, gridListItemPointerCancel, gridListItemPointerDown, gridListItemPointerUp, gridListItemProps, gridListKeyDown, gridListKeyDownCapture, gridListMouseDown, gridListProps, gridListSectionProps, gridListSelectionCheckboxChange, gridListSelectionCheckboxProps, listStateView, } from "./behaviour.js";
|
|
60
|
+
import { gridListBlur, gridListAutoFocus, gridListFocus, gridListHighlightDescription, gridListItemClick, gridListItemDoubleClick, gridListItemDragStart, gridListItemFocus, gridListItemKeyDown, gridListItemKeyDownCapture, gridListItemKeyUp, gridListItemPointerCancel, gridListItemPointerDown, gridListItemPointerUp, gridListItemProps, gridListRowIndex, gridListKeyDown, gridListKeyDownCapture, gridListMouseDown, gridListProps, gridListSectionProps, gridListSelectionCheckboxChange, gridListSelectionCheckboxProps, listStateView, } from "./behaviour.js";
|
|
56
61
|
import { hasLive, isLive, liveKeys, liveView } from "../utils/live.jsx";
|
|
57
62
|
import { liveRenderProps } from "../slots/index.jsx";
|
|
58
63
|
/** RAC `GridListContext`. */
|
|
@@ -79,8 +84,17 @@ export function GridList(inProps) {
|
|
|
79
84
|
const sharedElements = {};
|
|
80
85
|
const isVirtualized = !!renderer.isVirtualized, layoutDelegate = renderer.layoutDelegate;
|
|
81
86
|
const { locale, direction } = useLocale();
|
|
87
|
+
const content = () => (<Collection items={items} idScope={idScope}>
|
|
88
|
+
{children}
|
|
89
|
+
</Collection>);
|
|
90
|
+
// The list renders from its built collection (RAC: inside a Virtualizer only the visible window has rows; the
|
|
91
|
+
// handlers navigate the built collection, not the rendered DOM; LAZY-ROWS T3: every GridList, so its rows are the rows
|
|
92
|
+
// of the renderer's keyed `For`). A computed in the body: the compiler records it as a resumable computed (with the
|
|
93
|
+
// host's provider frame), so a resumed page rebuilds it from `content` on its first read.
|
|
94
|
+
const collection = computed(() => buildCollectionFrom(content));
|
|
82
95
|
const state = createListState({
|
|
83
96
|
id: props.id,
|
|
97
|
+
collection,
|
|
84
98
|
selectionMode: props.selectionMode,
|
|
85
99
|
selectionBehavior: props.selectionBehavior,
|
|
86
100
|
// Live props: a controlled `selectedKeys={keys()}` stays live (accessor; never written).
|
|
@@ -142,9 +156,6 @@ export function GridList(inProps) {
|
|
|
142
156
|
direction,
|
|
143
157
|
}),
|
|
144
158
|
});
|
|
145
|
-
const content = () => (<Collection items={items} idScope={idScope}>
|
|
146
|
-
{children}
|
|
147
|
-
</Collection>);
|
|
148
159
|
const itemsOf = items;
|
|
149
160
|
const isEmpty = itemsOf != null
|
|
150
161
|
? () => {
|
|
@@ -154,10 +165,7 @@ export function GridList(inProps) {
|
|
|
154
165
|
return true;
|
|
155
166
|
}
|
|
156
167
|
: renderEmptyState
|
|
157
|
-
? (
|
|
158
|
-
const built = collectionOf(content);
|
|
159
|
-
return () => built().size === 0;
|
|
160
|
-
})()
|
|
168
|
+
? () => collection().size === 0
|
|
161
169
|
: () => false;
|
|
162
170
|
const version = itemsOf != null ? (typeof itemsOf === "function" ? itemsOf : () => itemsOf) : () => null;
|
|
163
171
|
const wantsStates = isFn(props.class) || isFn(props.style) || !!renderEmptyState;
|
|
@@ -177,10 +185,11 @@ export function GridList(inProps) {
|
|
|
177
185
|
// Grid attributes (reactive): the render view only needs the size and the focused key.
|
|
178
186
|
const gridAttrs = computed(() => {
|
|
179
187
|
const empty = isEmpty();
|
|
180
|
-
const
|
|
188
|
+
const none = buildCollection([]);
|
|
181
189
|
const v = {
|
|
182
|
-
|
|
183
|
-
|
|
190
|
+
// Virtualized: the built collection (useGridList `aria-rowcount` = its size).
|
|
191
|
+
collection: isVirtualized ? collection() : { ...none, size: empty ? 0 : 1 },
|
|
192
|
+
selectionManager: listSelectionManager(state, none),
|
|
184
193
|
disabledKeys: state.disabledKeys,
|
|
185
194
|
list: shared,
|
|
186
195
|
};
|
|
@@ -195,11 +204,20 @@ export function GridList(inProps) {
|
|
|
195
204
|
list: shared,
|
|
196
205
|
});
|
|
197
206
|
};
|
|
198
|
-
const internal = { state, shared, locale, direction, keyboardNavigationBehavior, isVirtualized, version, filter };
|
|
207
|
+
const internal = { state, shared, locale, direction, keyboardNavigationBehavior, isVirtualized, version, filter, collection };
|
|
199
208
|
const dom = globalDOMProps(props, GRID_OWNED);
|
|
200
209
|
const slot = props.slot || undefined;
|
|
201
210
|
const gridEl = createRef();
|
|
202
211
|
const rootRef = mergeRefValues(ref, gridEl);
|
|
212
|
+
// Virtualized: the grid element is the scroll container (RAC scrollRef); its focused row stays rendered and its
|
|
213
|
+
// scroll events reach the renderer (the engine follows the scroll position).
|
|
214
|
+
const rootCollection = collection;
|
|
215
|
+
const focusedKey = isVirtualized ? state.selectionState.focusedKey : undefined;
|
|
216
|
+
const virtualScroll = isVirtualized ? customRenderer?.onScroll : undefined;
|
|
217
|
+
const scrolled = (e) => {
|
|
218
|
+
virtualScroll?.(e);
|
|
219
|
+
onScroll?.(e);
|
|
220
|
+
};
|
|
203
221
|
// react-aria useSelectableCollection autoFocus mount effect (retried until the grid has items).
|
|
204
222
|
if (autoFocus !== undefined)
|
|
205
223
|
clientEffect(() => {
|
|
@@ -261,13 +279,19 @@ export function GridList(inProps) {
|
|
|
261
279
|
},
|
|
262
280
|
onFocus: (e) => focusStateFocus(ring, e),
|
|
263
281
|
onBlur: (e) => focusStateBlur(ring, e),
|
|
264
|
-
onScroll: (e) =>
|
|
282
|
+
onScroll: (e) => scrolled(e),
|
|
265
283
|
children: () => [
|
|
266
284
|
<Provider values={[
|
|
267
285
|
[GridListInternalContext, internal],
|
|
268
286
|
[SharedElementContext, sharedElements],
|
|
269
287
|
]}>
|
|
270
|
-
{(customRenderer ?? DefaultCollectionRenderer).CollectionRoot({
|
|
288
|
+
{(customRenderer ?? DefaultCollectionRenderer).CollectionRoot({
|
|
289
|
+
collection: rootCollection,
|
|
290
|
+
content,
|
|
291
|
+
idScope,
|
|
292
|
+
scrollRef: gridEl,
|
|
293
|
+
focusedKey,
|
|
294
|
+
})}
|
|
271
295
|
</Provider>,
|
|
272
296
|
<Show when={isEmpty}>
|
|
273
297
|
{() => renderEmptyState ? (<div role="row" aria-rowindex={1} style={{ display: "contents" }}>
|
|
@@ -301,13 +325,19 @@ export function GridList(inProps) {
|
|
|
301
325
|
const v = view();
|
|
302
326
|
gridListMouseDown(e, eventOptions(v, e.currentTarget), v);
|
|
303
327
|
onMouseDown?.(e);
|
|
304
|
-
}} onFocus={(e) => focusStateFocus(ring, e)} onBlur={(e) => focusStateBlur(ring, e)} onScroll={onScroll ?
|
|
328
|
+
}} onFocus={(e) => focusStateFocus(ring, e)} onBlur={(e) => focusStateBlur(ring, e)} onScroll={virtualScroll || onScroll ? scrolled : undefined}>
|
|
305
329
|
<Provider values={[
|
|
306
330
|
[GridListInternalContext, internal],
|
|
307
331
|
// RAC: the items share one SharedElementTransition scope (a SelectionIndicator animates between them).
|
|
308
332
|
[SharedElementContext, sharedElements],
|
|
309
333
|
]}>
|
|
310
|
-
{(customRenderer ?? DefaultCollectionRenderer).CollectionRoot({
|
|
334
|
+
{(customRenderer ?? DefaultCollectionRenderer).CollectionRoot({
|
|
335
|
+
collection: rootCollection,
|
|
336
|
+
content,
|
|
337
|
+
idScope,
|
|
338
|
+
scrollRef: gridEl,
|
|
339
|
+
focusedKey,
|
|
340
|
+
})}
|
|
311
341
|
</Provider>
|
|
312
342
|
<Show when={isEmpty}>
|
|
313
343
|
{() => renderEmptyState ? (<div role="row" aria-rowindex={1} style={{ display: "contents" }}>
|
|
@@ -331,6 +361,7 @@ export const GridListItem = /*#__PURE__*/ createLeafComponent(ItemNode, function
|
|
|
331
361
|
if (!ctx)
|
|
332
362
|
throw new Error("GridListItem must be rendered inside a GridList.");
|
|
333
363
|
const { state, shared, locale, direction, keyboardNavigationBehavior, isVirtualized } = ctx;
|
|
364
|
+
const built = ctx.collection;
|
|
334
365
|
const key = item.key;
|
|
335
366
|
const spec = rowSpec(key, item, props);
|
|
336
367
|
// Live `isDisabled` (LIVE-SWEEP round 2): the row's spec with the current value (read in the computed / at event time).
|
|
@@ -361,6 +392,9 @@ export const GridListItem = /*#__PURE__*/ createLeafComponent(ItemNode, function
|
|
|
361
392
|
list: shared,
|
|
362
393
|
};
|
|
363
394
|
const result = gridListItemProps(itemOptions(node), v);
|
|
395
|
+
// RA useGridListItem (virtualized): the row's index in the whole collection (the one-node collection's is 1).
|
|
396
|
+
if (isVirtualized && built)
|
|
397
|
+
result.rowProps["aria-rowindex"] = gridListRowIndex(built(), key);
|
|
364
398
|
const checkbox = gridListSelectionCheckboxProps({ key, locale, id: checkboxId }, v).checkboxProps;
|
|
365
399
|
return {
|
|
366
400
|
...result,
|
|
@@ -616,6 +650,8 @@ export const GridListHeaderInnerContext = resumableContext(createContext(null, {
|
|
|
616
650
|
/** A GridListSection represents a section within a GridList (RAC `GridListSection`). */
|
|
617
651
|
export const GridListSection = /*#__PURE__*/ createBranchComponent(SectionNode, function GridListSection(props, forwardedRef, item) {
|
|
618
652
|
const { CollectionBranch } = useContext(CollectionRendererContext);
|
|
653
|
+
// The grid list's built collection (rows render from it); null outside a GridList (live content).
|
|
654
|
+
const collection = (useContext(GridListInternalContext)?.collection ?? null);
|
|
619
655
|
const heading = createSlotId();
|
|
620
656
|
const ariaLabel = props["aria-label"];
|
|
621
657
|
const aria = gridListSectionProps({ "aria-label": ariaLabel, headingId: heading.id });
|
|
@@ -639,7 +675,7 @@ export const GridListSection = /*#__PURE__*/ createBranchComponent(SectionNode,
|
|
|
639
675
|
[GridListHeaderContext, { ...aria.rowProps }],
|
|
640
676
|
[GridListHeaderInnerContext, { ...aria.rowHeaderProps, id: heading }],
|
|
641
677
|
]}>
|
|
642
|
-
{CollectionBranch({ collection
|
|
678
|
+
{CollectionBranch({ collection, parent: item })}
|
|
643
679
|
</Provider>),
|
|
644
680
|
};
|
|
645
681
|
return props.render(attrs, undefined);
|
|
@@ -649,7 +685,7 @@ export const GridListSection = /*#__PURE__*/ createBranchComponent(SectionNode,
|
|
|
649
685
|
[GridListHeaderContext, { ...aria.rowProps }],
|
|
650
686
|
[GridListHeaderInnerContext, { ...aria.rowHeaderProps, id: heading }],
|
|
651
687
|
]}>
|
|
652
|
-
{CollectionBranch({ collection
|
|
688
|
+
{CollectionBranch({ collection, parent: item })}
|
|
653
689
|
</Provider>
|
|
654
690
|
</div>);
|
|
655
691
|
});
|
package/interactions/press.jsx
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
import { createContext, signal, useContext } from "@rivetjs/reactivity";
|
|
24
24
|
import { resumableContext } from "@rivetjs/runtime/resume-format";
|
|
25
25
|
import { mergeProps } from "./merge-props.jsx";
|
|
26
|
-
import { disableTextSelection, focusWithoutScrolling, getEventTarget, getOwnerDocument, isMac, isVirtualClick, isVirtualPointerEvent, linkState, nodeContains, nonTextInputTypes,
|
|
26
|
+
import { disableTextSelection, focusWithoutScrolling, getEventTarget, getOwnerDocument, isMac, isVirtualClick, isVirtualPointerEvent, linkState, nodeContains, nonTextInputTypes, preventFocus, read, restoreTextSelection, } from "./utils.js";
|
|
27
|
+
import { openLink } from "../utils/links.jsx";
|
|
27
28
|
/** `Press.track`: bind `dragstart` while pressed. */
|
|
28
29
|
export const PRESS_TRACK_DRAG = 1;
|
|
29
30
|
/** `Press.track`: bind `pointerenter` / `pointerleave` while pressed. */
|
package/interactions/utils.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
export { isAndroid, isChrome, isFirefox, isIOS, isIPad, isIPhone, isMac, isWebKit, isVirtualClick, isVirtualPointerEvent } from "../platform/index.js";
|
|
2
2
|
export { getActiveElement, getOwnerDocument, getOwnerWindow, isFocusWithin, isShadowRoot, nodeContains } from "../utils/dom.js";
|
|
3
|
-
export { focusWithoutScrolling, runAfterTransition } from "../utils/
|
|
4
|
-
export {
|
|
5
|
-
export { linkState, openLink, type Modifiers } from "../utils/links.jsx";
|
|
3
|
+
export { focusWithoutScrolling, runAfterTransition } from "../utils/transitions.js";
|
|
4
|
+
export { linkState } from "../utils/link-state.js";
|
|
6
5
|
export type PointerType = "mouse" | "pen" | "touch" | "keyboard" | "virtual";
|
|
7
6
|
export type FocusableElement = HTMLElement | SVGElement;
|
|
8
7
|
/** A value or an accessor of it (signals qualify): options are read at event time. */
|