bits-ui 1.4.1 → 1.4.2
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/bits/alert-dialog/components/alert-dialog-content.svelte +1 -1
- package/dist/bits/command/command.svelte.d.ts +2 -3
- package/dist/bits/command/command.svelte.js +28 -42
- package/dist/bits/command/types.d.ts +1 -1
- package/dist/bits/dialog/components/dialog-content.svelte +3 -1
- package/dist/bits/dialog/dialog.svelte.d.ts +0 -2
- package/dist/bits/dialog/dialog.svelte.js +1 -3
- package/dist/bits/utilities/focus-scope/use-focus-scope.svelte.js +49 -13
- package/dist/internal/focus.js +1 -2
- package/package.json +1 -1
|
@@ -28,7 +28,6 @@ declare class CommandRootState {
|
|
|
28
28
|
labelNode: HTMLElement | null;
|
|
29
29
|
commandState: CommandState;
|
|
30
30
|
_commandState: CommandState;
|
|
31
|
-
searchHasHadValue: boolean;
|
|
32
31
|
setState<K extends keyof CommandState>(key: K, value: CommandState[K], opts?: boolean): void;
|
|
33
32
|
constructor(opts: CommandRootStateProps);
|
|
34
33
|
/**
|
|
@@ -104,7 +103,7 @@ declare class CommandRootState {
|
|
|
104
103
|
* @param keywords - Optional search boost terms
|
|
105
104
|
* @returns Cleanup function
|
|
106
105
|
*/
|
|
107
|
-
registerValue(
|
|
106
|
+
registerValue(value: string, keywords?: string[]): () => void;
|
|
108
107
|
/**
|
|
109
108
|
* Registers item in command list and its group.
|
|
110
109
|
* Handles filtering, sorting and selection updates.
|
|
@@ -153,8 +152,8 @@ declare class CommandGroupContainerState {
|
|
|
153
152
|
readonly opts: CommandGroupContainerStateProps;
|
|
154
153
|
readonly root: CommandRootState;
|
|
155
154
|
headingNode: HTMLElement | null;
|
|
156
|
-
shouldRender: boolean;
|
|
157
155
|
trueValue: string;
|
|
156
|
+
shouldRender: boolean;
|
|
158
157
|
constructor(opts: CommandGroupContainerStateProps, root: CommandRootState);
|
|
159
158
|
props: {
|
|
160
159
|
readonly id: string;
|
|
@@ -5,7 +5,6 @@ import { kbd } from "../../internal/kbd.js";
|
|
|
5
5
|
import { getAriaDisabled, getAriaExpanded, getAriaSelected, getDataDisabled, getDataSelected, } from "../../internal/attrs.js";
|
|
6
6
|
import { getFirstNonCommentChild } from "../../internal/dom.js";
|
|
7
7
|
import { computeCommandScore } from "./index.js";
|
|
8
|
-
import { noop } from "../../internal/noop.js";
|
|
9
8
|
// attributes
|
|
10
9
|
const COMMAND_ROOT_ATTR = "data-command-root";
|
|
11
10
|
const COMMAND_LIST_ATTR = "data-command-list";
|
|
@@ -60,8 +59,6 @@ class CommandRootState {
|
|
|
60
59
|
commandState = $state.raw(defaultState);
|
|
61
60
|
// internal state that we mutate in batches and publish to the `state` at once
|
|
62
61
|
_commandState = $state(defaultState);
|
|
63
|
-
// whether the search has had a value other than ""
|
|
64
|
-
searchHasHadValue = $state(false);
|
|
65
62
|
#snapshot() {
|
|
66
63
|
return $state.snapshot(this._commandState);
|
|
67
64
|
}
|
|
@@ -87,10 +84,6 @@ class CommandRootState {
|
|
|
87
84
|
// Filter synchronously before emitting back to children
|
|
88
85
|
this.#filterItems();
|
|
89
86
|
this.#sort();
|
|
90
|
-
this.#selectFirstItem();
|
|
91
|
-
afterTick(() => {
|
|
92
|
-
this.#selectFirstItem();
|
|
93
|
-
});
|
|
94
87
|
}
|
|
95
88
|
else if (key === "value") {
|
|
96
89
|
// opts is a boolean referring to whether it should NOT be scrolled into view
|
|
@@ -108,11 +101,6 @@ class CommandRootState {
|
|
|
108
101
|
this.commandState = defaults;
|
|
109
102
|
useRefById(opts);
|
|
110
103
|
this.onkeydown = this.onkeydown.bind(this);
|
|
111
|
-
$effect(() => {
|
|
112
|
-
if (this._commandState.search !== "") {
|
|
113
|
-
this.searchHasHadValue = true;
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
104
|
}
|
|
117
105
|
/**
|
|
118
106
|
* Calculates score for an item based on search text and keywords.
|
|
@@ -135,8 +123,10 @@ class CommandRootState {
|
|
|
135
123
|
#sort() {
|
|
136
124
|
if (!this._commandState.search || this.opts.shouldFilter.current === false) {
|
|
137
125
|
// If no search and no selection yet, select first item
|
|
138
|
-
|
|
139
|
-
|
|
126
|
+
this.#selectFirstItem();
|
|
127
|
+
// if (!this.commandState.value) {
|
|
128
|
+
// this.#selectFirstItem();
|
|
129
|
+
// }
|
|
140
130
|
return;
|
|
141
131
|
}
|
|
142
132
|
const scores = this._commandState.filtered.items;
|
|
@@ -161,8 +151,8 @@ class CommandRootState {
|
|
|
161
151
|
// Sort groups to bottom (pushes all non-grouped items to the top)
|
|
162
152
|
const listInsertionElement = this.viewportNode;
|
|
163
153
|
const sorted = this.getValidItems().sort((a, b) => {
|
|
164
|
-
const valueA = a.getAttribute("
|
|
165
|
-
const valueB = b.getAttribute("
|
|
154
|
+
const valueA = a.getAttribute("data-value");
|
|
155
|
+
const valueB = b.getAttribute("data-value");
|
|
166
156
|
const scoresA = scores.get(valueA) ?? 0;
|
|
167
157
|
const scoresB = scores.get(valueB) ?? 0;
|
|
168
158
|
return scoresB - scoresA;
|
|
@@ -191,6 +181,7 @@ class CommandRootState {
|
|
|
191
181
|
const element = listInsertionElement?.querySelector(`${COMMAND_GROUP_SELECTOR}[${COMMAND_VALUE_ATTR}="${encodeURIComponent(group[0])}"]`);
|
|
192
182
|
element?.parentElement?.appendChild(element);
|
|
193
183
|
}
|
|
184
|
+
this.#selectFirstItem();
|
|
194
185
|
}
|
|
195
186
|
/**
|
|
196
187
|
* Sets current value and triggers re-render if cleared.
|
|
@@ -399,11 +390,11 @@ class CommandRootState {
|
|
|
399
390
|
* @param keywords - Optional search boost terms
|
|
400
391
|
* @returns Cleanup function
|
|
401
392
|
*/
|
|
402
|
-
registerValue(
|
|
403
|
-
if (value === this.allIds.get(
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
this._commandState.filtered.items.set(
|
|
393
|
+
registerValue(value, keywords) {
|
|
394
|
+
if (!(value && value === this.allIds.get(value)?.value)) {
|
|
395
|
+
this.allIds.set(value, { value, keywords });
|
|
396
|
+
}
|
|
397
|
+
this._commandState.filtered.items.set(value, this.#score(value, keywords));
|
|
407
398
|
// Schedule sorting to run after this tick when all items are added not each time an item is added
|
|
408
399
|
if (!this.sortAfterTick) {
|
|
409
400
|
this.sortAfterTick = true;
|
|
@@ -413,7 +404,7 @@ class CommandRootState {
|
|
|
413
404
|
});
|
|
414
405
|
}
|
|
415
406
|
return () => {
|
|
416
|
-
this.allIds.delete(
|
|
407
|
+
this.allIds.delete(value);
|
|
417
408
|
};
|
|
418
409
|
}
|
|
419
410
|
/**
|
|
@@ -587,9 +578,7 @@ class CommandEmptyState {
|
|
|
587
578
|
root;
|
|
588
579
|
#isInitialRender = true;
|
|
589
580
|
shouldRender = $derived.by(() => {
|
|
590
|
-
return ((this.root._commandState.filtered.count === 0 &&
|
|
591
|
-
this.#isInitialRender === false &&
|
|
592
|
-
this.root.searchHasHadValue) ||
|
|
581
|
+
return ((this.root._commandState.filtered.count === 0 && this.#isInitialRender === false) ||
|
|
593
582
|
this.opts.forceMount.current);
|
|
594
583
|
});
|
|
595
584
|
constructor(opts, root) {
|
|
@@ -613,6 +602,7 @@ class CommandGroupContainerState {
|
|
|
613
602
|
opts;
|
|
614
603
|
root;
|
|
615
604
|
headingNode = $state(null);
|
|
605
|
+
trueValue = $state("");
|
|
616
606
|
shouldRender = $derived.by(() => {
|
|
617
607
|
if (this.opts.forceMount.current)
|
|
618
608
|
return true;
|
|
@@ -620,9 +610,8 @@ class CommandGroupContainerState {
|
|
|
620
610
|
return true;
|
|
621
611
|
if (!this.root.commandState.search)
|
|
622
612
|
return true;
|
|
623
|
-
return this.root.
|
|
613
|
+
return this.root._commandState.filtered.groups.has(this.trueValue);
|
|
624
614
|
});
|
|
625
|
-
trueValue = $state("");
|
|
626
615
|
constructor(opts, root) {
|
|
627
616
|
this.opts = opts;
|
|
628
617
|
this.root = root;
|
|
@@ -631,21 +620,21 @@ class CommandGroupContainerState {
|
|
|
631
620
|
...opts,
|
|
632
621
|
deps: () => this.shouldRender,
|
|
633
622
|
});
|
|
634
|
-
watch(() => this.
|
|
635
|
-
return this.root.registerGroup(this.
|
|
623
|
+
watch(() => this.trueValue, () => {
|
|
624
|
+
return this.root.registerGroup(this.trueValue);
|
|
636
625
|
});
|
|
637
626
|
$effect(() => {
|
|
638
627
|
if (this.opts.value.current) {
|
|
639
628
|
this.trueValue = this.opts.value.current;
|
|
640
|
-
return this.root.registerValue(this.opts.
|
|
629
|
+
return this.root.registerValue(this.opts.value.current);
|
|
641
630
|
}
|
|
642
631
|
else if (this.headingNode && this.headingNode.textContent) {
|
|
643
632
|
this.trueValue = this.headingNode.textContent.trim().toLowerCase();
|
|
644
|
-
return this.root.registerValue(this.
|
|
633
|
+
return this.root.registerValue(this.trueValue);
|
|
645
634
|
}
|
|
646
635
|
else if (this.opts.ref.current?.textContent) {
|
|
647
636
|
this.trueValue = this.opts.ref.current.textContent.trim().toLowerCase();
|
|
648
|
-
return this.root.registerValue(this.
|
|
637
|
+
return this.root.registerValue(this.trueValue);
|
|
649
638
|
}
|
|
650
639
|
});
|
|
651
640
|
}
|
|
@@ -750,7 +739,7 @@ class CommandItemState {
|
|
|
750
739
|
!this.root.commandState.search) {
|
|
751
740
|
return true;
|
|
752
741
|
}
|
|
753
|
-
const currentScore = this.root.commandState.filtered.items.get(this.
|
|
742
|
+
const currentScore = this.root.commandState.filtered.items.get(this.trueValue);
|
|
754
743
|
if (currentScore === undefined)
|
|
755
744
|
return false;
|
|
756
745
|
return currentScore > 0;
|
|
@@ -766,23 +755,20 @@ class CommandItemState {
|
|
|
766
755
|
deps: () => Boolean(this.root.commandState.search),
|
|
767
756
|
});
|
|
768
757
|
watch([
|
|
769
|
-
() => this.
|
|
770
|
-
() => this.#group?.
|
|
758
|
+
() => this.trueValue,
|
|
759
|
+
() => this.#group?.trueValue,
|
|
771
760
|
() => this.opts.forceMount.current,
|
|
772
|
-
() => this.opts.ref.current,
|
|
773
761
|
], () => {
|
|
774
762
|
if (this.opts.forceMount.current)
|
|
775
763
|
return;
|
|
776
|
-
return this.root.registerItem(this.
|
|
764
|
+
return this.root.registerItem(this.trueValue, this.#group?.trueValue);
|
|
777
765
|
});
|
|
778
766
|
watch([() => this.opts.value.current, () => this.opts.ref.current], () => {
|
|
779
|
-
if (!this.opts.ref.current)
|
|
780
|
-
return;
|
|
781
|
-
if (!this.opts.value.current && this.opts.ref.current.textContent) {
|
|
767
|
+
if (!this.opts.value.current && this.opts.ref.current?.textContent) {
|
|
782
768
|
this.trueValue = this.opts.ref.current.textContent.trim();
|
|
783
769
|
}
|
|
784
|
-
this.root.registerValue(this.
|
|
785
|
-
this.opts.ref.current
|
|
770
|
+
this.root.registerValue(this.trueValue, opts.keywords.current.map((kw) => kw.trim()));
|
|
771
|
+
this.opts.ref.current?.setAttribute(COMMAND_VALUE_ATTR, this.trueValue);
|
|
786
772
|
});
|
|
787
773
|
// bindings
|
|
788
774
|
this.onclick = this.onclick.bind(this);
|
|
@@ -8,7 +8,7 @@ export type CommandState = {
|
|
|
8
8
|
filtered: {
|
|
9
9
|
/** The count of all visible items. */
|
|
10
10
|
count: number;
|
|
11
|
-
/** Map from visible item
|
|
11
|
+
/** Map from visible item value to its search store. */
|
|
12
12
|
items: Map<string, number>;
|
|
13
13
|
/** Set of groups with at least one visible item. */
|
|
14
14
|
groups: Set<string>;
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
ref = $bindable(null),
|
|
20
20
|
forceMount = false,
|
|
21
21
|
onCloseAutoFocus = noop,
|
|
22
|
+
onOpenAutoFocus = noop,
|
|
22
23
|
onEscapeKeydown = noop,
|
|
23
24
|
onInteractOutside = noop,
|
|
24
25
|
trapFocus = true,
|
|
@@ -52,7 +53,8 @@
|
|
|
52
53
|
trapFocus,
|
|
53
54
|
open: contentState.root.opts.open.current,
|
|
54
55
|
})}
|
|
55
|
-
{
|
|
56
|
+
{onOpenAutoFocus}
|
|
57
|
+
{id}
|
|
56
58
|
onCloseAutoFocus={(e) => {
|
|
57
59
|
onCloseAutoFocus(e);
|
|
58
60
|
if (e.defaultPrevented) return;
|
|
@@ -9,7 +9,6 @@ type DialogRootStateProps = WritableBoxedValues<{
|
|
|
9
9
|
declare class DialogRootState {
|
|
10
10
|
readonly opts: DialogRootStateProps;
|
|
11
11
|
triggerNode: HTMLElement | null;
|
|
12
|
-
titleNode: HTMLElement | null;
|
|
13
12
|
contentNode: HTMLElement | null;
|
|
14
13
|
descriptionNode: HTMLElement | null;
|
|
15
14
|
contentId: string | undefined;
|
|
@@ -95,7 +94,6 @@ declare class DialogTitleState {
|
|
|
95
94
|
props: {
|
|
96
95
|
readonly "data-state": "open" | "closed";
|
|
97
96
|
readonly id: string;
|
|
98
|
-
readonly role: "heading";
|
|
99
97
|
readonly "aria-level": 1 | 2 | 3 | 4 | 5 | 6;
|
|
100
98
|
};
|
|
101
99
|
}
|
|
@@ -17,7 +17,6 @@ function createAttrs(variant) {
|
|
|
17
17
|
class DialogRootState {
|
|
18
18
|
opts;
|
|
19
19
|
triggerNode = $state(null);
|
|
20
|
-
titleNode = $state(null);
|
|
21
20
|
contentNode = $state(null);
|
|
22
21
|
descriptionNode = $state(null);
|
|
23
22
|
contentId = $state(undefined);
|
|
@@ -151,7 +150,6 @@ class DialogTitleState {
|
|
|
151
150
|
useRefById({
|
|
152
151
|
...opts,
|
|
153
152
|
onRefChange: (node) => {
|
|
154
|
-
this.root.titleNode = node;
|
|
155
153
|
this.root.titleId = node?.id;
|
|
156
154
|
},
|
|
157
155
|
deps: () => this.root.opts.open.current,
|
|
@@ -159,7 +157,7 @@ class DialogTitleState {
|
|
|
159
157
|
}
|
|
160
158
|
props = $derived.by(() => ({
|
|
161
159
|
id: this.opts.id.current,
|
|
162
|
-
role: "heading",
|
|
160
|
+
// role: "heading",
|
|
163
161
|
"aria-level": this.opts.level.current,
|
|
164
162
|
[this.root.attrs.title]: "",
|
|
165
163
|
...this.root.sharedProps,
|
|
@@ -56,15 +56,46 @@ export function useFocusScope({ id, loop, enabled, onOpenAutoFocus, onCloseAutoF
|
|
|
56
56
|
focusScope.isHandlingFocus = false;
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
function handleMutations(
|
|
66
|
-
|
|
67
|
-
if (!
|
|
59
|
+
/**
|
|
60
|
+
* Handles DOM mutations within the container. Specifically checks if the
|
|
61
|
+
* last known focused element inside the container has been removed. If so,
|
|
62
|
+
* and focus has escaped the container (likely moved to document.body),
|
|
63
|
+
* it refocuses the container itself to maintain the trap.
|
|
64
|
+
*/
|
|
65
|
+
function handleMutations(mutations) {
|
|
66
|
+
// if there's no record of a last focused el, or container isn't mounted, bail
|
|
67
|
+
if (!lastFocusedElement || !ref.current)
|
|
68
|
+
return;
|
|
69
|
+
// track if the last focused element was removed
|
|
70
|
+
let elementWasRemoved = false;
|
|
71
|
+
for (const mutation of mutations) {
|
|
72
|
+
// we only care about mutations where nodes were removed
|
|
73
|
+
if (mutation.type === "childList" && mutation.removedNodes.length > 0) {
|
|
74
|
+
// check if any removed nodes are the last focused element or contain it
|
|
75
|
+
for (const removedNode of mutation.removedNodes) {
|
|
76
|
+
if (removedNode === lastFocusedElement) {
|
|
77
|
+
elementWasRemoved = true;
|
|
78
|
+
// found it directly
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
// contains() only works on elements, so we need to check nodeType
|
|
82
|
+
if (removedNode.nodeType === Node.ELEMENT_NODE &&
|
|
83
|
+
removedNode.contains(lastFocusedElement)) {
|
|
84
|
+
elementWasRemoved = true;
|
|
85
|
+
// descendant found,
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// if we've confirmed removal in any mutation, bail
|
|
91
|
+
if (elementWasRemoved)
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* If the element was removed and focus is now outside the container,
|
|
96
|
+
* (e.g., browser moved it to body), refocus the container.
|
|
97
|
+
*/
|
|
98
|
+
if (elementWasRemoved && ref.current && !ref.current.contains(document.activeElement)) {
|
|
68
99
|
focus(ref.current);
|
|
69
100
|
}
|
|
70
101
|
}
|
|
@@ -73,7 +104,11 @@ export function useFocusScope({ id, loop, enabled, onOpenAutoFocus, onCloseAutoF
|
|
|
73
104
|
return;
|
|
74
105
|
const removeEvents = executeCallbacks(on(document, "focusin", manageFocus), on(document, "focusout", manageFocus));
|
|
75
106
|
const mutationObserver = new MutationObserver(handleMutations);
|
|
76
|
-
mutationObserver.observe(container, {
|
|
107
|
+
mutationObserver.observe(container, {
|
|
108
|
+
childList: true,
|
|
109
|
+
subtree: true,
|
|
110
|
+
attributes: false,
|
|
111
|
+
});
|
|
77
112
|
return () => {
|
|
78
113
|
removeEvents();
|
|
79
114
|
mutationObserver.disconnect();
|
|
@@ -115,10 +150,11 @@ export function useFocusScope({ id, loop, enabled, onOpenAutoFocus, onCloseAutoF
|
|
|
115
150
|
afterTick(() => {
|
|
116
151
|
if (!container)
|
|
117
152
|
return;
|
|
118
|
-
focusFirst(removeLinks(getTabbableCandidates(container)), {
|
|
119
|
-
|
|
153
|
+
const result = focusFirst(removeLinks(getTabbableCandidates(container)), {
|
|
154
|
+
select: true,
|
|
155
|
+
});
|
|
156
|
+
if (!result)
|
|
120
157
|
focus(container);
|
|
121
|
-
}
|
|
122
158
|
});
|
|
123
159
|
}
|
|
124
160
|
}
|
package/dist/internal/focus.js
CHANGED
|
@@ -51,9 +51,8 @@ export function focusFirst(candidates, { select = false } = {}) {
|
|
|
51
51
|
const previouslyFocusedElement = document.activeElement;
|
|
52
52
|
for (const candidate of candidates) {
|
|
53
53
|
focus(candidate, { select });
|
|
54
|
-
if (document.activeElement !== previouslyFocusedElement)
|
|
54
|
+
if (document.activeElement !== previouslyFocusedElement)
|
|
55
55
|
return true;
|
|
56
|
-
}
|
|
57
56
|
}
|
|
58
57
|
}
|
|
59
58
|
/**
|