@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/interactions/utils.js
CHANGED
|
@@ -5,18 +5,19 @@
|
|
|
5
5
|
//
|
|
6
6
|
// One implementation per helper: platform probes come from `../platform` (return `undefined` on the
|
|
7
7
|
// server), DOM / shadow-DOM functions from `../utils/dom.ts`, `runAfterTransition` /
|
|
8
|
-
// `focusWithoutScrolling` from `../utils/
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// `focusWithoutScrolling` from the leaf `../utils/transitions.ts`, `linkState` from the leaf
|
|
9
|
+
// `../utils/link-state.ts` (the flag usePress and focus-visible read is the one `openLink` sets).
|
|
10
|
+
// They are re-exported here for the hooks. Leaves only (DOCS-COLD 2026-10-20): `chain` (`./merge-props.tsx`)
|
|
11
|
+
// and `openLink` (`../utils/links.tsx`) are imported from their modules, so a focus / overlay helper
|
|
12
|
+
// importing this file does not pull prop merging or the router module into its chunk.
|
|
11
13
|
import { getEventTarget as domEventTarget } from "../utils/dom.js";
|
|
12
14
|
import { isIOS, isWebKit } from "../platform/index.js";
|
|
13
15
|
import { getActiveElement, getOwnerDocument, getOwnerWindow, isShadowRoot, nodeContains } from "../utils/dom.js";
|
|
14
|
-
import { focusWithoutScrolling, runAfterTransition } from "../utils/
|
|
16
|
+
import { focusWithoutScrolling, runAfterTransition } from "../utils/transitions.js";
|
|
15
17
|
export { isAndroid, isChrome, isFirefox, isIOS, isIPad, isIPhone, isMac, isWebKit, isVirtualClick, isVirtualPointerEvent } from "../platform/index.js";
|
|
16
18
|
export { getActiveElement, getOwnerDocument, getOwnerWindow, isFocusWithin, isShadowRoot, nodeContains } from "../utils/dom.js";
|
|
17
|
-
export { focusWithoutScrolling, runAfterTransition } from "../utils/
|
|
18
|
-
export {
|
|
19
|
-
export { linkState, openLink } from "../utils/links.jsx";
|
|
19
|
+
export { focusWithoutScrolling, runAfterTransition } from "../utils/transitions.js";
|
|
20
|
+
export { linkState } from "../utils/link-state.js";
|
|
20
21
|
/** Read a `MaybeAccessor` (functions are called; signals are functions). */
|
|
21
22
|
export function read(value) {
|
|
22
23
|
return typeof value === "function" ? value() : value;
|
package/listbox/behaviour.jsx
CHANGED
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
// list's `onAction(key)`, then the bubbling `react-aria-item-action` (useSelectableItem).
|
|
18
18
|
// - Option hover callbacks: `rv-hover-start` / `rv-hover-end` events on the option element (same scheme).
|
|
19
19
|
// - Items marked `data-rv-long-press` (a long press interaction, `@rivetjs/cdk/morph/item`) select on press up.
|
|
20
|
-
// Deviation: no drag and drop (C8)
|
|
20
|
+
// Deviation: no drag and drop (C8).
|
|
21
21
|
import { signal, untrack } from "@rivetjs/reactivity";
|
|
22
22
|
import { listCollection, listSelectionManager } from "../collections/list-state.js";
|
|
23
|
+
import { itemPosition } from "../collections/nodes.js";
|
|
23
24
|
import { SelectionManager } from "../selection/manager.js";
|
|
24
25
|
import { isFocusedKey } from "../selection/keyed.js";
|
|
25
26
|
import { selectableCollectionAutoFocus, selectableCollectionBlur, selectableCollectionClearVirtualFocus, selectableCollectionFocus, selectableCollectionKeyDown, selectableCollectionKeyDownCapture, selectableCollectionMouseDown, selectableCollectionVirtualFocus, } from "../selection/selectable-collection.js";
|
|
@@ -179,20 +180,28 @@ export function optionProps(props, state, data) {
|
|
|
179
180
|
const states = () => selectableItemStates(options());
|
|
180
181
|
const isFocused = () => states().isFocused;
|
|
181
182
|
const id = getItemId(data.id, key);
|
|
183
|
+
const attrs = {
|
|
184
|
+
id,
|
|
185
|
+
role: "option",
|
|
186
|
+
"data-key": String(key),
|
|
187
|
+
"data-key-type": typeof key === "number" ? "number" : undefined,
|
|
188
|
+
"data-collection": data.id,
|
|
189
|
+
"aria-label": props["aria-label"],
|
|
190
|
+
"aria-labelledby": props.labelId,
|
|
191
|
+
"aria-describedby": props.descriptionId,
|
|
192
|
+
"aria-disabled": () => (states().isDisabled ? "true" : undefined),
|
|
193
|
+
"aria-selected": () => (state.selectionState.selectionMode === "none" ? undefined : states().isSelected ? "true" : "false"),
|
|
194
|
+
tabIndex: () => (read(data.shouldUseVirtualFocus) || states().isDisabled ? undefined : isFocusedKey(state.selectionState, key) ? 0 : -1),
|
|
195
|
+
};
|
|
196
|
+
if (data.isVirtualized)
|
|
197
|
+
// useOption: a virtualized list renders a window of its options; their position comes from the collection.
|
|
198
|
+
// (Object-literal arrows: resumable closures.)
|
|
199
|
+
Object.assign(attrs, {
|
|
200
|
+
"aria-posinset": () => optionPosInSet(state, key),
|
|
201
|
+
"aria-setsize": () => optionSetSize(state, key),
|
|
202
|
+
});
|
|
182
203
|
return {
|
|
183
|
-
optionProps:
|
|
184
|
-
id,
|
|
185
|
-
role: "option",
|
|
186
|
-
"data-key": String(key),
|
|
187
|
-
"data-key-type": typeof key === "number" ? "number" : undefined,
|
|
188
|
-
"data-collection": data.id,
|
|
189
|
-
"aria-label": props["aria-label"],
|
|
190
|
-
"aria-labelledby": props.labelId,
|
|
191
|
-
"aria-describedby": props.descriptionId,
|
|
192
|
-
"aria-disabled": () => (states().isDisabled ? "true" : undefined),
|
|
193
|
-
"aria-selected": () => (state.selectionState.selectionMode === "none" ? undefined : states().isSelected ? "true" : "false"),
|
|
194
|
-
tabIndex: () => (read(data.shouldUseVirtualFocus) || states().isDisabled ? undefined : isFocusedKey(state.selectionState, key) ? 0 : -1),
|
|
195
|
-
},
|
|
204
|
+
optionProps: attrs,
|
|
196
205
|
isSelected: () => states().isSelected,
|
|
197
206
|
isDisabled: () => states().isDisabled,
|
|
198
207
|
isFocused,
|
|
@@ -203,6 +212,20 @@ export function optionProps(props, state, data) {
|
|
|
203
212
|
hasAction: () => states().hasAction,
|
|
204
213
|
};
|
|
205
214
|
}
|
|
215
|
+
/** The built collection of a list state (a virtualized ListBox renders from it). */
|
|
216
|
+
function builtOf(state) {
|
|
217
|
+
const c = state.collection;
|
|
218
|
+
return typeof c === "function" ? c() : c;
|
|
219
|
+
}
|
|
220
|
+
// Position within the option's section (`itemPosition`: sections are groups; RA counts the header in `index`).
|
|
221
|
+
const optionPosInSet = (state, key) => {
|
|
222
|
+
const c = builtOf(state);
|
|
223
|
+
return c ? itemPosition(c, key)?.[0] : undefined;
|
|
224
|
+
};
|
|
225
|
+
const optionSetSize = (state, key) => {
|
|
226
|
+
const c = builtOf(state);
|
|
227
|
+
return c ? itemPosition(c, key)?.[1] : undefined;
|
|
228
|
+
};
|
|
206
229
|
function rootOf(e) {
|
|
207
230
|
return e.currentTarget;
|
|
208
231
|
}
|
package/listbox/index.jsx
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
// Parts: ListBox, ListBoxItem, ListBoxSection, ListBoxLoadMoreItem (+ Header / Text / Separator / Collection /
|
|
3
3
|
// SelectionIndicator inside); contexts ListBoxContext, ListStateContext.
|
|
4
4
|
// Rivet:
|
|
5
|
-
// -
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// - A standalone ListBox builds its collection (`computed(buildCollectionFrom(content))` in the body, B5 build mode;
|
|
6
|
+
// LAZY-ROWS T3, 2026-10-19 — live mode before) and renders node by node (`CollectionRoot({ collection })`): options
|
|
7
|
+
// are the rows of the renderer's keyed `For` (`(key) => renderNodeAt(collection, key)`, lazy-row eligible), the
|
|
8
|
+
// handlers navigate the built collection; a resumed page rebuilds it from the content on first need (no per-option
|
|
9
|
+
// state). A ListStateContext from a parent (Select, ComboBox) whose state carries a built `collection` renders from
|
|
10
|
+
// that one, filtered by the SelectableCollectionContext `filter`.
|
|
9
11
|
// - One delegated handler set on the listbox element runs useSelectableList + every option's useSelectableItem /
|
|
10
12
|
// useHover (`./behaviour.tsx`); option `data-hovered` / `data-pressed` are written by those handlers, root
|
|
11
13
|
// `data-focused` / `data-focus-visible` bind to the ListData focus signals they set; option `data-selected` / `data-focused` / `data-focus-visible` / `data-disabled` bind
|
|
@@ -17,14 +19,13 @@
|
|
|
17
19
|
// objects with a `children` array → `ListBoxSection` + `Header`).
|
|
18
20
|
// - Option label / description slot ids (useSlotId) are created only for non-text children (text children can hold
|
|
19
21
|
// no `<Text slot>`: RA's effect would drop the ids anyway).
|
|
20
|
-
// - `isEmpty`: `items` length, else the built size of the children when `renderEmptyState` is given
|
|
21
|
-
// else false; with a parent state, its collection size.
|
|
22
|
+
// - `isEmpty`: `items` length, else the built size of the children when `renderEmptyState` is given, else false; with a parent state, its collection size.
|
|
22
23
|
// `render` (RAC DOMRenderProps) on ListBox (`<div>`), ListBoxSection (`<section>`) and ListBoxItem (`<div>` / `<a>`): given, the attributes object goes to `render(attrs, renderProps)`
|
|
23
24
|
// (custom events as `on<Type>` keys: `onRv-item-press`). Deviations: no drag and drop (`dragAndDropHooks`, drop indicators: C8); `ListBoxContext` is not re-provided to the
|
|
24
25
|
// options (its value holds the JSX children: not serializable); no FocusScope; the SharedElementTransition scope is
|
|
25
26
|
// a `SharedElementContext` provide;
|
|
26
27
|
// live-mode filtering hides options but keeps empty sections.
|
|
27
|
-
import { createContext, provide, untrack, useContext } from "@rivetjs/reactivity";
|
|
28
|
+
import { computed, createContext, provide, untrack, useContext } from "@rivetjs/reactivity";
|
|
28
29
|
import { Show, clientEffect, ref as createRef } from "@rivetjs/runtime/dom";
|
|
29
30
|
import { isSlot } from "@rivetjs/runtime/slot";
|
|
30
31
|
import { ItemNode, LoaderNode, SectionNode } from "../collections/base.js";
|
|
@@ -105,7 +106,16 @@ export function ListBox(inProps) {
|
|
|
105
106
|
}
|
|
106
107
|
function StandaloneListBox(input) {
|
|
107
108
|
const { props, listBoxRef, children } = input;
|
|
108
|
-
const
|
|
109
|
+
const renderer = useContext(CollectionRendererContext);
|
|
110
|
+
const layoutDelegate = renderer.layoutDelegate;
|
|
111
|
+
const items = props.items;
|
|
112
|
+
const dependencies = props.dependencies;
|
|
113
|
+
const content = () => Collection({ items: items, children, dependencies });
|
|
114
|
+
// The list renders from its built collection (RAC: inside a Virtualizer only the visible window has options; LAZY-ROWS
|
|
115
|
+
// T3: every standalone list, so its options are the rows of the renderer's keyed `For`, `(key) => renderNodeAt(…)`).
|
|
116
|
+
// A computed in the body (the compiler records it as a resumable computed with the host's provider frame): a resumed
|
|
117
|
+
// page rebuilds it from `content` on its first read, so no per-option state ships.
|
|
118
|
+
const collection = computed(() => buildCollectionFrom(content));
|
|
109
119
|
// Live props: a controlled `selectedKeys={keys()}` stays live (accessor; never written); the copy reads no
|
|
110
120
|
// `selectedKeys` getter (a resumed region is not re-created on a change).
|
|
111
121
|
const selectedKeys = liveKeys(props, "selectedKeys");
|
|
@@ -117,16 +127,13 @@ function StandaloneListBox(input) {
|
|
|
117
127
|
id: props.id ?? mergedIdOf(useContext(SelectableCollectionContext)?.id) ?? createId(),
|
|
118
128
|
items: undefined,
|
|
119
129
|
children: undefined,
|
|
120
|
-
collection
|
|
130
|
+
collection,
|
|
121
131
|
layoutDelegate,
|
|
122
132
|
});
|
|
123
|
-
const items = props.items;
|
|
124
|
-
const dependencies = props.dependencies;
|
|
125
|
-
const content = () => Collection({ items: items, children, dependencies });
|
|
126
133
|
// isEmpty: the items' length, or (with an empty state to show) the size of a built collection of the children.
|
|
127
134
|
const empty = props.renderEmptyState;
|
|
128
135
|
// Static children do not change: their emptiness is computed once (a build pass, only with an empty state).
|
|
129
|
-
const staticEmpty = items != null ? false : empty && children != null ? untrack(() =>
|
|
136
|
+
const staticEmpty = items != null ? false : empty && children != null ? untrack(() => collection().size === 0) : children == null;
|
|
130
137
|
const isEmpty = () => (items != null ? isEmptyIterable(typeof items === "function" ? items() : items) : staticEmpty);
|
|
131
138
|
return <ListBoxInner state={state} props={own} listBoxRef={listBoxRef} content={content} isEmpty={isEmpty}/>;
|
|
132
139
|
}
|
|
@@ -149,6 +156,12 @@ function ListBoxInner(input) {
|
|
|
149
156
|
// RAC mergeIds: the ListBox's own id wins over the parent's (ComboBox listbox / Autocomplete collection id).
|
|
150
157
|
const id = props.id || state.id;
|
|
151
158
|
const data = createListData(props, id, state.collection ? undefined : filter, useContext(RouterContext));
|
|
159
|
+
const renderer = useContext(CollectionRendererContext);
|
|
160
|
+
// Resumable (as GridList): the render path's lazy collection slot captures `customRenderer`, undefined for the default renderer.
|
|
161
|
+
const customRenderer = renderer === DefaultCollectionRenderer ? undefined : renderer;
|
|
162
|
+
const isVirtualized = !!renderer.isVirtualized && !!state.collection;
|
|
163
|
+
if (isVirtualized)
|
|
164
|
+
data.isVirtualized = true;
|
|
152
165
|
// A parent's built collection (filtered like RAC UNSTABLE_useFilteredListState); live mode: none.
|
|
153
166
|
const provided = state.collection;
|
|
154
167
|
const builtCollection = () => {
|
|
@@ -171,6 +184,7 @@ function ListBoxInner(input) {
|
|
|
171
184
|
onFocus: props.onFocus,
|
|
172
185
|
onBlur: props.onBlur,
|
|
173
186
|
onFocusChange: props.onFocusChange,
|
|
187
|
+
isVirtualized: isVirtualized || undefined,
|
|
174
188
|
};
|
|
175
189
|
// mergeIds: a parent's label self-reference (ComboBox `labelsProps(listBoxId, …)`) follows the merged id.
|
|
176
190
|
// A host's accessor (Select: the menu is labelled by its Label only while one renders) is read in the binding.
|
|
@@ -212,13 +226,18 @@ function ListBoxInner(input) {
|
|
|
212
226
|
});
|
|
213
227
|
}
|
|
214
228
|
const setRef = mergeRefValues(root, userRef);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
229
|
+
// Virtualized: the listbox element is the scroll container (RAC scrollRef); its focused option stays rendered.
|
|
230
|
+
const focusedKey = isVirtualized ? state.selectionState.focusedKey : undefined;
|
|
231
|
+
const virtualScroll = isVirtualized ? customRenderer?.onScroll : undefined;
|
|
232
|
+
const virtualScrolled = (e) => {
|
|
233
|
+
virtualScroll?.(e);
|
|
234
|
+
onScroll?.(e);
|
|
235
|
+
};
|
|
236
|
+
const scrolled = virtualScroll ? virtualScrolled : onScroll;
|
|
218
237
|
const sharedElements = {};
|
|
219
238
|
if (props.render) {
|
|
220
|
-
// The body is a compiled `<Provider>` binding (resumable, as Menu
|
|
221
|
-
//
|
|
239
|
+
// The body is a compiled `<Provider>` binding (resumable, as Menu; the default path below uses the same one): a render
|
|
240
|
+
// closure capturing an eager `provide()` chain's result cannot be serialized.
|
|
222
241
|
const renderBody = (<Provider values={[
|
|
223
242
|
[ListStateContext, state],
|
|
224
243
|
[ListBoxDataContext, data],
|
|
@@ -226,7 +245,7 @@ function ListBoxInner(input) {
|
|
|
226
245
|
[SectionContext, { name: "ListBoxSection", render: ListBoxSectionInner }],
|
|
227
246
|
[SharedElementContext, sharedElements],
|
|
228
247
|
]}>
|
|
229
|
-
{(customRenderer ?? DefaultCollectionRenderer).CollectionRoot({ collection, content, scrollRef: root })}
|
|
248
|
+
{(customRenderer ?? DefaultCollectionRenderer).CollectionRoot({ collection, content, scrollRef: root, focusedKey })}
|
|
230
249
|
</Provider>);
|
|
231
250
|
const attrs = {
|
|
232
251
|
...dom,
|
|
@@ -303,7 +322,7 @@ function ListBoxInner(input) {
|
|
|
303
322
|
listBoxDragStart(e, behaviour, state);
|
|
304
323
|
onDragStart?.(e);
|
|
305
324
|
},
|
|
306
|
-
onScroll: (e) =>
|
|
325
|
+
onScroll: (e) => scrolled?.(e),
|
|
307
326
|
// Custom events: the spread takes `on<Type>` keys (no `on:` form).
|
|
308
327
|
"onReact-aria-focus": (e) => listBoxVirtualFocusEvent(e, behaviour, state),
|
|
309
328
|
"onReact-aria-clear-focus": (e) => listBoxClearFocusEvent(e, behaviour, state),
|
|
@@ -318,9 +337,6 @@ function ListBoxInner(input) {
|
|
|
318
337
|
};
|
|
319
338
|
return props.render(attrs, values);
|
|
320
339
|
}
|
|
321
|
-
const body = provide(ListStateContext, state, () => provide(ListBoxDataContext, data, () => provide(SeparatorContext, { elementType: "div" }, () => provide(SectionContext, { name: "ListBoxSection", render: ListBoxSectionInner }, () =>
|
|
322
|
-
// RAC: the options share one SharedElementTransition scope (a SelectionIndicator animates between them).
|
|
323
|
-
provide(SharedElementContext, sharedElements, () => renderer.CollectionRoot({ collection, content, scrollRef: root }))))));
|
|
324
340
|
return (<div {...dom} ref={setRef} id={id} role="listbox" preload="visible" slot={slot} class={rp.class()} style={rp.style()} data-collection={id} aria-label={ariaLabel} aria-labelledby={labelledByAccessor ? labelledByAccessor() : ariaLabelledby} aria-describedby={ariaDescribedby} aria-details={ariaDetails} aria-orientation={orientation} aria-multiselectable={state.selectionState.selectionMode === "multiple" ? "true" : undefined} tabIndex={tabIndex()} data-empty={dataBool(isEmpty())} data-focused={dataBool(data.isFocused())} data-focus-visible={dataBool(data.isFocused() && data.focusVisible())} data-layout={layout} data-orientation={orientation} data-rk={listBoxNavigationKeys(layout, orientation)} onKeyDownCapture={(e) => listBoxKeyDownCapture(e, behaviour, state)} onKeyDown={(e) => {
|
|
325
341
|
onKeyDown?.(e);
|
|
326
342
|
listBoxKeyDown(e, behaviour, state);
|
|
@@ -360,8 +376,21 @@ function ListBoxInner(input) {
|
|
|
360
376
|
}} onDragStart={(e) => {
|
|
361
377
|
listBoxDragStart(e, behaviour, state);
|
|
362
378
|
onDragStart?.(e);
|
|
363
|
-
}} onScroll={
|
|
364
|
-
{
|
|
379
|
+
}} onScroll={scrolled} on:react-aria-focus={(e) => listBoxVirtualFocusEvent(e, behaviour, state)} on:react-aria-clear-focus={(e) => listBoxClearFocusEvent(e, behaviour, state)}>
|
|
380
|
+
{/* A compiled `<Provider>` (as the `render` path, GridList, Table): the renderer's keyed `For` is created inside its
|
|
381
|
+
child region, so the reads of its `each` (the collection: items, Autocomplete filter) re-run only that region on a
|
|
382
|
+
resumed page. An eager provide() chain in the body created the For in the ListBox's enclosing region: its first
|
|
383
|
+
change re-created the whole ListBox (a fresh state whose focus the adopted options' resumed bindings never see). */}
|
|
384
|
+
<Provider values={[
|
|
385
|
+
[ListStateContext, state],
|
|
386
|
+
[ListBoxDataContext, data],
|
|
387
|
+
[SeparatorContext, { elementType: "div" }],
|
|
388
|
+
[SectionContext, { name: "ListBoxSection", render: ListBoxSectionInner }],
|
|
389
|
+
// RAC: the options share one SharedElementTransition scope (a SelectionIndicator animates between them).
|
|
390
|
+
[SharedElementContext, sharedElements],
|
|
391
|
+
]}>
|
|
392
|
+
{(customRenderer ?? DefaultCollectionRenderer).CollectionRoot({ collection, content, scrollRef: root, focusedKey })}
|
|
393
|
+
</Provider>
|
|
365
394
|
{renderEmptyState ? (<Show when={isEmpty}>
|
|
366
395
|
{() => (<div role="option" style={{ display: "contents" }}>
|
|
367
396
|
{renderEmptyState(values)}
|
|
@@ -495,6 +524,9 @@ function listBoxItem(props, forwardedRef, item, state, data) {
|
|
|
495
524
|
const ariaDisabled = optionAttrs["aria-disabled"];
|
|
496
525
|
const ariaSelected = optionAttrs["aria-selected"];
|
|
497
526
|
const tabIndex = optionAttrs.tabIndex;
|
|
527
|
+
// useOption (virtualized list): the option's position in the whole collection.
|
|
528
|
+
const posInSet = optionAttrs["aria-posinset"];
|
|
529
|
+
const setSize = optionAttrs["aria-setsize"];
|
|
498
530
|
const ref = forwardedRef;
|
|
499
531
|
// `ping` / `referrerpolicy` are missing from the anchor typings: spread.
|
|
500
532
|
// `data-rv-disabled` (the item's own isDisabled, read by DOM collections) goes through the spread: an accessor when live.
|
|
@@ -520,6 +552,8 @@ function listBoxItem(props, forwardedRef, item, state, data) {
|
|
|
520
552
|
"aria-describedby": () => describedBy?.(),
|
|
521
553
|
"aria-disabled": () => ariaDisabled(),
|
|
522
554
|
"aria-selected": () => ariaSelected(),
|
|
555
|
+
"aria-posinset": () => posInSet?.(),
|
|
556
|
+
"aria-setsize": () => setSize?.(),
|
|
523
557
|
tabindex: () => (href ? (tabIndex() ?? -1) : tabIndex()),
|
|
524
558
|
"data-selected": () => dataBool(opt.isSelected()),
|
|
525
559
|
"data-disabled": () => dataBool(opt.isDisabled()),
|
|
@@ -537,9 +571,9 @@ function listBoxItem(props, forwardedRef, item, state, data) {
|
|
|
537
571
|
};
|
|
538
572
|
return props.render(attrs, values);
|
|
539
573
|
}
|
|
540
|
-
return href ? (<a {...linkDom} ref={ref} id={id} role="option" class={rp.class()} style={rp.style()} href={href} hreflang={hrefLang} target={target} rel={rel} download={download === true ? "" : download || undefined} data-key={String(key)} data-key-type={keyType} data-collection={data.id} data-text-value={textValue} data-rv-action={onAction ? "" : undefined} data-rv-press={hasPress ? "" : undefined} aria-label={ariaLabel} aria-labelledby={labelledBy?.()} aria-describedby={describedBy?.()} aria-disabled={ariaDisabled()} aria-selected={ariaSelected()} tabIndex={tabIndex() ?? -1} data-selected={dataBool(opt.isSelected())} data-disabled={dataBool(opt.isDisabled())} data-focused={dataBool(opt.isFocused())} data-focus-visible={dataBool(opt.isFocusVisible())} data-selection-mode={selectionMode === "none" ? undefined : selectionMode} onKeyDown={itemKeyDown ? (e) => keyboardKeyDown({ onKeyDown: itemKeyDown }, e) : undefined} onKeyUp={itemKeyUp ? (e) => keyboardKeyUp({ onKeyUp: itemKeyUp }, e) : undefined} on:rv-item-action={onAction ? () => onAction() : undefined} on:rv-item-press={hasPress ? (e) => itemPressEvent(e, onPressStart, onPressEnd, onPressChange, onPress, onPressUp, onClick) : undefined} on:rv-hover-start={hasHover ? (e) => hoverStart(e, onHoverStart, onHoverChange) : undefined} on:rv-hover-end={hasHover ? (e) => hoverEnd(e, onHoverEnd, onHoverChange) : undefined}>
|
|
574
|
+
return href ? (<a {...linkDom} ref={ref} id={id} role="option" class={rp.class()} style={rp.style()} href={href} hreflang={hrefLang} target={target} rel={rel} download={download === true ? "" : download || undefined} data-key={String(key)} data-key-type={keyType} data-collection={data.id} data-text-value={textValue} data-rv-action={onAction ? "" : undefined} data-rv-press={hasPress ? "" : undefined} aria-label={ariaLabel} aria-labelledby={labelledBy?.()} aria-describedby={describedBy?.()} aria-disabled={ariaDisabled()} aria-selected={ariaSelected()} aria-posinset={posInSet?.()} aria-setsize={setSize?.()} tabIndex={tabIndex() ?? -1} data-selected={dataBool(opt.isSelected())} data-disabled={dataBool(opt.isDisabled())} data-focused={dataBool(opt.isFocused())} data-focus-visible={dataBool(opt.isFocusVisible())} data-selection-mode={selectionMode === "none" ? undefined : selectionMode} onKeyDown={itemKeyDown ? (e) => keyboardKeyDown({ onKeyDown: itemKeyDown }, e) : undefined} onKeyUp={itemKeyUp ? (e) => keyboardKeyUp({ onKeyUp: itemKeyUp }, e) : undefined} on:rv-item-action={onAction ? () => onAction() : undefined} on:rv-item-press={hasPress ? (e) => itemPressEvent(e, onPressStart, onPressEnd, onPressChange, onPress, onPressUp, onClick) : undefined} on:rv-hover-start={hasHover ? (e) => hoverStart(e, onHoverStart, onHoverChange) : undefined} on:rv-hover-end={hasHover ? (e) => hoverEnd(e, onHoverEnd, onHoverChange) : undefined}>
|
|
541
575
|
{content()}
|
|
542
|
-
</a>) : (<div {...linkDom} ref={ref} id={id} role="option" class={rp.class()} style={rp.style()} data-key={String(key)} data-key-type={keyType} data-collection={data.id} data-text-value={textValue} data-rv-action={onAction ? "" : undefined} data-rv-press={hasPress ? "" : undefined} aria-label={ariaLabel} aria-labelledby={labelledBy?.()} aria-describedby={describedBy?.()} aria-disabled={ariaDisabled()} aria-selected={ariaSelected()} tabIndex={tabIndex()} data-selected={dataBool(opt.isSelected())} data-disabled={dataBool(opt.isDisabled())} data-focused={dataBool(opt.isFocused())} data-focus-visible={dataBool(opt.isFocusVisible())} data-selection-mode={selectionMode === "none" ? undefined : selectionMode} onKeyDown={itemKeyDown ? (e) => keyboardKeyDown({ onKeyDown: itemKeyDown }, e) : undefined} onKeyUp={itemKeyUp ? (e) => keyboardKeyUp({ onKeyUp: itemKeyUp }, e) : undefined} on:rv-item-action={onAction ? () => onAction() : undefined} on:rv-item-press={hasPress ? (e) => itemPressEvent(e, onPressStart, onPressEnd, onPressChange, onPress, onPressUp, onClick) : undefined} on:rv-hover-start={hasHover ? (e) => hoverStart(e, onHoverStart, onHoverChange) : undefined} on:rv-hover-end={hasHover ? (e) => hoverEnd(e, onHoverEnd, onHoverChange) : undefined}>
|
|
576
|
+
</a>) : (<div {...linkDom} ref={ref} id={id} role="option" class={rp.class()} style={rp.style()} data-key={String(key)} data-key-type={keyType} data-collection={data.id} data-text-value={textValue} data-rv-action={onAction ? "" : undefined} data-rv-press={hasPress ? "" : undefined} aria-label={ariaLabel} aria-labelledby={labelledBy?.()} aria-describedby={describedBy?.()} aria-disabled={ariaDisabled()} aria-selected={ariaSelected()} aria-posinset={posInSet?.()} aria-setsize={setSize?.()} tabIndex={tabIndex()} data-selected={dataBool(opt.isSelected())} data-disabled={dataBool(opt.isDisabled())} data-focused={dataBool(opt.isFocused())} data-focus-visible={dataBool(opt.isFocusVisible())} data-selection-mode={selectionMode === "none" ? undefined : selectionMode} onKeyDown={itemKeyDown ? (e) => keyboardKeyDown({ onKeyDown: itemKeyDown }, e) : undefined} onKeyUp={itemKeyUp ? (e) => keyboardKeyUp({ onKeyUp: itemKeyUp }, e) : undefined} on:rv-item-action={onAction ? () => onAction() : undefined} on:rv-item-press={hasPress ? (e) => itemPressEvent(e, onPressStart, onPressEnd, onPressChange, onPress, onPressUp, onClick) : undefined} on:rv-hover-start={hasHover ? (e) => hoverStart(e, onHoverStart, onHoverChange) : undefined} on:rv-hover-end={hasHover ? (e) => hoverEnd(e, onHoverEnd, onHoverChange) : undefined}>
|
|
543
577
|
{content()}
|
|
544
578
|
</div>);
|
|
545
579
|
};
|
|
@@ -576,8 +610,10 @@ export const ListBoxLoadMoreItem = /*#__PURE__*/ createLeafComponent(LoaderNode,
|
|
|
576
610
|
const state = useContext(ListStateContext);
|
|
577
611
|
const { isLoading, onLoadMore, scrollOffset } = props;
|
|
578
612
|
const sentinel = createRef();
|
|
579
|
-
// RAC passes `state.collection
|
|
580
|
-
|
|
613
|
+
// RAC passes `state.collection` (re-created on every ListBox render: a loading change re-arms the observer); Rivet: the
|
|
614
|
+
// sentinel re-arms when the collection or the loading state changes (a live `isLoading` does not rebuild the collection).
|
|
615
|
+
const built = state?.collection;
|
|
616
|
+
const collection = () => (read(isLoading), built ? resolveCollection(built) : null);
|
|
581
617
|
loadMoreSentinel({ collection, onLoadMore, scrollOffset }, sentinel);
|
|
582
618
|
const dom = globalDOMProps(props, ["style"]);
|
|
583
619
|
const cls = props.class ?? "rv-ListBoxLoadingIndicator";
|
package/menu/index.jsx
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
// (useSafelyMouseToSubmenu) attaches while the submenu is mounted.
|
|
20
20
|
// - render props are accessors; `data-hovered` / `data-pressed` / `data-focus-visible` are written by the
|
|
21
21
|
// delegated handler (signals only when a `class` / `style` / `children` function asks for them).
|
|
22
|
+
// - Inside a `<CollectionVirtualizer layout={ListLayout}>` the menu builds its collection (`builtCollectionOf`): the
|
|
23
|
+
// state's `virtual` (built collection + layout delegate) makes the delegated handler navigate it, items render as
|
|
24
|
+
// views with `aria-posinset` / `aria-setsize`, the menu element is the scroll container. Submenus: not covered.
|
|
22
25
|
// - `class` = RAC `className` (defaults `rv-Menu`, `rv-MenuItem`, `rv-MenuSection`, `rv-MenuLoadingIndicator`).
|
|
23
26
|
// - SelectableCollectionContext (Autocomplete) merges under the menu props: `shouldUseVirtualFocus` /
|
|
24
27
|
// `disallowTypeAhead` / `autoFocus` may be accessors (read in bindings; a client effect mirrors them into
|
|
@@ -26,8 +29,8 @@
|
|
|
26
29
|
// `Show`, live collection); `onFocusIn` goes on the menu element; `react-aria-focus` /
|
|
27
30
|
// `react-aria-clear-focus` are handled on the menu element.
|
|
28
31
|
// `render` (RAC DOMRenderProps) on Menu (`<div>`), MenuSection (`<section>`) and MenuItem (`<div>` / `<a>`): given, the attributes object goes to `render(attrs, renderProps)`. Deviations: no FocusScope around the menu (the popover's focus scope contains focus;
|
|
29
|
-
// a standalone menu has no FocusManager);
|
|
30
|
-
//
|
|
32
|
+
// a standalone menu has no FocusManager); `isEmpty` counts rendered (visible) items (live collection; a
|
|
33
|
+
// virtualized menu: its rendered window); live-mode filtering keeps empty sections (RAC drops them); no `--trigger-width` on the popover.
|
|
31
34
|
import { _regionState, onCleanup, signal, untrack, useContext } from "@rivetjs/reactivity";
|
|
32
35
|
import { Show, clientEffect, ref as createRef } from "@rivetjs/runtime/dom";
|
|
33
36
|
import { isSlot } from "@rivetjs/runtime/slot";
|
|
@@ -39,7 +42,8 @@ import { globalDOMProps } from "../utils/dom-props.js";
|
|
|
39
42
|
import { dataBool } from "../utils/data-attrs.jsx";
|
|
40
43
|
import { PressResponder } from "../interactions/pressable.jsx";
|
|
41
44
|
import { OverlayTriggerStateContext, PopoverContext } from "../overlay/contexts.js";
|
|
42
|
-
import { CollectionRendererContext, Collection, SectionContext, createBranchComponent, createLeafComponent, isCollectionProbe, isHidden, } from "../collections/components.jsx";
|
|
45
|
+
import { CollectionRendererContext, Collection, SectionContext, createBranchComponent, createLeafComponent, builtCollectionOf, isCollectionProbe, isHidden, } from "../collections/components.jsx";
|
|
46
|
+
import { itemPosition } from "../collections/nodes.js";
|
|
43
47
|
import { CollectionNode, ItemNode, LoaderNode, SectionNode } from "../collections/base.js";
|
|
44
48
|
import { HeaderContext } from "../header/index.jsx";
|
|
45
49
|
import { SeparatorContext } from "../separator/index.jsx";
|
|
@@ -60,6 +64,11 @@ function liveItemDisabled(state, key, own) {
|
|
|
60
64
|
return () => itemDisabled(state, key, !!own());
|
|
61
65
|
}
|
|
62
66
|
/** `aria-disabled` of a live disabled flag. */
|
|
67
|
+
/** `aria-posinset` (0) / `aria-setsize` (1) of a virtualized menu's item (`itemPosition`: per section). */
|
|
68
|
+
function menuItemPosition(state, key, part) {
|
|
69
|
+
const collection = state.virtual?.collection();
|
|
70
|
+
return collection ? itemPosition(collection, key)?.[part] : undefined;
|
|
71
|
+
}
|
|
63
72
|
function ariaDisabledOf(disabled) {
|
|
64
73
|
return () => (disabled() ? "true" : undefined);
|
|
65
74
|
}
|
|
@@ -214,6 +223,15 @@ export function Menu(inProps) {
|
|
|
214
223
|
const triggerState = useContext(RootMenuTriggerStateContext);
|
|
215
224
|
const scope = useContext(SubmenuScopeContext);
|
|
216
225
|
const userOnClose = props.onClose;
|
|
226
|
+
const items = props.items, children = props.children;
|
|
227
|
+
const content = () => <Collection items={items} children={children}/>;
|
|
228
|
+
const renderer = useContext(CollectionRendererContext);
|
|
229
|
+
// RAC: inside a Virtualizer the menu renders from its built collection (only the visible window has items; the
|
|
230
|
+
// delegated handler navigates the built collection, page keys use the layout). A local const arrow (resumable)
|
|
231
|
+
// over a per-instance memo key.
|
|
232
|
+
const builtKey = renderer.isVirtualized ? {} : null;
|
|
233
|
+
const virtualCollection = () => builtCollectionOf(builtKey, content);
|
|
234
|
+
const virtual = builtKey ? { collection: virtualCollection, layoutDelegate: renderer.layoutDelegate } : undefined;
|
|
217
235
|
const state = createMenuState({
|
|
218
236
|
selectionMode: props.selectionMode,
|
|
219
237
|
// Live props: a controlled `selectedKeys={keys()}` stays live (accessor; never written).
|
|
@@ -235,6 +253,7 @@ export function Menu(inProps) {
|
|
|
235
253
|
level: scope ? scope.sub.submenuLevel + 1 : 0,
|
|
236
254
|
...(scope ? { submenuTrigger: scope.sub, parentId: scope.parentId } : {}),
|
|
237
255
|
...(props.id ? { id: props.id } : {}),
|
|
256
|
+
...(virtual ? { virtual } : {}),
|
|
238
257
|
});
|
|
239
258
|
// MenuTrigger's reactive menu id (DialogContext `idRef` pattern): the trigger's aria-controls follows a Menu `id`
|
|
240
259
|
// (RA mergeIds; on a resumed page the merge runs in the browser, where idSignal has no updaters).
|
|
@@ -284,9 +303,12 @@ export function Menu(inProps) {
|
|
|
284
303
|
return () => menuSubmenuClosed(sub);
|
|
285
304
|
});
|
|
286
305
|
});
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
306
|
+
const { CollectionRoot } = renderer;
|
|
307
|
+
// Virtualized: the menu element is the scroll container (RAC scrollRef); its focused item stays rendered and its
|
|
308
|
+
// scroll events reach the renderer.
|
|
309
|
+
const rootCollection = builtKey ? virtualCollection : null;
|
|
310
|
+
const focusedKey = builtKey ? state.focusedKey : undefined;
|
|
311
|
+
const virtualScroll = builtKey ? renderer.onScroll : undefined;
|
|
290
312
|
// Items first (they count themselves for `data-empty`), then the element.
|
|
291
313
|
const body = (<Provider values={[
|
|
292
314
|
[MenuStateContext, state],
|
|
@@ -301,7 +323,7 @@ export function Menu(inProps) {
|
|
|
301
323
|
// Ensure the root MenuTriggerState is defined, in case Menu is rendered outside a MenuTrigger.
|
|
302
324
|
[RootMenuTriggerStateContext, state.root],
|
|
303
325
|
]}>
|
|
304
|
-
{() => CollectionRoot({ collection:
|
|
326
|
+
{() => CollectionRoot({ collection: rootCollection, content, scrollRef: element, focusedKey })}
|
|
305
327
|
</Provider>);
|
|
306
328
|
const renderEmptyState = props.renderEmptyState;
|
|
307
329
|
const dom = globalDOMProps(props, MENU_OWNED);
|
|
@@ -366,6 +388,7 @@ export function Menu(inProps) {
|
|
|
366
388
|
onDragStart?.(e);
|
|
367
389
|
},
|
|
368
390
|
onScroll: (e) => {
|
|
391
|
+
virtualScroll?.(e);
|
|
369
392
|
menuEvent(e, aria, state);
|
|
370
393
|
onScroll?.(e);
|
|
371
394
|
},
|
|
@@ -416,6 +439,7 @@ export function Menu(inProps) {
|
|
|
416
439
|
menuEvent(e, aria, state);
|
|
417
440
|
onDragStart?.(e);
|
|
418
441
|
}} onScroll={(e) => {
|
|
442
|
+
virtualScroll?.(e);
|
|
419
443
|
menuEvent(e, aria, state);
|
|
420
444
|
onScroll?.(e);
|
|
421
445
|
}} on:react-aria-focus={(e) => menuVirtualFocus(e, state)} on:react-aria-clear-focus={(e) => menuClearVirtualFocus(e, state)}>
|
|
@@ -601,6 +625,13 @@ export const MenuItem = /*#__PURE__*/ createLeafComponent(ItemNode, function Men
|
|
|
601
625
|
"aria-haspopup": hasSubmenu ? "menu" : undefined,
|
|
602
626
|
"data-has-submenu": dataBool(hasSubmenu),
|
|
603
627
|
};
|
|
628
|
+
if (state.virtual)
|
|
629
|
+
// useMenuItem (virtualized): the item's position comes from the built collection (only a window is rendered).
|
|
630
|
+
// (Object-literal arrows: resumable closures.)
|
|
631
|
+
Object.assign(attrs, {
|
|
632
|
+
"aria-posinset": () => menuItemPosition(state, key, 0),
|
|
633
|
+
"aria-setsize": () => menuItemPosition(state, key, 1),
|
|
634
|
+
});
|
|
604
635
|
const renderItem = () => {
|
|
605
636
|
count();
|
|
606
637
|
const content = body();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rivetjs/cdk",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
4
|
"description": "Headless, accessible components and utilities for Rivet: overlays, collections, focus, interactions, dates, drag and drop.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -614,9 +614,9 @@
|
|
|
614
614
|
}
|
|
615
615
|
},
|
|
616
616
|
"dependencies": {
|
|
617
|
-
"@rivetjs/kit": "0.1.0-alpha.
|
|
618
|
-
"@rivetjs/reactivity": "0.1.0-alpha.
|
|
619
|
-
"@rivetjs/runtime": "0.1.0-alpha.
|
|
617
|
+
"@rivetjs/kit": "0.1.0-alpha.1",
|
|
618
|
+
"@rivetjs/reactivity": "0.1.0-alpha.1",
|
|
619
|
+
"@rivetjs/runtime": "0.1.0-alpha.1"
|
|
620
620
|
},
|
|
621
621
|
"engines": {
|
|
622
622
|
"node": ">=24"
|
package/portal/index.d.ts
CHANGED
|
@@ -8,6 +8,14 @@ export declare function PortalProvider(props: {
|
|
|
8
8
|
container?: PortalMount;
|
|
9
9
|
children?: Content | (() => Content);
|
|
10
10
|
}): import("@rivetjs/runtime/jsx-runtime").CompiledElement;
|
|
11
|
+
/**
|
|
12
|
+
* Rivet: optional hooks of the overlay layer, unset by default (cdk alone pays one property read per move).
|
|
13
|
+
* `place(host, from)` runs right after a `<Portal>` host moved from its in-place parent `from` (the trigger's subtree,
|
|
14
|
+
* still connected) to its mount; a styled layer copies scope attributes there (`@rivetjs/ui` stamps its theme scope).
|
|
15
|
+
*/
|
|
16
|
+
export declare const portalHooks: {
|
|
17
|
+
place?(host: HTMLElement, from: Element | null): void;
|
|
18
|
+
};
|
|
11
19
|
export interface PortalProps {
|
|
12
20
|
/** Where to render. @default the nearest PortalProvider's container, else document.body */
|
|
13
21
|
mount?: PortalMount;
|
package/portal/index.jsx
CHANGED
|
@@ -30,6 +30,12 @@ export function PortalProvider(props) {
|
|
|
30
30
|
{renderSlot(children)}
|
|
31
31
|
</Provider>);
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Rivet: optional hooks of the overlay layer, unset by default (cdk alone pays one property read per move).
|
|
35
|
+
* `place(host, from)` runs right after a `<Portal>` host moved from its in-place parent `from` (the trigger's subtree,
|
|
36
|
+
* still connected) to its mount; a styled layer copies scope attributes there (`@rivetjs/ui` stamps its theme scope).
|
|
37
|
+
*/
|
|
38
|
+
export const portalHooks = {};
|
|
33
39
|
/**
|
|
34
40
|
* Render `children` at the end of `mount` (default `document.body`), inside a
|
|
35
41
|
* `<div data-rivet-portal>`. Server: rendered in place and moved at `load`.
|
|
@@ -45,8 +51,9 @@ export function Portal(props) {
|
|
|
45
51
|
if (element.parentNode !== target) {
|
|
46
52
|
// Moving a node blurs focus inside it (content that focused itself on mount, e.g. a menu item).
|
|
47
53
|
const focused = element.ownerDocument.activeElement;
|
|
48
|
-
const inside = !!focused && element.contains(focused);
|
|
54
|
+
const inside = !!focused && element.contains(focused), from = element.parentElement;
|
|
49
55
|
target.append(element);
|
|
56
|
+
portalHooks.place?.(element, from);
|
|
50
57
|
if (inside && element.ownerDocument.activeElement !== focused)
|
|
51
58
|
focused.focus({ preventScroll: true });
|
|
52
59
|
}
|
package/radio-group/index.jsx
CHANGED
|
@@ -98,8 +98,11 @@ function RadioGroupItems(props) {
|
|
|
98
98
|
return (<label {...itemDom} onMouseDown={(event) => {
|
|
99
99
|
// RAC Radio mergeProps(DOMProps, labelProps): the user's handler first, then RA useRadio's
|
|
100
100
|
// "Prevent label from being focused when mouse down on it" (the click still focuses the input).
|
|
101
|
+
// Not for a press on the preset's visible native radio itself: canceling its mousedown cancels its
|
|
102
|
+
// focus (RA's input usePress focuses it on pointerdown), so arrow keys after a click had no target.
|
|
101
103
|
option.onMouseDown?.(event);
|
|
102
|
-
event.
|
|
104
|
+
if (!(event.target instanceof HTMLInputElement))
|
|
105
|
+
event.preventDefault();
|
|
103
106
|
}} class={itemClass} data-selected={state.selectedValue() === valueProp ? "true" : undefined} data-disabled={disabled || undefined} data-readonly={isReadOnly || undefined} data-invalid={invalid() ? "true" : undefined} data-required={isRequired || undefined}>
|
|
104
107
|
<input type="radio" id={inputId} class={props.inputClass} name={state.name} form={props.form} value={option.value} disabled={disabled || undefined} required={(isRequired && behaviour === "native") || undefined} aria-describedby={[descriptionId, invalid() && showsError() ? ids.errorMessageId : undefined, groupDescription].filter(Boolean).join(" ") || undefined} data-rv-error={validityError(state.validation)} checked={state.selectedValue() === option.value} onChange={(event) => radioChange(state, option.value, event)} onFocus={(event) => {
|
|
105
108
|
radioFocus(state, option.value, event);
|
package/selection/keyed.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// The selectors live in module WeakMaps keyed by the state's signals (shared by spread copies of a state, e.g.
|
|
8
8
|
// `state/list.ts` ListState, and never serialized). `selector` is owner-free: its watcher lives while
|
|
9
9
|
// bindings read keys.
|
|
10
|
-
import { selector } from "@rivetjs/reactivity";
|
|
10
|
+
import { memberKeys, selector } from "@rivetjs/reactivity";
|
|
11
11
|
/** `selectionOf` result: not selected, in the selected Set, or covered by an "all" selection. */
|
|
12
12
|
export const NOT_SELECTED = 0, IN_SELECTION = 1, ALL_SELECTED = 2;
|
|
13
13
|
const EMPTY = new Set();
|
|
@@ -18,20 +18,10 @@ const hasFocus = new WeakMap();
|
|
|
18
18
|
const selectionIn = (key, selection) => (selection === "all" ? ALL_SELECTED : selection.has(key) ? IN_SELECTION : NOT_SELECTED);
|
|
19
19
|
const isEmpty = (_key, selection) => selection !== "all" && selection.size === 0;
|
|
20
20
|
const onlyKey = () => [0];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const keys = [];
|
|
26
|
-
for (const key of next)
|
|
27
|
-
if (!previous.has(key))
|
|
28
|
-
keys.push(key);
|
|
29
|
-
for (const key of previous)
|
|
30
|
-
if (!next.has(key))
|
|
31
|
-
keys.push(key);
|
|
32
|
-
return keys;
|
|
33
|
-
}
|
|
34
|
-
const changedFocus = (next, previous) => [next, previous];
|
|
21
|
+
// Change hints are reactivity's own (`memberKeys`: keys in one of two Sets, null when "all" is involved; the default
|
|
22
|
+
// pair hint for focus): resume knows them, so a lazy collection row reading `selectionOf` / `isFocusedKey` of a
|
|
23
|
+
// signal-backed state wakes only when its key flips (LAZY-T6 keyed wake, `_trackReads` keyed hook).
|
|
24
|
+
const changedKeys = memberKeys;
|
|
35
25
|
/** The selection read of `state` (react-stately `useMultipleSelectionState`, or the single-key backing). */
|
|
36
26
|
function readerOf(state) {
|
|
37
27
|
const single = state.selectedKey;
|
|
@@ -63,7 +53,7 @@ export function isFocusedKey(state, key) {
|
|
|
63
53
|
const id = state.focusedKey;
|
|
64
54
|
let select = focused.get(id);
|
|
65
55
|
if (select === undefined)
|
|
66
|
-
focused.set(id, (select = selector(id
|
|
56
|
+
focused.set(id, (select = selector(id)));
|
|
67
57
|
return select(key);
|
|
68
58
|
}
|
|
69
59
|
/** `state.focusedKey() != null`; a binding re-runs only when that flips (not on every focus move). */
|