ropav 0.4.0 → 0.6.0
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/dist/components/autocomplete/autocomplete-root.js +3 -1
- package/dist/components/autocomplete/index.d.ts +3 -0
- package/dist/components/breadcrumbs/breadcrumbs-item.js +1 -0
- package/dist/components/breadcrumbs/index.d.ts +1 -0
- package/dist/components/calendar/index.d.ts +5 -3
- package/dist/components/calendar-year-picker/index.d.ts +4 -2
- package/dist/components/checkbox/checkbox-content.js +1 -1
- package/dist/components/color-field/index.d.ts +1 -0
- package/dist/components/color-input-group/color-input-group-root.js +2 -0
- package/dist/components/color-input-group/color-input-group.types.d.ts +2 -0
- package/dist/components/color-input-group/index.d.ts +2 -0
- package/dist/components/combo-box/combo-box-root.js +9 -2
- package/dist/components/combo-box/combo-box.types.d.ts +3 -0
- package/dist/components/combo-box/index.d.ts +3 -0
- package/dist/components/date-field/index.d.ts +1 -0
- package/dist/components/date-input-group/date-input-group-root.js +2 -0
- package/dist/components/date-input-group/date-input-group-segment.js +1 -1
- package/dist/components/date-input-group/date-input-group.types.d.ts +2 -0
- package/dist/{composables → components/date-input-group}/use-date-segment.d.ts +2 -2
- package/dist/{composables → components/date-input-group}/use-date-segment.js +9 -9
- package/dist/components/date-range-picker/date-range-picker-root.js +1 -1
- package/dist/components/date-range-picker/index.d.ts +15 -14
- package/dist/{composables → components/date-range-picker}/use-date-range-picker.d.ts +7 -7
- package/dist/{composables → components/date-range-picker}/use-date-range-picker.js +10 -10
- package/dist/components/drawer/drawer-dialog.js +1 -1
- package/dist/{composables → components/drawer}/use-drawer-drag.d.ts +1 -1
- package/dist/{composables → components/drawer}/use-drawer-drag.js +1 -1
- package/dist/components/drop-zone/drop-zone-root.d.ts +23 -0
- package/dist/components/drop-zone/drop-zone-root.js +153 -0
- package/dist/components/drop-zone/drop-zone-trigger.d.ts +21 -0
- package/dist/components/drop-zone/drop-zone-trigger.js +20 -0
- package/dist/components/drop-zone/drop-zone.accept.d.ts +19 -0
- package/dist/components/drop-zone/drop-zone.accept.js +28 -0
- package/dist/components/drop-zone/drop-zone.context.d.ts +6 -0
- package/dist/components/drop-zone/drop-zone.context.js +5 -0
- package/dist/components/drop-zone/drop-zone.types.d.ts +30 -0
- package/dist/components/drop-zone/index.d.ts +86 -0
- package/dist/components/drop-zone/index.js +11 -0
- package/dist/components/dropdown/dropdown-menu.js +1 -0
- package/dist/components/dropdown/dropdown-submenu-trigger.js +1 -1
- package/dist/components/dropdown/index.d.ts +4 -2
- package/dist/{composables → components/dropdown}/use-safely-mouse-to-submenu.js +1 -1
- package/dist/components/icons/icon-overlay-arrow.js +1 -1
- package/dist/components/index.d.ts +5 -0
- package/dist/components/input/index.d.ts +2 -0
- package/dist/components/input/input-root.js +3 -0
- package/dist/components/input/input.types.d.ts +5 -0
- package/dist/components/input-group/index.d.ts +2 -0
- package/dist/components/input-group/input-group-root.js +3 -0
- package/dist/components/input-group/input-group.types.d.ts +5 -0
- package/dist/components/input-otp/index.d.ts +6 -4
- package/dist/components/link/index.d.ts +2 -0
- package/dist/components/link/link-root.js +22 -5
- package/dist/components/link/link.types.d.ts +14 -3
- package/dist/{composables → components/list-box}/drag-collection.d.ts +2 -2
- package/dist/{composables → components/list-box}/drag-collection.js +1 -1
- package/dist/components/list-box/index.d.ts +4 -0
- package/dist/components/list-box/list-box-root.js +53 -32
- package/dist/components/list-box/list-box.types.d.ts +2 -0
- package/dist/components/menu/index.d.ts +2 -0
- package/dist/components/menu/menu-root.js +5 -1
- package/dist/components/menu/menu.types.d.ts +3 -0
- package/dist/components/number-field/index.d.ts +3 -0
- package/dist/components/number-field/number-field-root.js +2 -0
- package/dist/components/number-field/number-field.types.d.ts +2 -0
- package/dist/components/overlay/overlay-arrow.js +64 -11
- package/dist/components/progress-bar/index.d.ts +4 -0
- package/dist/components/progress-bar/progress-bar-root.js +4 -0
- package/dist/components/progress-bar/progress-bar.types.d.ts +7 -0
- package/dist/components/radio/radio-content.js +1 -1
- package/dist/components/range-calendar/index.d.ts +18 -15
- package/dist/components/router-provider/index.d.ts +5 -0
- package/dist/components/router-provider/index.js +3 -0
- package/dist/components/router-provider/router-provider.context.d.ts +40 -0
- package/dist/components/router-provider/router-provider.context.js +16 -0
- package/dist/components/router-provider/router-provider.d.ts +21 -0
- package/dist/components/router-provider/router-provider.js +22 -0
- package/dist/components/router-provider/router-provider.types.d.ts +23 -0
- package/dist/components/scroll-shadow/use-scroll-shadow.js +25 -1
- package/dist/components/search-field/index.d.ts +2 -0
- package/dist/components/search-field/search-field-root.js +3 -1
- package/dist/components/search-field/search-field.types.d.ts +2 -0
- package/dist/{composables → components/search-field}/use-search-field.d.ts +2 -2
- package/dist/{composables → components/search-field}/use-search-field.js +3 -3
- package/dist/components/segmented-control/index.d.ts +134 -0
- package/dist/components/segmented-control/index.js +15 -0
- package/dist/components/segmented-control/segmented-control-indicator.d.ts +19 -0
- package/dist/components/segmented-control/segmented-control-indicator.js +36 -0
- package/dist/components/segmented-control/segmented-control-item.d.ts +21 -0
- package/dist/components/segmented-control/segmented-control-item.js +96 -0
- package/dist/components/segmented-control/segmented-control-root.d.ts +25 -0
- package/dist/components/segmented-control/segmented-control-root.js +112 -0
- package/dist/components/segmented-control/segmented-control-separator.d.ts +19 -0
- package/dist/components/segmented-control/segmented-control-separator.js +18 -0
- package/dist/components/segmented-control/segmented-control.context.d.ts +17 -0
- package/dist/components/segmented-control/segmented-control.context.js +12 -0
- package/dist/components/segmented-control/segmented-control.types.d.ts +67 -0
- package/dist/components/select/index.d.ts +3 -0
- package/dist/components/select/select-hidden-select.js +1 -1
- package/dist/components/select/select-root.js +2 -0
- package/dist/components/select/select.types.d.ts +2 -0
- package/dist/components/sidebar/index.d.ts +546 -0
- package/dist/components/sidebar/index.js +47 -0
- package/dist/components/sidebar/sidebar-collapsible-trigger.d.ts +21 -0
- package/dist/components/sidebar/sidebar-collapsible-trigger.js +70 -0
- package/dist/components/sidebar/sidebar-collapsible.d.ts +24 -0
- package/dist/components/sidebar/sidebar-collapsible.js +78 -0
- package/dist/components/sidebar/sidebar-content.d.ts +21 -0
- package/dist/components/sidebar/sidebar-content.js +37 -0
- package/dist/components/sidebar/sidebar-footer.d.ts +21 -0
- package/dist/components/sidebar/sidebar-footer.js +24 -0
- package/dist/components/sidebar/sidebar-group-label.d.ts +21 -0
- package/dist/components/sidebar/sidebar-group-label.js +32 -0
- package/dist/components/sidebar/sidebar-group.d.ts +21 -0
- package/dist/components/sidebar/sidebar-group.js +48 -0
- package/dist/components/sidebar/sidebar-header.d.ts +21 -0
- package/dist/components/sidebar/sidebar-header.js +24 -0
- package/dist/components/sidebar/sidebar-inset.d.ts +21 -0
- package/dist/components/sidebar/sidebar-inset.js +24 -0
- package/dist/components/sidebar/sidebar-item-icon.d.ts +21 -0
- package/dist/components/sidebar/sidebar-item-icon.js +20 -0
- package/dist/components/sidebar/sidebar-item-indicator.d.ts +21 -0
- package/dist/components/sidebar/sidebar-item-indicator.js +48 -0
- package/dist/components/sidebar/sidebar-item-label.d.ts +21 -0
- package/dist/components/sidebar/sidebar-item-label.js +28 -0
- package/dist/components/sidebar/sidebar-item-tooltip.d.ts +21 -0
- package/dist/components/sidebar/sidebar-item-tooltip.js +38 -0
- package/dist/components/sidebar/sidebar-item-trailing.d.ts +21 -0
- package/dist/components/sidebar/sidebar-item-trailing.js +28 -0
- package/dist/components/sidebar/sidebar-item.d.ts +23 -0
- package/dist/components/sidebar/sidebar-item.js +160 -0
- package/dist/components/sidebar/sidebar-panel.d.ts +21 -0
- package/dist/components/sidebar/sidebar-panel.js +88 -0
- package/dist/components/sidebar/sidebar-rail.d.ts +21 -0
- package/dist/components/sidebar/sidebar-rail.js +210 -0
- package/dist/components/sidebar/sidebar-root.d.ts +31 -0
- package/dist/components/sidebar/sidebar-root.js +140 -0
- package/dist/components/sidebar/sidebar-separator.d.ts +19 -0
- package/dist/components/sidebar/sidebar-separator.js +16 -0
- package/dist/components/sidebar/sidebar-sub-menu.d.ts +23 -0
- package/dist/components/sidebar/sidebar-sub-menu.js +40 -0
- package/dist/components/sidebar/sidebar-trigger.d.ts +21 -0
- package/dist/components/sidebar/sidebar-trigger.js +69 -0
- package/dist/components/sidebar/sidebar.context.d.ts +75 -0
- package/dist/components/sidebar/sidebar.context.js +30 -0
- package/dist/components/sidebar/sidebar.state.d.ts +73 -0
- package/dist/components/sidebar/sidebar.state.js +76 -0
- package/dist/components/sidebar/sidebar.storage.d.ts +8 -0
- package/dist/components/sidebar/sidebar.storage.js +45 -0
- package/dist/components/sidebar/sidebar.types.d.ts +273 -0
- package/dist/components/splitter/index.d.ts +143 -0
- package/dist/components/splitter/index.js +13 -0
- package/dist/components/splitter/splitter-handle.d.ts +21 -0
- package/dist/components/splitter/splitter-handle.js +209 -0
- package/dist/components/splitter/splitter-panel.d.ts +21 -0
- package/dist/components/splitter/splitter-panel.js +70 -0
- package/dist/components/splitter/splitter-root.d.ts +31 -0
- package/dist/components/splitter/splitter-root.js +152 -0
- package/dist/components/splitter/splitter.context.d.ts +15 -0
- package/dist/components/splitter/splitter.context.js +5 -0
- package/dist/components/splitter/splitter.state.d.ts +87 -0
- package/dist/components/splitter/splitter.state.js +322 -0
- package/dist/components/splitter/splitter.storage.d.ts +15 -0
- package/dist/components/splitter/splitter.storage.js +56 -0
- package/dist/components/splitter/splitter.types.d.ts +98 -0
- package/dist/components/switch/switch-content.js +1 -1
- package/dist/components/table/index.d.ts +10 -9
- package/dist/components/table/table-body.js +13 -13
- package/dist/components/table/table-cell.js +49 -46
- package/dist/components/table/table-column-resizer.js +1 -1
- package/dist/components/table/table-column.js +1 -1
- package/dist/components/table/table-content.js +37 -18
- package/dist/{composables → components/table}/table-drag-collection.d.ts +1 -1
- package/dist/{composables → components/table}/table-drag-collection.js +1 -1
- package/dist/components/table/table-drop-indicator.js +3 -1
- package/dist/components/table/table-resizable-container.d.ts +4 -5
- package/dist/components/table/table-row.js +45 -14
- package/dist/components/table/table-virtualizer-item.d.ts +2 -7
- package/dist/components/table/table-virtualizer-item.js +20 -9
- package/dist/components/table/table.context.d.ts +11 -3
- package/dist/components/table/table.types.d.ts +1 -1
- package/dist/{composables → components/table}/use-grid-keyboard.d.ts +3 -3
- package/dist/{composables → components/table}/use-grid-keyboard.js +57 -55
- package/dist/{composables → components/table}/use-grid-selection-announcement.d.ts +2 -2
- package/dist/{composables → components/table}/use-grid-selection-announcement.js +2 -2
- package/dist/{composables → components/table}/use-table-collection.d.ts +2 -2
- package/dist/{composables → components/table}/use-table-collection.js +4 -4
- package/dist/{composables → components/table}/use-table-column-layout.d.ts +13 -20
- package/dist/components/table/use-table-column-layout.js +115 -0
- package/dist/components/textarea/index.d.ts +2 -0
- package/dist/components/textarea/textarea-root.js +3 -0
- package/dist/components/textarea/textarea.types.d.ts +5 -0
- package/dist/components/textfield/index.d.ts +2 -0
- package/dist/components/textfield/textfield-root.js +5 -1
- package/dist/components/textfield/textfield.context.d.ts +2 -0
- package/dist/components/textfield/textfield.types.d.ts +5 -0
- package/dist/components/time-field/index.d.ts +1 -0
- package/dist/components/toast/index.d.ts +1 -0
- package/dist/components/toast/toast-default-content.js +1 -1
- package/dist/components/toolbar/index.d.ts +3 -2
- package/dist/components/tooltip/index.js +1 -1
- package/dist/components/tooltip/tooltip-root.js +1 -1
- package/dist/components/virtualizer/index.d.ts +3 -1
- package/dist/components/virtualizer/index.js +2 -1
- package/dist/components/virtualizer/use-virtualizer-item.d.ts +23 -0
- package/dist/components/virtualizer/use-virtualizer-item.js +45 -0
- package/dist/components/virtualizer/virtualizer-item.js +7 -41
- package/dist/components/virtualizer/virtualizer-scrollbar-track.d.ts +20 -0
- package/dist/components/virtualizer/virtualizer-scrollbar-track.js +141 -0
- package/dist/components/virtualizer/virtualizer-scrollbar.d.ts +18 -0
- package/dist/components/virtualizer/virtualizer-scrollbar.js +55 -0
- package/dist/components/virtualizer/virtualizer.context.d.ts +17 -1
- package/dist/composables/drag-manager.js +1 -13
- package/dist/composables/index.d.ts +3 -70
- package/dist/composables/use-collection.d.ts +22 -3
- package/dist/composables/use-collection.js +67 -14
- package/dist/composables/use-color-area.js +2 -2
- package/dist/composables/use-color-field.js +1 -1
- package/dist/composables/use-combo-box.js +1 -1
- package/dist/composables/use-date-field.js +1 -1
- package/dist/composables/use-drag.d.ts +6 -5
- package/dist/composables/use-draggable-item.d.ts +2 -2
- package/dist/composables/use-droppable-collection.js +3 -3
- package/dist/composables/use-droppable-item.js +1 -1
- package/dist/composables/use-form-reset.d.ts +6 -2
- package/dist/composables/use-form-reset.js +9 -5
- package/dist/composables/use-list-keyboard.js +64 -62
- package/dist/composables/use-number-field.js +1 -1
- package/dist/composables/use-select.js +2 -2
- package/dist/composables/use-selection-manager.js +4 -4
- package/dist/composables/use-single-select-list-state.d.ts +37 -0
- package/dist/composables/use-single-select-list-state.js +110 -0
- package/dist/composables/use-slider-thumb.js +2 -2
- package/dist/composables/use-slider.js +1 -1
- package/dist/composables/use-tab-list-state.d.ts +10 -29
- package/dist/composables/use-tab-list-state.js +10 -99
- package/dist/composables/use-text-field.js +1 -1
- package/dist/composables/use-virtualizer-scroll.d.ts +52 -2
- package/dist/composables/use-virtualizer-scroll.js +79 -13
- package/dist/composables/use-virtualizer.d.ts +24 -4
- package/dist/composables/use-virtualizer.js +40 -6
- package/dist/index.js +60 -83
- package/dist/utils/document-order.d.ts +14 -0
- package/dist/utils/document-order.js +17 -0
- package/dist/utils/flex-sizing.d.ts +41 -0
- package/dist/utils/flex-sizing.js +140 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/open-link.d.ts +31 -0
- package/dist/utils/open-link.js +32 -0
- package/dist/utils/story-truncation.d.ts +20 -0
- package/dist/utils/story-users.d.ts +11 -0
- package/dist/utils/virtualizer-collection.d.ts +14 -4
- package/dist/utils/virtualizer-collection.js +9 -14
- package/dist/utils/virtualizer-layout-info.js +3 -2
- package/dist/utils/virtualizer-list-layout.d.ts +75 -29
- package/dist/utils/virtualizer-list-layout.js +182 -99
- package/dist/utils/virtualizer-offsets.d.ts +111 -0
- package/dist/utils/virtualizer-offsets.js +159 -0
- package/dist/utils/virtualizer-overscan.d.ts +9 -15
- package/dist/utils/virtualizer-overscan.js +14 -33
- package/dist/utils/virtualizer-table-layout.d.ts +50 -21
- package/dist/utils/virtualizer-table-layout.js +130 -89
- package/dist/version.js +1 -1
- package/package.json +25 -5
- package/dist/composables/use-table-column-layout.js +0 -214
- /package/dist/{composables → components/dropdown}/use-safely-mouse-to-submenu.d.ts +0 -0
|
@@ -1,6 +1,26 @@
|
|
|
1
|
+
import { sortByDocumentOrder } from "../utils/document-order.js";
|
|
1
2
|
import { computed, shallowRef } from "vue";
|
|
2
3
|
//#region src/composables/use-collection.ts
|
|
3
4
|
/**
|
|
5
|
+
* `key → index` for a data-backed collection, memoized on the source itself.
|
|
6
|
+
*
|
|
7
|
+
* A source is rebuilt whenever its data changes — it is handed over out of a `computed` — so its
|
|
8
|
+
* identity is already the right cache key and nothing has to invalidate this by hand. Held weakly,
|
|
9
|
+
* so a collection that has gone away takes its map with it.
|
|
10
|
+
*
|
|
11
|
+
* This is what keeps `getIndex` off a scan on the one path that has no operation to scope to:
|
|
12
|
+
* `aria-posinset` and `aria-rowindex` are read during render, once per item, and a virtualized
|
|
13
|
+
* collection's source holds every item rather than the rendered window.
|
|
14
|
+
*/
|
|
15
|
+
var sourceIndices = /* @__PURE__ */ new WeakMap();
|
|
16
|
+
var indicesOf = (source) => {
|
|
17
|
+
const cached = sourceIndices.get(source);
|
|
18
|
+
if (cached) return cached;
|
|
19
|
+
const indices = new Map(source.keys.map((key, index) => [key, index]));
|
|
20
|
+
sourceIndices.set(source, indices);
|
|
21
|
+
return indices;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
4
24
|
* The set of items a listbox, menu or tag group navigates over.
|
|
5
25
|
*
|
|
6
26
|
* React Aria builds this by rendering the children into a hidden tree and reading the result
|
|
@@ -8,7 +28,7 @@ import { computed, shallowRef } from "vue";
|
|
|
8
28
|
* rendering a slot is what creates its DOM, so there is no "render to inspect" pass. The flow
|
|
9
29
|
* is inverted instead — each item registers itself, and the parent asks the DOM for the order.
|
|
10
30
|
*
|
|
11
|
-
*
|
|
31
|
+
* Three choices carry the weight:
|
|
12
32
|
*
|
|
13
33
|
* Metadata is stored as **getters**, not values, so the parent always reads an item's current
|
|
14
34
|
* text and disabled state. Nothing has to re-register when a prop changes, and no
|
|
@@ -18,8 +38,20 @@ import { computed, shallowRef } from "vue";
|
|
|
18
38
|
* invalidated by a counter bumped on register and unregister — but reordering items that are
|
|
19
39
|
* already registered changes their document order without touching that counter, which would
|
|
20
40
|
* silently break arrow navigation. Order is only ever read on an interaction (a keypress, a
|
|
21
|
-
* focus change, extending a selection), never during render, so
|
|
22
|
-
*
|
|
41
|
+
* focus change, extending a selection), never during render, so `size` stays reactive — because
|
|
42
|
+
* it *is* read during render — while order deliberately stays outside reactivity.
|
|
43
|
+
*
|
|
44
|
+
* Sorting the DOM per call, however, is only cheap for a *single* read, and the callers that
|
|
45
|
+
* matter walk the collection one neighbour at a time: typeahead, paging, and skipping disabled
|
|
46
|
+
* items all ask for a neighbour in a loop. Sorting inside that loop is quadratic, and a search
|
|
47
|
+
* that matches nothing walks the whole collection twice. {@link UseCollectionReturn.withOrder}
|
|
48
|
+
* is what those callers wrap themselves in: the order is read once and every step reads that one
|
|
49
|
+
* snapshot.
|
|
50
|
+
*
|
|
51
|
+
* The snapshot is **scoped to one synchronous operation** rather than cached across operations,
|
|
52
|
+
* because caching is the very thing the second choice above rules out. Vue flushes renders on
|
|
53
|
+
* `nextTick`, so the DOM cannot reorder itself in the middle of a keydown handler — which is
|
|
54
|
+
* exactly as long as a snapshot is allowed to live. It must never become a `computed`.
|
|
23
55
|
*
|
|
24
56
|
* A collection that is virtualized cannot work that way at all: all but a window of its items are
|
|
25
57
|
* absent from the DOM, so asking the DOM would answer for the window and call the rest
|
|
@@ -56,18 +88,38 @@ var useCollection = (options = {}) => {
|
|
|
56
88
|
textValue: () => node.textValue ?? ""
|
|
57
89
|
};
|
|
58
90
|
};
|
|
59
|
-
const
|
|
91
|
+
const domOrderedKeys = () => sortByDocumentOrder([...items.entries()], ([, meta]) => meta.element()).map(([key]) => key);
|
|
92
|
+
const buildOrder = () => {
|
|
60
93
|
const sourced = source();
|
|
61
|
-
|
|
62
|
-
|
|
94
|
+
const keys = sourced ? sourced.keys : domOrderedKeys();
|
|
95
|
+
let indices = sourced ? indicesOf(sourced) : null;
|
|
96
|
+
return {
|
|
97
|
+
indexOf: (key) => {
|
|
98
|
+
indices ??= new Map(keys.map((entry, index) => [entry, index]));
|
|
99
|
+
return indices.get(key) ?? -1;
|
|
100
|
+
},
|
|
101
|
+
keys
|
|
102
|
+
};
|
|
63
103
|
};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
104
|
+
/** The order this operation is reading, or a fresh reading for a call that stands alone. */
|
|
105
|
+
let scoped = null;
|
|
106
|
+
let depth = 0;
|
|
107
|
+
const order = () => scoped ?? buildOrder();
|
|
108
|
+
const withOrder = (operation) => {
|
|
109
|
+
if (depth === 0) scoped = buildOrder();
|
|
110
|
+
depth += 1;
|
|
111
|
+
try {
|
|
112
|
+
return operation();
|
|
113
|
+
} finally {
|
|
114
|
+
depth -= 1;
|
|
115
|
+
if (depth === 0) scoped = null;
|
|
116
|
+
}
|
|
67
117
|
};
|
|
118
|
+
const orderedKeys = () => order().keys;
|
|
119
|
+
const keyAt = (index) => order().keys[index] ?? null;
|
|
68
120
|
const neighbour = (key, step) => {
|
|
69
|
-
const keys =
|
|
70
|
-
const index =
|
|
121
|
+
const { indexOf, keys } = order();
|
|
122
|
+
const index = indexOf(key);
|
|
71
123
|
if (index === -1) return null;
|
|
72
124
|
return keys[index + step] ?? null;
|
|
73
125
|
};
|
|
@@ -76,18 +128,19 @@ var useCollection = (options = {}) => {
|
|
|
76
128
|
return items.get(key)?.element() ?? null;
|
|
77
129
|
},
|
|
78
130
|
getFirstKey: () => keyAt(0),
|
|
79
|
-
getIndex: (key) =>
|
|
131
|
+
getIndex: (key) => order().indexOf(key),
|
|
80
132
|
getItem: (key) => {
|
|
81
133
|
return items.get(key) ?? metaFromSource(key);
|
|
82
134
|
},
|
|
83
135
|
getKeyAfter: (key) => neighbour(key, 1),
|
|
84
136
|
getKeyBefore: (key) => neighbour(key, -1),
|
|
85
|
-
getLastKey: () =>
|
|
137
|
+
getLastKey: () => order().keys.at(-1) ?? null,
|
|
86
138
|
orderedKeys,
|
|
87
139
|
register,
|
|
88
140
|
size: computed(() => {
|
|
89
141
|
return source()?.itemCount ?? items.size;
|
|
90
|
-
})
|
|
142
|
+
}),
|
|
143
|
+
withOrder
|
|
91
144
|
};
|
|
92
145
|
};
|
|
93
146
|
//#endregion
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useMove } from "./use-move.js";
|
|
1
2
|
import { useLocale } from "./use-locale.js";
|
|
2
3
|
import { useLocalizedStringFormatter } from "./use-localized-string-formatter.js";
|
|
3
4
|
import { colorStrings } from "../i18n/color.js";
|
|
@@ -6,7 +7,6 @@ import { visuallyHiddenStyle } from "../utils/visually-hidden.js";
|
|
|
6
7
|
import { useLabels } from "./use-labels.js";
|
|
7
8
|
import { setFormValue } from "../utils/form-value.js";
|
|
8
9
|
import { useColorAreaGradient } from "./use-color-area-gradient.js";
|
|
9
|
-
import { useMove } from "./use-move.js";
|
|
10
10
|
import { computed, onScopeDispose, shallowRef, toValue, watch } from "vue";
|
|
11
11
|
//#region src/composables/use-color-area.ts
|
|
12
12
|
/**
|
|
@@ -68,7 +68,7 @@ var useColorArea = (options) => {
|
|
|
68
68
|
flush: "post",
|
|
69
69
|
immediate: true
|
|
70
70
|
});
|
|
71
|
-
useFormReset(inputXEl, () => state.defaultValue.value, state.setValue);
|
|
71
|
+
useFormReset(inputXEl, () => state.defaultValue.value, state.setValue, options.form);
|
|
72
72
|
/** Where the thumb is during a drag, in fractions of the area — not in pixels. */
|
|
73
73
|
let currentPosition = null;
|
|
74
74
|
/** The pointer that owns the interaction in flight, if any. */
|
|
@@ -86,7 +86,7 @@ var useColorField = (options = {}) => {
|
|
|
86
86
|
setInputValue: state.setInputValue,
|
|
87
87
|
validate: state.validate
|
|
88
88
|
});
|
|
89
|
-
useFormReset(field.element, () => state.defaultColorValue.value, state.setColorValue);
|
|
89
|
+
useFormReset(field.element, () => state.defaultColorValue.value, state.setColorValue, options.form);
|
|
90
90
|
/**
|
|
91
91
|
* Whether the input has focus, which gates the wheel.
|
|
92
92
|
*
|
|
@@ -119,7 +119,7 @@ var useComboBox = (options, state) => {
|
|
|
119
119
|
validationState: state,
|
|
120
120
|
value: () => state.inputValue.value
|
|
121
121
|
});
|
|
122
|
-
useFormReset(computed(() => inputElement.value), () => state.defaultValue.value, state.setValue);
|
|
122
|
+
useFormReset(computed(() => inputElement.value), () => state.defaultValue.value, state.setValue, options.form);
|
|
123
123
|
/**
|
|
124
124
|
* Whether focus is still somewhere inside the combo box.
|
|
125
125
|
*
|
|
@@ -89,7 +89,7 @@ var useDateField = (options) => {
|
|
|
89
89
|
options.onFocusChange?.(false);
|
|
90
90
|
};
|
|
91
91
|
const inputValue = computed(() => toValue(options.inputValue) ?? state.value.value?.toString() ?? "");
|
|
92
|
-
useFormReset(options.inputElement, state.defaultValue, state.setValue);
|
|
92
|
+
useFormReset(options.inputElement, state.defaultValue, state.setValue, options.form);
|
|
93
93
|
watch([options.inputElement, inputValue], ([input, text]) => setFormValue(input, text), {
|
|
94
94
|
flush: "post",
|
|
95
95
|
immediate: true
|
|
@@ -31,12 +31,13 @@ export interface UseDragHandlers {
|
|
|
31
31
|
onKeyupCapture?: (event: KeyboardEvent) => void;
|
|
32
32
|
onClick?: (event: MouseEvent) => void;
|
|
33
33
|
}
|
|
34
|
+
/** Attributes for a draggable element. Attributes only, never listeners. */
|
|
35
|
+
export interface DragAttrs {
|
|
36
|
+
draggable?: "true" | "false";
|
|
37
|
+
"aria-describedby"?: string;
|
|
38
|
+
}
|
|
34
39
|
export interface UseDragReturn {
|
|
35
|
-
|
|
36
|
-
attrs: ComputedRef<{
|
|
37
|
-
draggable: "true" | "false";
|
|
38
|
-
"aria-describedby"?: string;
|
|
39
|
-
}>;
|
|
40
|
+
attrs: ComputedRef<Required<Pick<DragAttrs, "draggable">> & DragAttrs>;
|
|
40
41
|
handlers: UseDragHandlers;
|
|
41
42
|
/** Attributes for the explicit drag control, when there is one. */
|
|
42
43
|
dragButtonAttrs: ComputedRef<{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DragKey } from "../utils/dnd-types.js";
|
|
2
|
-
import type { UseDragHandlers } from "./use-drag.js";
|
|
2
|
+
import type { DragAttrs, UseDragHandlers } from "./use-drag.js";
|
|
3
3
|
import type { UseDraggableCollectionStateReturn } from "./use-draggable-collection-state.js";
|
|
4
4
|
import type { PressEvent } from "./use-press.js";
|
|
5
5
|
import type { ComputedRef, MaybeRefOrGetter } from "vue";
|
|
@@ -16,7 +16,7 @@ export interface UseDraggableItemOptions {
|
|
|
16
16
|
hasAction?: MaybeRefOrGetter<boolean | undefined>;
|
|
17
17
|
}
|
|
18
18
|
export interface UseDraggableItemReturn {
|
|
19
|
-
attrs: ComputedRef<
|
|
19
|
+
attrs: ComputedRef<DragAttrs>;
|
|
20
20
|
handlers: UseDragHandlers;
|
|
21
21
|
dragButtonAttrs: ComputedRef<{
|
|
22
22
|
"aria-describedby": string | undefined;
|
|
@@ -3,11 +3,11 @@ import { useLocale } from "./use-locale.js";
|
|
|
3
3
|
import { DIRECTORY_DRAG_TYPE } from "../utils/dnd-constants.js";
|
|
4
4
|
import { dragTypesFromSet, getTypes } from "../utils/dnd-data-transfer.js";
|
|
5
5
|
import { registerDropTarget } from "./drag-manager.js";
|
|
6
|
-
import { registerDroppableCollection } from "./droppable-collection-registry.js";
|
|
7
|
-
import { useAutoScroll } from "./use-auto-scroll.js";
|
|
8
6
|
import { clearGlobalDnDState, globalDndState, isInternalDropOperation, setDropCollectionRef } from "../utils/dnd-state.js";
|
|
9
|
-
import { navigateDropTarget } from "../utils/dnd-drop-target-navigation.js";
|
|
10
7
|
import { useDrop } from "./use-drop.js";
|
|
8
|
+
import { registerDroppableCollection } from "./droppable-collection-registry.js";
|
|
9
|
+
import { navigateDropTarget } from "../utils/dnd-drop-target-navigation.js";
|
|
10
|
+
import { useAutoScroll } from "./use-auto-scroll.js";
|
|
11
11
|
import { computed, onScopeDispose, useId, watch } from "vue";
|
|
12
12
|
//#region src/composables/use-droppable-collection.ts
|
|
13
13
|
/** How long to wait for a drop handler's async work before deciding where focus lands. */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getTypes } from "../utils/dnd-data-transfer.js";
|
|
2
2
|
import { registerDropItem, useDragSession } from "./drag-manager.js";
|
|
3
|
-
import { getDroppableCollectionElement } from "./droppable-collection-registry.js";
|
|
4
3
|
import { globalDndState, isInternalDropOperation } from "../utils/dnd-state.js";
|
|
5
4
|
import { useVirtualDrop } from "./use-virtual-drop.js";
|
|
5
|
+
import { getDroppableCollectionElement } from "./droppable-collection-registry.js";
|
|
6
6
|
import { computed, watch } from "vue";
|
|
7
7
|
//#region src/composables/use-droppable-item.ts
|
|
8
8
|
/**
|
|
@@ -13,11 +13,15 @@ type ResettableElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElem
|
|
|
13
13
|
* `initialValue` is read when the reset happens rather than when the listener is attached,
|
|
14
14
|
* so a default that changes over the component's life still resets to the current one.
|
|
15
15
|
*
|
|
16
|
+
* `form` is the `id` the caller renders as the control's `form` attribute, for a control that
|
|
17
|
+
* submits to a form it does not sit inside. Pass it wherever that attribute is rendered: it is
|
|
18
|
+
* what moves the listener when a mounted control is repointed.
|
|
19
|
+
*
|
|
16
20
|
* @example
|
|
17
21
|
* ```ts
|
|
18
22
|
* const input = useTemplateRef<HTMLInputElement>("input");
|
|
19
|
-
* useFormReset(input, () => props.defaultSelected ?? false, setSelected);
|
|
23
|
+
* useFormReset(input, () => props.defaultSelected ?? false, setSelected, () => props.form);
|
|
20
24
|
* ```
|
|
21
25
|
*/
|
|
22
|
-
export declare const useFormReset: <T>(element: Ref<ResettableElement | null | undefined>, initialValue: MaybeRefOrGetter<T>, onReset: (value: T) => void) => void;
|
|
26
|
+
export declare const useFormReset: <T>(element: Ref<ResettableElement | null | undefined>, initialValue: MaybeRefOrGetter<T>, onReset: (value: T) => void, form?: MaybeRefOrGetter<string | undefined>) => void;
|
|
23
27
|
export {};
|
|
@@ -12,13 +12,17 @@ import { onScopeDispose, toValue, watch } from "vue";
|
|
|
12
12
|
* `initialValue` is read when the reset happens rather than when the listener is attached,
|
|
13
13
|
* so a default that changes over the component's life still resets to the current one.
|
|
14
14
|
*
|
|
15
|
+
* `form` is the `id` the caller renders as the control's `form` attribute, for a control that
|
|
16
|
+
* submits to a form it does not sit inside. Pass it wherever that attribute is rendered: it is
|
|
17
|
+
* what moves the listener when a mounted control is repointed.
|
|
18
|
+
*
|
|
15
19
|
* @example
|
|
16
20
|
* ```ts
|
|
17
21
|
* const input = useTemplateRef<HTMLInputElement>("input");
|
|
18
|
-
* useFormReset(input, () => props.defaultSelected ?? false, setSelected);
|
|
22
|
+
* useFormReset(input, () => props.defaultSelected ?? false, setSelected, () => props.form);
|
|
19
23
|
* ```
|
|
20
24
|
*/
|
|
21
|
-
var useFormReset = (element, initialValue, onReset) => {
|
|
25
|
+
var useFormReset = (element, initialValue, onReset, form) => {
|
|
22
26
|
let attachedForm = null;
|
|
23
27
|
const onFormReset = (event) => {
|
|
24
28
|
if (event.defaultPrevented) return;
|
|
@@ -28,10 +32,10 @@ var useFormReset = (element, initialValue, onReset) => {
|
|
|
28
32
|
attachedForm?.removeEventListener("reset", onFormReset);
|
|
29
33
|
attachedForm = null;
|
|
30
34
|
};
|
|
31
|
-
watch(() => element.value?.form ?? null, (form) => {
|
|
35
|
+
watch([() => element.value?.form ?? null, () => toValue(form)], ([owner]) => {
|
|
32
36
|
detach();
|
|
33
|
-
|
|
34
|
-
attachedForm =
|
|
37
|
+
owner?.addEventListener("reset", onFormReset);
|
|
38
|
+
attachedForm = owner;
|
|
35
39
|
}, {
|
|
36
40
|
flush: "post",
|
|
37
41
|
immediate: true
|
|
@@ -169,7 +169,7 @@ var useListKeyboard = (options) => {
|
|
|
169
169
|
}
|
|
170
170
|
return next ?? (step === 1 ? getLastKey() : getFirstKey());
|
|
171
171
|
};
|
|
172
|
-
const getKeyForSearch = (search, fromKey) => {
|
|
172
|
+
const getKeyForSearch = (search, fromKey) => collection.withOrder(() => {
|
|
173
173
|
let key = fromKey ?? getFirstKey();
|
|
174
174
|
while (key != null) {
|
|
175
175
|
const item = collection.getItem(key);
|
|
@@ -179,7 +179,7 @@ var useListKeyboard = (options) => {
|
|
|
179
179
|
key = walk(collection.getKeyAfter(key), 1);
|
|
180
180
|
}
|
|
181
181
|
return null;
|
|
182
|
-
};
|
|
182
|
+
});
|
|
183
183
|
const land = (element, scroll) => {
|
|
184
184
|
if (!isVirtual.value) element.focus();
|
|
185
185
|
if (scroll && typeof element.scrollIntoView === "function") element.scrollIntoView({ block: "nearest" });
|
|
@@ -228,67 +228,69 @@ var useListKeyboard = (options) => {
|
|
|
228
228
|
event.stopPropagation();
|
|
229
229
|
return true;
|
|
230
230
|
};
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
231
|
+
collection.withOrder(() => {
|
|
232
|
+
switch (event.key) {
|
|
233
|
+
case "ArrowDown":
|
|
234
|
+
move(focused == null ? getFirstKey() : getKeyBelow(focused));
|
|
235
|
+
return;
|
|
236
|
+
case "ArrowUp":
|
|
237
|
+
move(focused == null ? getLastKey() : getKeyAbove(focused));
|
|
238
|
+
return;
|
|
239
|
+
case "ArrowRight":
|
|
240
|
+
if (!answersInlineAxis()) return;
|
|
241
|
+
move(focused == null ? getFirstKey() : getKeyRightOf(focused));
|
|
242
|
+
return;
|
|
243
|
+
case "ArrowLeft":
|
|
244
|
+
if (!answersInlineAxis()) return;
|
|
245
|
+
move(focused == null ? getLastKey() : getKeyLeftOf(focused));
|
|
246
|
+
return;
|
|
247
|
+
case "Home":
|
|
248
|
+
if (focused == null && event.shiftKey) return;
|
|
249
|
+
move(getFirstKey(), false);
|
|
250
|
+
return;
|
|
251
|
+
case "End":
|
|
252
|
+
if (focused == null && event.shiftKey) return;
|
|
253
|
+
move(getLastKey(), false);
|
|
254
|
+
return;
|
|
255
|
+
case "PageUp":
|
|
256
|
+
move(focused == null ? getFirstKey() : pageStep(focused, -1));
|
|
257
|
+
return;
|
|
258
|
+
case "PageDown":
|
|
259
|
+
move(focused == null ? getLastKey() : pageStep(focused, 1));
|
|
260
|
+
return;
|
|
261
|
+
case "a":
|
|
262
|
+
if (!(event.ctrlKey || event.metaKey) || toValue(options.disallowSelectAll)) return;
|
|
263
|
+
if (selection.selectionMode.value !== "multiple") return;
|
|
264
|
+
selection.selectAll();
|
|
265
|
+
event.preventDefault();
|
|
266
|
+
event.stopPropagation();
|
|
267
|
+
return;
|
|
268
|
+
case "Escape":
|
|
269
|
+
if (escapeKeyBehavior.value !== "clearSelection" || selection.isEmpty.value) return;
|
|
270
|
+
selection.clearSelection();
|
|
271
|
+
event.preventDefault();
|
|
272
|
+
event.stopPropagation();
|
|
273
|
+
return;
|
|
274
|
+
case "Tab": {
|
|
275
|
+
if (isVirtual.value) return;
|
|
276
|
+
const focusable = focusableIn(element);
|
|
277
|
+
(event.shiftKey ? element : focusable.at(-1))?.focus();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
case "Enter":
|
|
281
|
+
if (focused == null || toValue(options.disallowActivation)) return;
|
|
282
|
+
options.onAction?.(focused);
|
|
283
|
+
event.preventDefault();
|
|
284
|
+
return;
|
|
285
|
+
case " ":
|
|
286
|
+
if (focused == null || toValue(options.disallowActivation)) return;
|
|
287
|
+
if (selection.selectionMode.value === "none") options.onAction?.(focused);
|
|
288
|
+
else selection.select(focused, { isShiftPressed: event.shiftKey });
|
|
289
|
+
event.preventDefault();
|
|
290
|
+
return;
|
|
291
|
+
default: return;
|
|
278
292
|
}
|
|
279
|
-
|
|
280
|
-
if (focused == null || toValue(options.disallowActivation)) return;
|
|
281
|
-
options.onAction?.(focused);
|
|
282
|
-
event.preventDefault();
|
|
283
|
-
return;
|
|
284
|
-
case " ":
|
|
285
|
-
if (focused == null || toValue(options.disallowActivation)) return;
|
|
286
|
-
if (selection.selectionMode.value === "none") options.onAction?.(focused);
|
|
287
|
-
else selection.select(focused, { isShiftPressed: event.shiftKey });
|
|
288
|
-
event.preventDefault();
|
|
289
|
-
return;
|
|
290
|
-
default: return;
|
|
291
|
-
}
|
|
293
|
+
});
|
|
292
294
|
};
|
|
293
295
|
const onFocusin = (event) => {
|
|
294
296
|
if (isVirtual.value) return;
|
|
@@ -128,7 +128,7 @@ var useNumberField = (options = {}) => {
|
|
|
128
128
|
setInputValue: state.setInputValue,
|
|
129
129
|
validate: state.validate
|
|
130
130
|
});
|
|
131
|
-
useFormReset(element, () => state.defaultNumberValue.value, state.setNumberValue);
|
|
131
|
+
useFormReset(element, () => state.defaultNumberValue.value, state.setNumberValue, options.form);
|
|
132
132
|
/**
|
|
133
133
|
* Range validation borrowed from a real number input.
|
|
134
134
|
*
|
|
@@ -46,7 +46,7 @@ var useSelect = (options, state) => {
|
|
|
46
46
|
sensitivity: "base",
|
|
47
47
|
usage: "search"
|
|
48
48
|
});
|
|
49
|
-
const getKeyForSearch = (search, fromKey) => {
|
|
49
|
+
const getKeyForSearch = (search, fromKey) => state.collection.withOrder(() => {
|
|
50
50
|
let key = fromKey ?? state.collection.getFirstKey();
|
|
51
51
|
while (key != null) {
|
|
52
52
|
const item = state.collection.getItem(key);
|
|
@@ -56,7 +56,7 @@ var useSelect = (options, state) => {
|
|
|
56
56
|
key = state.collection.getKeyAfter(key);
|
|
57
57
|
}
|
|
58
58
|
return null;
|
|
59
|
-
};
|
|
59
|
+
});
|
|
60
60
|
const typeahead = useTypeahead({
|
|
61
61
|
focusedKey: () => state.selectedKey.value,
|
|
62
62
|
getKeyForSearch,
|
|
@@ -73,13 +73,13 @@ var useSelectionManager = (options) => {
|
|
|
73
73
|
commit(next);
|
|
74
74
|
};
|
|
75
75
|
/** The keys from `from` to `to` inclusive, in document order whichever way round they are. */
|
|
76
|
-
const keyRange = (from, to) => {
|
|
76
|
+
const keyRange = (from, to) => collection.withOrder(() => {
|
|
77
77
|
const keys = collection.orderedKeys();
|
|
78
|
-
const start =
|
|
79
|
-
const end =
|
|
78
|
+
const start = collection.getIndex(from);
|
|
79
|
+
const end = collection.getIndex(to);
|
|
80
80
|
if (start === -1 || end === -1) return [];
|
|
81
81
|
return keys.slice(Math.min(start, end), Math.max(start, end) + 1);
|
|
82
|
-
};
|
|
82
|
+
});
|
|
83
83
|
const extendSelection = (key) => {
|
|
84
84
|
if (selectionMode.value === "none") return;
|
|
85
85
|
if (selectionMode.value === "single") {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CollectionKey, UseCollectionReturn } from "./use-collection.js";
|
|
2
|
+
import type { UseSelectionManagerReturn } from "./use-selection-manager.js";
|
|
3
|
+
import type { ComputedRef, MaybeRefOrGetter } from "vue";
|
|
4
|
+
export interface UseSingleSelectListStateOptions {
|
|
5
|
+
/** The selected item, when the caller drives it. */
|
|
6
|
+
selectedKey?: MaybeRefOrGetter<CollectionKey | undefined>;
|
|
7
|
+
/** The initially selected item. Falls back to the first item that is not disabled. */
|
|
8
|
+
defaultSelectedKey?: CollectionKey;
|
|
9
|
+
onSelectionChange?: (key: CollectionKey) => void;
|
|
10
|
+
disabledKeys?: MaybeRefOrGetter<Iterable<CollectionKey> | undefined>;
|
|
11
|
+
/** Whether the whole list is disabled: a selection that exists but cannot be changed. */
|
|
12
|
+
isDisabled?: MaybeRefOrGetter<boolean | undefined>;
|
|
13
|
+
}
|
|
14
|
+
export interface UseSingleSelectListStateReturn {
|
|
15
|
+
collection: UseCollectionReturn;
|
|
16
|
+
selection: UseSelectionManagerReturn;
|
|
17
|
+
/** The selected item, with the default resolved against the collection. */
|
|
18
|
+
selectedKey: ComputedRef<CollectionKey | null>;
|
|
19
|
+
setSelectedKey: (key: CollectionKey) => void;
|
|
20
|
+
isDisabled: ComputedRef<boolean>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Which item of a collection is selected, for a list that always has exactly one.
|
|
24
|
+
*
|
|
25
|
+
* Ported from React Stately's `useSingleSelectListState`
|
|
26
|
+
* (`react-stately/src/list/useSingleSelectListState.ts`, react-stately 3.49.0).
|
|
27
|
+
*
|
|
28
|
+
* "Always exactly one" is `disallowEmptySelection` plus a fallback for the case where nothing says
|
|
29
|
+
* which. The selection behaviour is deliberately left at toggle: whether the arrow keys choose is a
|
|
30
|
+
* question about how activation works, asked by the caller through `selectOnFocus`, not a question
|
|
31
|
+
* about what a press means.
|
|
32
|
+
*
|
|
33
|
+
* The three pieces have to be built in one order — the collection, then the fallback that reads it,
|
|
34
|
+
* then the selection manager the fallback feeds — which is the reason this is a composable rather
|
|
35
|
+
* than a few lines in the root component.
|
|
36
|
+
*/
|
|
37
|
+
export declare const useSingleSelectListState: (options?: UseSingleSelectListStateOptions) => UseSingleSelectListStateReturn;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useControllableState } from "./use-controllable-state.js";
|
|
2
|
+
import { useCollection } from "./use-collection.js";
|
|
3
|
+
import { useSelectionManager } from "./use-selection-manager.js";
|
|
4
|
+
import { computed, toValue, watch } from "vue";
|
|
5
|
+
//#region src/composables/use-single-select-list-state.ts
|
|
6
|
+
/**
|
|
7
|
+
* Which item of a collection is selected, for a list that always has exactly one.
|
|
8
|
+
*
|
|
9
|
+
* Ported from React Stately's `useSingleSelectListState`
|
|
10
|
+
* (`react-stately/src/list/useSingleSelectListState.ts`, react-stately 3.49.0).
|
|
11
|
+
*
|
|
12
|
+
* "Always exactly one" is `disallowEmptySelection` plus a fallback for the case where nothing says
|
|
13
|
+
* which. The selection behaviour is deliberately left at toggle: whether the arrow keys choose is a
|
|
14
|
+
* question about how activation works, asked by the caller through `selectOnFocus`, not a question
|
|
15
|
+
* about what a press means.
|
|
16
|
+
*
|
|
17
|
+
* The three pieces have to be built in one order — the collection, then the fallback that reads it,
|
|
18
|
+
* then the selection manager the fallback feeds — which is the reason this is a composable rather
|
|
19
|
+
* than a few lines in the root component.
|
|
20
|
+
*/
|
|
21
|
+
var useSingleSelectListState = (options = {}) => {
|
|
22
|
+
const collection = useCollection();
|
|
23
|
+
const disabledKeySet = computed(() => new Set(toValue(options.disabledKeys) ?? []));
|
|
24
|
+
/**
|
|
25
|
+
* Whether an item can be selected, asked of the collection rather than of the selection manager.
|
|
26
|
+
*
|
|
27
|
+
* Two reasons, and either alone would be enough: the fallback below runs before the manager
|
|
28
|
+
* exists, and React Stately asks the same question the same way — an item's own disabled state
|
|
29
|
+
* or the group's disabled keys, with no `disabledBehavior` in it.
|
|
30
|
+
*/
|
|
31
|
+
const isKeyDisabled = (key) => disabledKeySet.value.has(key) || Boolean(collection.getItem(key)?.isDisabled());
|
|
32
|
+
/** The first item that can be selected, or the first item when none of them can. */
|
|
33
|
+
const findDefaultSelectedKey = () => collection.withOrder(() => {
|
|
34
|
+
let key = collection.getFirstKey();
|
|
35
|
+
while (key != null && isKeyDisabled(key) && key !== collection.getLastKey()) key = collection.getKeyAfter(key);
|
|
36
|
+
if (key != null && isKeyDisabled(key) && key === collection.getLastKey()) key = collection.getFirstKey();
|
|
37
|
+
return key;
|
|
38
|
+
});
|
|
39
|
+
const stored = useControllableState({
|
|
40
|
+
defaultValue: options.defaultSelectedKey ?? null,
|
|
41
|
+
onValueChange: (key) => {
|
|
42
|
+
if (key != null) options.onSelectionChange?.(key);
|
|
43
|
+
},
|
|
44
|
+
value: () => toValue(options.selectedKey)
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* The selected item, with the fallback resolved as a read rather than written back.
|
|
48
|
+
*
|
|
49
|
+
* React Stately can resolve it before the first render, because the collection is built by a
|
|
50
|
+
* pass that runs before it. Rendering is what creates the collection here, so writing the
|
|
51
|
+
* fallback into the selection would report a selection change nobody made, on every mount of
|
|
52
|
+
* every uncontrolled list.
|
|
53
|
+
*
|
|
54
|
+
* `collection.size` is read for its own sake: the fallback walks the collection through
|
|
55
|
+
* calls that stay outside reactivity on purpose, so without it this would resolve against the
|
|
56
|
+
* empty collection of the first render and keep that answer.
|
|
57
|
+
*/
|
|
58
|
+
const selectedKey = computed(() => {
|
|
59
|
+
const key = stored.state.value;
|
|
60
|
+
if (stored.isControlled.value) return key ?? null;
|
|
61
|
+
if (key != null && collection.getItem(key)) return key;
|
|
62
|
+
return findDefaultSelectedKey();
|
|
63
|
+
});
|
|
64
|
+
const selection = useSelectionManager({
|
|
65
|
+
collection,
|
|
66
|
+
disabledKeys: () => toValue(options.disabledKeys),
|
|
67
|
+
disallowEmptySelection: true,
|
|
68
|
+
onSelectionChange: (keys) => {
|
|
69
|
+
if (keys === "all") return;
|
|
70
|
+
const [key] = [...keys];
|
|
71
|
+
if (key != null) stored.setState(key);
|
|
72
|
+
},
|
|
73
|
+
selectedKeys: () => selectedKey.value != null ? [selectedKey.value] : [],
|
|
74
|
+
selectionMode: "single"
|
|
75
|
+
});
|
|
76
|
+
let lastSelectedKey = selectedKey.value;
|
|
77
|
+
/**
|
|
78
|
+
* The selection places focus, because on a list like this it is the only thing that can.
|
|
79
|
+
*
|
|
80
|
+
* React Aria's tab list element ends up with no tab index of its own, so the roving index on
|
|
81
|
+
* the selected item is the only way in — and with no focused key nothing inside is tabbable
|
|
82
|
+
* either, which would put the whole list out of the tab order.
|
|
83
|
+
*
|
|
84
|
+
* Post-flush and keyed on the collection's size, because setting a focused key the collection
|
|
85
|
+
* does not hold is refused: the first attempt runs before the items have registered, and the
|
|
86
|
+
* registration is what makes the retry land.
|
|
87
|
+
*/
|
|
88
|
+
watch([
|
|
89
|
+
selectedKey,
|
|
90
|
+
collection.size,
|
|
91
|
+
() => selection.focusedKey.value == null
|
|
92
|
+
], () => {
|
|
93
|
+
const key = selectedKey.value;
|
|
94
|
+
if (key == null) return;
|
|
95
|
+
if (selection.focusedKey.value == null || !selection.isFocused.value && key !== lastSelectedKey) selection.setFocusedKey(key);
|
|
96
|
+
lastSelectedKey = key;
|
|
97
|
+
}, {
|
|
98
|
+
flush: "post",
|
|
99
|
+
immediate: true
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
collection,
|
|
103
|
+
isDisabled: computed(() => Boolean(toValue(options.isDisabled))),
|
|
104
|
+
selectedKey,
|
|
105
|
+
selection,
|
|
106
|
+
setSelectedKey: (key) => stored.setState(key)
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
//#endregion
|
|
110
|
+
export { useSingleSelectListState };
|