bits-ui 1.0.0-next.91 → 1.0.0-next.93
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/command/command.svelte.d.ts +0 -1
- package/dist/bits/command/command.svelte.js +50 -61
- package/dist/bits/command/components/command.svelte +3 -2
- package/dist/bits/command/compute-command-score.d.ts +26 -0
- package/dist/bits/command/{command-score.js → compute-command-score.js} +47 -15
- package/dist/bits/command/index.d.ts +1 -0
- package/dist/bits/command/index.js +1 -0
- package/dist/bits/command/types.d.ts +2 -1
- package/dist/bits/index.d.ts +1 -1
- package/dist/bits/index.js +1 -1
- package/dist/bits/tooltip/tooltip.svelte.js +1 -1
- package/dist/bits/utilities/focus-scope/focus-scope-stack.svelte.d.ts +4 -2
- package/dist/bits/utilities/focus-scope/focus-scope-stack.svelte.js +17 -10
- package/dist/bits/utilities/focus-scope/use-focus-scope.svelte.js +40 -52
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/focus.js +2 -0
- package/package.json +1 -1
- package/dist/bits/command/command-score.d.ts +0 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { CommandState } from "./types.js";
|
|
2
2
|
import type { BitsKeyboardEvent, BitsMouseEvent, BitsPointerEvent, WithRefProps } from "../../internal/types.js";
|
|
3
3
|
import type { ReadableBoxedValues, WritableBoxedValues } from "../../internal/box.svelte.js";
|
|
4
|
-
export declare function defaultFilter(value: string, search: string, keywords?: string[]): number;
|
|
5
4
|
type CommandRootStateProps = WithRefProps<ReadableBoxedValues<{
|
|
6
5
|
filter: (value: string, search: string, keywords?: string[]) => number;
|
|
7
6
|
shouldFilter: boolean;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { untrack } from "svelte";
|
|
2
1
|
import { afterSleep, afterTick, srOnlyStyles, useRefById } from "svelte-toolbelt";
|
|
3
|
-
import { Context } from "runed";
|
|
2
|
+
import { Context, watch } from "runed";
|
|
4
3
|
import { findNextSibling, findPreviousSibling } from "./utils.js";
|
|
5
|
-
import { commandScore } from "./command-score.js";
|
|
6
4
|
import { kbd } from "../../internal/kbd.js";
|
|
7
5
|
import { getAriaDisabled, getAriaExpanded, getAriaSelected, getDataDisabled, getDataSelected, } from "../../internal/attrs.js";
|
|
8
6
|
import { getFirstNonCommentChild } from "../../internal/dom.js";
|
|
7
|
+
import { computeCommandScore } from "./index.js";
|
|
8
|
+
// attributes
|
|
9
9
|
const COMMAND_ROOT_ATTR = "data-command-root";
|
|
10
10
|
const COMMAND_LIST_ATTR = "data-command-list";
|
|
11
11
|
const COMMAND_INPUT_ATTR = "data-command-input";
|
|
@@ -18,15 +18,13 @@ const COMMAND_GROUP_HEADING_ATTR = "data-command-group-heading";
|
|
|
18
18
|
const COMMAND_ITEM_ATTR = "data-command-item";
|
|
19
19
|
const COMMAND_VIEWPORT_ATTR = "data-command-viewport";
|
|
20
20
|
const COMMAND_INPUT_LABEL_ATTR = "data-command-input-label";
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
return commandScore(value, search, keywords);
|
|
29
|
-
}
|
|
21
|
+
const COMMAND_VALUE_ATTR = "data-value";
|
|
22
|
+
// selectors
|
|
23
|
+
const COMMAND_GROUP_SELECTOR = `[${COMMAND_GROUP_ATTR}]`;
|
|
24
|
+
const COMMAND_GROUP_ITEMS_SELECTOR = `[${COMMAND_GROUP_ITEMS_ATTR}]`;
|
|
25
|
+
const COMMAND_GROUP_HEADING_SELECTOR = `[${COMMAND_GROUP_HEADING_ATTR}]`;
|
|
26
|
+
const COMMAND_ITEM_SELECTOR = `[${COMMAND_ITEM_ATTR}]`;
|
|
27
|
+
const COMMAND_VALID_ITEM_SELECTOR = `${COMMAND_ITEM_SELECTOR}:not([aria-disabled="true"])`;
|
|
30
28
|
const CommandRootContext = new Context("Command.Root");
|
|
31
29
|
const CommandListContext = new Context("Command.List");
|
|
32
30
|
const CommandGroupContainerContext = new Context("Command.Group");
|
|
@@ -103,7 +101,7 @@ class CommandRootState {
|
|
|
103
101
|
this.onkeydown = this.onkeydown.bind(this);
|
|
104
102
|
}
|
|
105
103
|
#score(value, keywords) {
|
|
106
|
-
const filter = this.opts.filter.current ??
|
|
104
|
+
const filter = this.opts.filter.current ?? computeCommandScore;
|
|
107
105
|
const score = value ? filter(value, this._commandState.search, keywords) : 0;
|
|
108
106
|
return score;
|
|
109
107
|
}
|
|
@@ -143,11 +141,11 @@ class CommandRootState {
|
|
|
143
141
|
return scoresB - scoresA;
|
|
144
142
|
});
|
|
145
143
|
for (const item of sorted) {
|
|
146
|
-
const group = item.closest(
|
|
144
|
+
const group = item.closest(COMMAND_GROUP_ITEMS_SELECTOR);
|
|
147
145
|
if (group) {
|
|
148
146
|
const itemToAppend = item.parentElement === group
|
|
149
147
|
? item
|
|
150
|
-
: item.closest(`${
|
|
148
|
+
: item.closest(`${COMMAND_GROUP_ITEMS_SELECTOR} > *`);
|
|
151
149
|
if (itemToAppend) {
|
|
152
150
|
group.appendChild(itemToAppend);
|
|
153
151
|
}
|
|
@@ -155,7 +153,7 @@ class CommandRootState {
|
|
|
155
153
|
else {
|
|
156
154
|
const itemToAppend = item.parentElement === listInsertionElement
|
|
157
155
|
? item
|
|
158
|
-
: item.closest(`${
|
|
156
|
+
: item.closest(`${COMMAND_GROUP_ITEMS_SELECTOR} > *`);
|
|
159
157
|
if (itemToAppend) {
|
|
160
158
|
listInsertionElement?.appendChild(itemToAppend);
|
|
161
159
|
}
|
|
@@ -163,7 +161,7 @@ class CommandRootState {
|
|
|
163
161
|
}
|
|
164
162
|
const sortedGroups = groups.sort((a, b) => b[1] - a[1]);
|
|
165
163
|
for (const group of sortedGroups) {
|
|
166
|
-
const element = listInsertionElement?.querySelector(`${
|
|
164
|
+
const element = listInsertionElement?.querySelector(`${COMMAND_GROUP_SELECTOR}[${COMMAND_VALUE_ATTR}="${encodeURIComponent(group[0])}"]`);
|
|
167
165
|
element?.parentElement?.appendChild(element);
|
|
168
166
|
}
|
|
169
167
|
}
|
|
@@ -179,7 +177,7 @@ class CommandRootState {
|
|
|
179
177
|
#selectFirstItem() {
|
|
180
178
|
afterTick(() => {
|
|
181
179
|
const item = this.#getValidItems().find((item) => item.getAttribute("aria-disabled") !== "true");
|
|
182
|
-
const value = item?.getAttribute(
|
|
180
|
+
const value = item?.getAttribute(COMMAND_VALUE_ATTR);
|
|
183
181
|
this.setValue(value || "");
|
|
184
182
|
});
|
|
185
183
|
}
|
|
@@ -216,14 +214,14 @@ class CommandRootState {
|
|
|
216
214
|
const node = this.opts.ref.current;
|
|
217
215
|
if (!node)
|
|
218
216
|
return [];
|
|
219
|
-
const validItems = Array.from(node.querySelectorAll(
|
|
217
|
+
const validItems = Array.from(node.querySelectorAll(COMMAND_VALID_ITEM_SELECTOR)).filter((el) => !!el);
|
|
220
218
|
return validItems;
|
|
221
219
|
}
|
|
222
220
|
#getSelectedItem() {
|
|
223
221
|
const node = this.opts.ref.current;
|
|
224
222
|
if (!node)
|
|
225
223
|
return;
|
|
226
|
-
const selectedNode = node.querySelector(`${
|
|
224
|
+
const selectedNode = node.querySelector(`${COMMAND_VALID_ITEM_SELECTOR}[aria-selected="true"]`);
|
|
227
225
|
if (!selectedNode)
|
|
228
226
|
return;
|
|
229
227
|
return selectedNode;
|
|
@@ -239,8 +237,8 @@ class CommandRootState {
|
|
|
239
237
|
const firstChildOfParent = getFirstNonCommentChild(grandparent);
|
|
240
238
|
if (firstChildOfParent && firstChildOfParent.dataset?.value === item.dataset?.value) {
|
|
241
239
|
item
|
|
242
|
-
?.closest(
|
|
243
|
-
?.querySelector(
|
|
240
|
+
?.closest(COMMAND_GROUP_SELECTOR)
|
|
241
|
+
?.querySelector(COMMAND_GROUP_HEADING_SELECTOR)
|
|
244
242
|
?.scrollIntoView({ block: "nearest" });
|
|
245
243
|
return;
|
|
246
244
|
}
|
|
@@ -251,7 +249,7 @@ class CommandRootState {
|
|
|
251
249
|
const items = this.#getValidItems();
|
|
252
250
|
const item = items[index];
|
|
253
251
|
if (item) {
|
|
254
|
-
this.setValue(item.getAttribute(
|
|
252
|
+
this.setValue(item.getAttribute(COMMAND_VALUE_ATTR) ?? "");
|
|
255
253
|
}
|
|
256
254
|
}
|
|
257
255
|
#updateSelectedByItem(change) {
|
|
@@ -269,22 +267,22 @@ class CommandRootState {
|
|
|
269
267
|
: items[index + change];
|
|
270
268
|
}
|
|
271
269
|
if (newSelected) {
|
|
272
|
-
this.setValue(newSelected.getAttribute(
|
|
270
|
+
this.setValue(newSelected.getAttribute(COMMAND_VALUE_ATTR) ?? "");
|
|
273
271
|
}
|
|
274
272
|
}
|
|
275
273
|
#updateSelectedByGroup(change) {
|
|
276
274
|
const selected = this.#getSelectedItem();
|
|
277
|
-
let group = selected?.closest(
|
|
275
|
+
let group = selected?.closest(COMMAND_GROUP_SELECTOR);
|
|
278
276
|
let item;
|
|
279
277
|
while (group && !item) {
|
|
280
278
|
group =
|
|
281
279
|
change > 0
|
|
282
|
-
? findNextSibling(group,
|
|
283
|
-
: findPreviousSibling(group,
|
|
284
|
-
item = group?.querySelector(
|
|
280
|
+
? findNextSibling(group, COMMAND_GROUP_SELECTOR)
|
|
281
|
+
: findPreviousSibling(group, COMMAND_GROUP_SELECTOR);
|
|
282
|
+
item = group?.querySelector(COMMAND_VALID_ITEM_SELECTOR);
|
|
285
283
|
}
|
|
286
284
|
if (item) {
|
|
287
|
-
this.setValue(item.getAttribute(
|
|
285
|
+
this.setValue(item.getAttribute(COMMAND_VALUE_ATTR) ?? "");
|
|
288
286
|
}
|
|
289
287
|
else {
|
|
290
288
|
this.#updateSelectedByItem(change);
|
|
@@ -534,7 +532,7 @@ class CommandInputState {
|
|
|
534
532
|
opts;
|
|
535
533
|
root;
|
|
536
534
|
#selectedItemId = $derived.by(() => {
|
|
537
|
-
const item = this.root.viewportNode?.querySelector(`${
|
|
535
|
+
const item = this.root.viewportNode?.querySelector(`${COMMAND_ITEM_SELECTOR}[${COMMAND_VALUE_ATTR}="${encodeURIComponent(this.opts.value.current)}"]`);
|
|
538
536
|
if (!item)
|
|
539
537
|
return;
|
|
540
538
|
return item?.getAttribute("id") ?? undefined;
|
|
@@ -548,21 +546,16 @@ class CommandInputState {
|
|
|
548
546
|
this.root.inputNode = node;
|
|
549
547
|
},
|
|
550
548
|
});
|
|
551
|
-
|
|
549
|
+
watch(() => this.opts.ref.current, () => {
|
|
552
550
|
const node = this.opts.ref.current;
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
});
|
|
551
|
+
if (node && this.opts.autofocus.current) {
|
|
552
|
+
afterSleep(10, () => node.focus());
|
|
553
|
+
}
|
|
558
554
|
});
|
|
559
|
-
|
|
560
|
-
this.opts.value.current
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
this.root.setState("search", this.opts.value.current);
|
|
564
|
-
}
|
|
565
|
-
});
|
|
555
|
+
watch(() => this.opts.value.current, () => {
|
|
556
|
+
if (this.root.commandState.search !== this.opts.value.current) {
|
|
557
|
+
this.root.setState("search", this.opts.value.current);
|
|
558
|
+
}
|
|
566
559
|
});
|
|
567
560
|
}
|
|
568
561
|
props = $derived.by(() => ({
|
|
@@ -609,27 +602,23 @@ class CommandItemState {
|
|
|
609
602
|
...opts,
|
|
610
603
|
deps: () => Boolean(this.root.commandState.search),
|
|
611
604
|
});
|
|
612
|
-
|
|
613
|
-
this.opts.id.current
|
|
614
|
-
this.#group?.opts.id.current
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
605
|
+
watch([
|
|
606
|
+
() => this.opts.id.current,
|
|
607
|
+
() => this.#group?.opts.id.current,
|
|
608
|
+
() => this.opts.forceMount.current,
|
|
609
|
+
], () => {
|
|
610
|
+
if (this.opts.forceMount.current)
|
|
611
|
+
return;
|
|
612
|
+
return this.root.registerItem(this.opts.id.current, this.#group?.opts.id.current);
|
|
620
613
|
});
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const node = this.opts.ref.current;
|
|
624
|
-
if (!node)
|
|
614
|
+
watch([() => this.opts.value.current, () => this.opts.ref.current], () => {
|
|
615
|
+
if (!this.opts.ref.current)
|
|
625
616
|
return;
|
|
626
|
-
if (!value &&
|
|
627
|
-
this.trueValue =
|
|
617
|
+
if (!this.opts.value.current && this.opts.ref.current.textContent) {
|
|
618
|
+
this.trueValue = this.opts.ref.current.textContent.trim();
|
|
628
619
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
node.setAttribute(VALUE_ATTR, this.trueValue);
|
|
632
|
-
});
|
|
620
|
+
this.root.registerValue(this.opts.id.current, this.trueValue, opts.keywords.current.map((kw) => kw.trim()));
|
|
621
|
+
this.opts.ref.current.setAttribute(COMMAND_VALUE_ATTR, this.trueValue);
|
|
633
622
|
});
|
|
634
623
|
// bindings
|
|
635
624
|
this.onclick = this.onclick.bind(this);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { box, mergeProps } from "svelte-toolbelt";
|
|
3
|
-
import {
|
|
3
|
+
import { useCommandRoot } from "../command.svelte.js";
|
|
4
4
|
import type { CommandRootProps } from "../types.js";
|
|
5
5
|
import CommandLabel from "./_command-label.svelte";
|
|
6
6
|
import { noop } from "../../../internal/noop.js";
|
|
7
7
|
import { useId } from "../../../internal/use-id.js";
|
|
8
|
+
import { computeCommandScore } from "../index.js";
|
|
8
9
|
|
|
9
10
|
let {
|
|
10
11
|
id = useId(),
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
onStateChange = noop,
|
|
15
16
|
loop = false,
|
|
16
17
|
shouldFilter = true,
|
|
17
|
-
filter =
|
|
18
|
+
filter = computeCommandScore,
|
|
18
19
|
label = "",
|
|
19
20
|
vimBindings = true,
|
|
20
21
|
disablePointerSelection = false,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Given a command, a search query, and (optionally) a list of keywords for the command,
|
|
3
|
+
* computes a score between 0 and 1 that represents how well the search query matches the
|
|
4
|
+
* abbreviation and keywords. 1 is a perfect match, 0 is no match.
|
|
5
|
+
*
|
|
6
|
+
* The score is calculated based on the following rules:
|
|
7
|
+
* - The scores are arranged so that a continuous match of characters will result in a total
|
|
8
|
+
* score of 1. The best case, this character is a match, and either this is the start of the string
|
|
9
|
+
* or the previous character was also a match.
|
|
10
|
+
* - A new match at the start of a word scores better than a new match elsewhere as it's more likely
|
|
11
|
+
* that the user will type the starts of fragments.
|
|
12
|
+
* - Word jumps between spaces are scored slightly higher than slashes, brackets, hyphens, etc.
|
|
13
|
+
* - A continuous match of characters will result in a total score of 1.
|
|
14
|
+
* - A new match at the start of a word scores better than a new match elsewhere as it's more likely that the user will type the starts of fragments.
|
|
15
|
+
* - Any other match isn't ideal, but we include it for completeness.
|
|
16
|
+
* - If the user transposed two letters, it should be significantly penalized.
|
|
17
|
+
* - The goodness of a match should decay slightly with each missing character.
|
|
18
|
+
* - Match higher for letters closer to the beginning of the word.
|
|
19
|
+
*
|
|
20
|
+
* @param command - The value to score against the search string (e.g. a command name like "Calculator")
|
|
21
|
+
* @param search - The search string to score against the value/aliases
|
|
22
|
+
* @param commandKeywords - An optional list of aliases/keywords to score against the search string - e.g. ["math", "add", "divide", "multiply", "subtract"]
|
|
23
|
+
* @returns A score between 0 and 1 that represents how well the search string matches the
|
|
24
|
+
* command (and keywords)
|
|
25
|
+
*/
|
|
26
|
+
export declare function computeCommandScore(command: string, search: string, commandKeywords?: string[]): number;
|
|
@@ -37,7 +37,7 @@ const PENALTY_SKIPPED = 0.999;
|
|
|
37
37
|
const PENALTY_CASE_MISMATCH = 0.9999;
|
|
38
38
|
// Match higher for letters closer to the beginning of the word
|
|
39
39
|
// If the word has more characters than the user typed, it should
|
|
40
|
-
// be
|
|
40
|
+
// be penalized slightly.
|
|
41
41
|
//
|
|
42
42
|
// i.e. "html" is more likely than "html5" if I type "html".
|
|
43
43
|
//
|
|
@@ -50,23 +50,21 @@ const IS_GAP_REGEXP = /[\\/_+.#"@[({&]/;
|
|
|
50
50
|
const COUNT_GAPS_REGEXP = /[\\/_+.#"@[({&]/g;
|
|
51
51
|
const IS_SPACE_REGEXP = /[\s-]/;
|
|
52
52
|
const COUNT_SPACE_REGEXP = /[\s-]/g;
|
|
53
|
-
function
|
|
53
|
+
function computeCommandScoreInner(string, abbreviation, lowerString, lowerAbbreviation, stringIndex, abbreviationIndex, memoizedResults) {
|
|
54
54
|
if (abbreviationIndex === abbreviation.length) {
|
|
55
|
-
if (stringIndex === string.length)
|
|
55
|
+
if (stringIndex === string.length)
|
|
56
56
|
return SCORE_CONTINUE_MATCH;
|
|
57
|
-
}
|
|
58
57
|
return PENALTY_NOT_COMPLETE;
|
|
59
58
|
}
|
|
60
59
|
const memoizeKey = `${stringIndex},${abbreviationIndex}`;
|
|
61
|
-
if (memoizedResults[memoizeKey] !== undefined)
|
|
60
|
+
if (memoizedResults[memoizeKey] !== undefined)
|
|
62
61
|
return memoizedResults[memoizeKey];
|
|
63
|
-
}
|
|
64
62
|
const abbreviationChar = lowerAbbreviation.charAt(abbreviationIndex);
|
|
65
63
|
let index = lowerString.indexOf(abbreviationChar, stringIndex);
|
|
66
64
|
let highScore = 0;
|
|
67
65
|
let score, transposedScore, wordBreaks, spaceBreaks;
|
|
68
66
|
while (index >= 0) {
|
|
69
|
-
score =
|
|
67
|
+
score = computeCommandScoreInner(string, abbreviation, lowerString, lowerAbbreviation, index + 1, abbreviationIndex + 1, memoizedResults);
|
|
70
68
|
if (score > highScore) {
|
|
71
69
|
if (index === stringIndex) {
|
|
72
70
|
score *= SCORE_CONTINUE_MATCH;
|
|
@@ -99,9 +97,9 @@ function commandScoreInner(string, abbreviation, lowerString, lowerAbbreviation,
|
|
|
99
97
|
lowerString.charAt(index - 1) ===
|
|
100
98
|
lowerAbbreviation.charAt(abbreviationIndex + 1)) ||
|
|
101
99
|
(lowerAbbreviation.charAt(abbreviationIndex + 1) ===
|
|
102
|
-
lowerAbbreviation.charAt(abbreviationIndex) &&
|
|
100
|
+
lowerAbbreviation.charAt(abbreviationIndex) &&
|
|
103
101
|
lowerString.charAt(index - 1) !== lowerAbbreviation.charAt(abbreviationIndex))) {
|
|
104
|
-
transposedScore =
|
|
102
|
+
transposedScore = computeCommandScoreInner(string, abbreviation, lowerString, lowerAbbreviation, index + 1, abbreviationIndex + 2, memoizedResults);
|
|
105
103
|
if (transposedScore * SCORE_TRANSPOSITION > score) {
|
|
106
104
|
score = transposedScore * SCORE_TRANSPOSITION;
|
|
107
105
|
}
|
|
@@ -114,15 +112,49 @@ function commandScoreInner(string, abbreviation, lowerString, lowerAbbreviation,
|
|
|
114
112
|
memoizedResults[memoizeKey] = highScore;
|
|
115
113
|
return highScore;
|
|
116
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
*
|
|
117
|
+
* @param string
|
|
118
|
+
* @returns
|
|
119
|
+
*/
|
|
117
120
|
function formatInput(string) {
|
|
118
121
|
// convert all valid space characters to space so they match each other
|
|
119
122
|
return string.toLowerCase().replace(COUNT_SPACE_REGEXP, " ");
|
|
120
123
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Given a command, a search query, and (optionally) a list of keywords for the command,
|
|
126
|
+
* computes a score between 0 and 1 that represents how well the search query matches the
|
|
127
|
+
* abbreviation and keywords. 1 is a perfect match, 0 is no match.
|
|
128
|
+
*
|
|
129
|
+
* The score is calculated based on the following rules:
|
|
130
|
+
* - The scores are arranged so that a continuous match of characters will result in a total
|
|
131
|
+
* score of 1. The best case, this character is a match, and either this is the start of the string
|
|
132
|
+
* or the previous character was also a match.
|
|
133
|
+
* - A new match at the start of a word scores better than a new match elsewhere as it's more likely
|
|
134
|
+
* that the user will type the starts of fragments.
|
|
135
|
+
* - Word jumps between spaces are scored slightly higher than slashes, brackets, hyphens, etc.
|
|
136
|
+
* - A continuous match of characters will result in a total score of 1.
|
|
137
|
+
* - A new match at the start of a word scores better than a new match elsewhere as it's more likely that the user will type the starts of fragments.
|
|
138
|
+
* - Any other match isn't ideal, but we include it for completeness.
|
|
139
|
+
* - If the user transposed two letters, it should be significantly penalized.
|
|
140
|
+
* - The goodness of a match should decay slightly with each missing character.
|
|
141
|
+
* - Match higher for letters closer to the beginning of the word.
|
|
142
|
+
*
|
|
143
|
+
* @param command - The value to score against the search string (e.g. a command name like "Calculator")
|
|
144
|
+
* @param search - The search string to score against the value/aliases
|
|
145
|
+
* @param commandKeywords - An optional list of aliases/keywords to score against the search string - e.g. ["math", "add", "divide", "multiply", "subtract"]
|
|
146
|
+
* @returns A score between 0 and 1 that represents how well the search string matches the
|
|
147
|
+
* command (and keywords)
|
|
148
|
+
*/
|
|
149
|
+
export function computeCommandScore(command, search, commandKeywords) {
|
|
150
|
+
/**
|
|
151
|
+
* NOTE: We used to do lower-casing on each recursive call, but this meant that `toLowerCase()`
|
|
152
|
+
* was the dominating cost in the algorithm. Passing both is a little ugly, but considerably
|
|
153
|
+
* faster.
|
|
125
154
|
*/
|
|
126
|
-
|
|
127
|
-
|
|
155
|
+
command =
|
|
156
|
+
commandKeywords && commandKeywords.length > 0
|
|
157
|
+
? `${`${command} ${commandKeywords?.join(" ")}`}`
|
|
158
|
+
: command;
|
|
159
|
+
return computeCommandScoreInner(command, search, formatInput(command), formatInput(search), 0, 0, {});
|
|
128
160
|
}
|
|
@@ -32,7 +32,8 @@ export type CommandRootPropsWithoutHTML = WithChild<{
|
|
|
32
32
|
* with `1` being a perfect match, and `0` being no match, resulting
|
|
33
33
|
* in the item being hidden entirely.
|
|
34
34
|
*
|
|
35
|
-
* By default, it will use the `
|
|
35
|
+
* By default, it will use the `computeCommandScore` function exported
|
|
36
|
+
* by this package to compute the score.
|
|
36
37
|
*/
|
|
37
38
|
filter?: (value: string, search: string, keywords?: string[]) => number;
|
|
38
39
|
/**
|
package/dist/bits/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { Calendar } from "./calendar/index.js";
|
|
|
7
7
|
export { Checkbox } from "./checkbox/index.js";
|
|
8
8
|
export { Collapsible } from "./collapsible/index.js";
|
|
9
9
|
export { Combobox } from "./combobox/index.js";
|
|
10
|
-
export { Command } from "./command/index.js";
|
|
10
|
+
export { Command, computeCommandScore } from "./command/index.js";
|
|
11
11
|
export { ContextMenu } from "./context-menu/index.js";
|
|
12
12
|
export { DateField } from "./date-field/index.js";
|
|
13
13
|
export { DatePicker } from "./date-picker/index.js";
|
package/dist/bits/index.js
CHANGED
|
@@ -7,7 +7,7 @@ export { Calendar } from "./calendar/index.js";
|
|
|
7
7
|
export { Checkbox } from "./checkbox/index.js";
|
|
8
8
|
export { Collapsible } from "./collapsible/index.js";
|
|
9
9
|
export { Combobox } from "./combobox/index.js";
|
|
10
|
-
export { Command } from "./command/index.js";
|
|
10
|
+
export { Command, computeCommandScore } from "./command/index.js";
|
|
11
11
|
export { ContextMenu } from "./context-menu/index.js";
|
|
12
12
|
export { DateField } from "./date-field/index.js";
|
|
13
13
|
export { DatePicker } from "./date-picker/index.js";
|
|
@@ -207,7 +207,7 @@ class TooltipContentState {
|
|
|
207
207
|
useGraceArea({
|
|
208
208
|
triggerNode: () => this.root.triggerNode,
|
|
209
209
|
contentNode: () => this.root.contentNode,
|
|
210
|
-
enabled: () => this.root.opts.open.current && this.root.disableHoverableContent,
|
|
210
|
+
enabled: () => this.root.opts.open.current && !this.root.disableHoverableContent,
|
|
211
211
|
onPointerExit: () => {
|
|
212
212
|
this.root.handleClose();
|
|
213
213
|
},
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface FocusScopeAPI {
|
|
2
2
|
id: string;
|
|
3
3
|
paused: boolean;
|
|
4
4
|
pause: () => void;
|
|
5
5
|
resume: () => void;
|
|
6
|
-
|
|
6
|
+
isHandlingFocus: boolean;
|
|
7
|
+
}
|
|
7
8
|
export declare function createFocusScopeStack(): {
|
|
8
9
|
add(focusScope: FocusScopeAPI): void;
|
|
9
10
|
remove(focusScope: FocusScopeAPI): void;
|
|
11
|
+
readonly current: FocusScopeAPI[];
|
|
10
12
|
};
|
|
11
13
|
export declare function createFocusScopeAPI(): FocusScopeAPI;
|
|
12
14
|
export declare function removeLinks(items: HTMLElement[]): HTMLElement[];
|
|
@@ -2,31 +2,38 @@ import { box } from "svelte-toolbelt";
|
|
|
2
2
|
import { useId } from "../../../internal/use-id.js";
|
|
3
3
|
const focusStack = box([]);
|
|
4
4
|
export function createFocusScopeStack() {
|
|
5
|
-
const stack = focusStack;
|
|
6
5
|
return {
|
|
7
6
|
add(focusScope) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
activeFocusScope?.pause();
|
|
7
|
+
const activeFocusScope = focusStack.current[0];
|
|
8
|
+
if (activeFocusScope && focusScope.id !== activeFocusScope.id) {
|
|
9
|
+
activeFocusScope.pause();
|
|
12
10
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
stack.current.unshift(focusScope);
|
|
11
|
+
focusStack.current = removeFromFocusScopeArray(focusStack.current, focusScope);
|
|
12
|
+
focusStack.current.unshift(focusScope);
|
|
16
13
|
},
|
|
17
14
|
remove(focusScope) {
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
focusStack.current = removeFromFocusScopeArray(focusStack.current, focusScope);
|
|
16
|
+
focusStack.current[0]?.resume();
|
|
17
|
+
},
|
|
18
|
+
get current() {
|
|
19
|
+
return focusStack.current;
|
|
20
20
|
},
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
export function createFocusScopeAPI() {
|
|
24
24
|
let paused = $state(false);
|
|
25
|
+
let isHandlingFocus = $state(false);
|
|
25
26
|
return {
|
|
26
27
|
id: useId(),
|
|
27
28
|
get paused() {
|
|
28
29
|
return paused;
|
|
29
30
|
},
|
|
31
|
+
get isHandlingFocus() {
|
|
32
|
+
return isHandlingFocus;
|
|
33
|
+
},
|
|
34
|
+
set isHandlingFocus(value) {
|
|
35
|
+
isHandlingFocus = value;
|
|
36
|
+
},
|
|
30
37
|
pause() {
|
|
31
38
|
paused = true;
|
|
32
39
|
},
|
|
@@ -21,69 +21,57 @@ export function useFocusScope({ id, loop, enabled, onOpenAutoFocus, onCloseAutoF
|
|
|
21
21
|
const focusScope = createFocusScopeAPI();
|
|
22
22
|
const ref = box(null);
|
|
23
23
|
const ctx = FocusScopeContext.getOr({ ignoreCloseAutoFocus: false });
|
|
24
|
+
let lastFocusedElement = null;
|
|
24
25
|
useRefById({
|
|
25
26
|
id,
|
|
26
27
|
ref,
|
|
27
28
|
deps: () => enabled.current,
|
|
28
29
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!container || !enabled)
|
|
30
|
+
function manageFocus(event) {
|
|
31
|
+
if (focusScope.paused || !ref.current || focusScope.isHandlingFocus)
|
|
32
32
|
return;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return;
|
|
33
|
+
focusScope.isHandlingFocus = true;
|
|
34
|
+
try {
|
|
36
35
|
const target = event.target;
|
|
37
36
|
if (!isHTMLElement(target))
|
|
38
37
|
return;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (focusScope.paused || !container || ctx.ignoreCloseAutoFocus) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const relatedTarget = event.relatedTarget;
|
|
53
|
-
if (!isHTMLElement(relatedTarget))
|
|
54
|
-
return;
|
|
55
|
-
// A `focusout` event with a `null` `relatedTarget` will happen in at least two cases:
|
|
56
|
-
//
|
|
57
|
-
// 1. When the user switches app/tabs/windows/the browser itself loses focus.
|
|
58
|
-
// 2. In Google Chrome, when the focused element is removed from the DOM.
|
|
59
|
-
//
|
|
60
|
-
// We let the browser do its thing here because:
|
|
61
|
-
//
|
|
62
|
-
// 1. The browser already keeps a memory of what's focused for when the
|
|
63
|
-
// page gets refocused.
|
|
64
|
-
// 2. In Google Chrome, if we try to focus the deleted focused element it throws
|
|
65
|
-
// the CPU to 100%, so we avoid doing anything for this reason here too.
|
|
66
|
-
if (relatedTarget === null)
|
|
67
|
-
return;
|
|
68
|
-
// If the focus has moved to an actual legitimate element (`relatedTarget !== null`)
|
|
69
|
-
// that is outside the container, we move focus to the last valid focused element inside.
|
|
70
|
-
if (!container.contains(relatedTarget)) {
|
|
71
|
-
focus(lastFocusedElement, { select: true });
|
|
38
|
+
const isWithinActiveScope = ref.current.contains(target);
|
|
39
|
+
if (event.type === "focusin") {
|
|
40
|
+
if (isWithinActiveScope) {
|
|
41
|
+
lastFocusedElement = target;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
if (ctx.ignoreCloseAutoFocus)
|
|
45
|
+
return;
|
|
46
|
+
focus(lastFocusedElement, { select: true });
|
|
47
|
+
}
|
|
72
48
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// instead of leaning on document.activeElement, we use lastFocusedElement to check
|
|
78
|
-
// if the element still exists inside the container,
|
|
79
|
-
// if not then we focus to the container
|
|
80
|
-
const handleMutations = (_) => {
|
|
81
|
-
const lastFocusedElementExists = container?.contains(lastFocusedElement);
|
|
82
|
-
if (!lastFocusedElementExists) {
|
|
83
|
-
focus(container);
|
|
49
|
+
else if (event.type === "focusout") {
|
|
50
|
+
if (!isWithinActiveScope && !ctx.ignoreCloseAutoFocus) {
|
|
51
|
+
focus(lastFocusedElement, { select: true });
|
|
52
|
+
}
|
|
84
53
|
}
|
|
85
|
-
}
|
|
86
|
-
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
focusScope.isHandlingFocus = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// When the focused element gets removed from the DOM, browsers move focus
|
|
60
|
+
// back to the document.body. In this case, we move focus to the container
|
|
61
|
+
// to keep focus trapped correctly.
|
|
62
|
+
// instead of leaning on document.activeElement, we use lastFocusedElement to check
|
|
63
|
+
// if the element still exists inside the container,
|
|
64
|
+
// if not then we focus to the container
|
|
65
|
+
function handleMutations(_) {
|
|
66
|
+
const lastFocusedElementExists = ref.current?.contains(lastFocusedElement);
|
|
67
|
+
if (!lastFocusedElementExists && ref.current) {
|
|
68
|
+
focus(ref.current);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
watch([() => ref.current, () => enabled.current], ([container, enabled]) => {
|
|
72
|
+
if (!container || !enabled)
|
|
73
|
+
return;
|
|
74
|
+
const removeEvents = executeCallbacks(on(document, "focusin", manageFocus), on(document, "focusout", manageFocus));
|
|
87
75
|
const mutationObserver = new MutationObserver(handleMutations);
|
|
88
76
|
mutationObserver.observe(container, { childList: true, subtree: true });
|
|
89
77
|
return () => {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { Accordion, AlertDialog, AspectRatio, Avatar, Button, Calendar, Checkbox, Collapsible, Combobox, Command, ContextMenu, DateField, DatePicker, DateRangeField, DateRangePicker, Dialog, DropdownMenu, Label, LinkPreview, Menubar, NavigationMenu, Pagination, PinInput, Popover, Progress, RadioGroup, RangeCalendar, ScrollArea, Select, Separator, Slider, Switch, Tabs, Toggle, ToggleGroup, Toolbar, Tooltip, Portal, IsUsingKeyboard, } from "./bits/index.js";
|
|
1
|
+
export { Accordion, AlertDialog, AspectRatio, Avatar, Button, Calendar, Checkbox, Collapsible, Combobox, Command, ContextMenu, DateField, DatePicker, DateRangeField, DateRangePicker, Dialog, DropdownMenu, Label, LinkPreview, Menubar, NavigationMenu, Pagination, PinInput, Popover, Progress, RadioGroup, RangeCalendar, ScrollArea, Select, Separator, Slider, Switch, Tabs, Toggle, ToggleGroup, Toolbar, Tooltip, Portal, IsUsingKeyboard, computeCommandScore, } from "./bits/index.js";
|
|
2
2
|
export * from "./shared/index.js";
|
|
3
3
|
export type * from "./shared/index.js";
|
|
4
4
|
export * from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { Accordion, AlertDialog, AspectRatio, Avatar, Button, Calendar, Checkbox, Collapsible, Combobox, Command, ContextMenu, DateField, DatePicker, DateRangeField, DateRangePicker, Dialog, DropdownMenu, Label, LinkPreview, Menubar, NavigationMenu, Pagination, PinInput, Popover, Progress, RadioGroup, RangeCalendar, ScrollArea, Select, Separator, Slider, Switch, Tabs, Toggle, ToggleGroup, Toolbar, Tooltip, Portal, IsUsingKeyboard, } from "./bits/index.js";
|
|
1
|
+
export { Accordion, AlertDialog, AspectRatio, Avatar, Button, Calendar, Checkbox, Collapsible, Combobox, Command, ContextMenu, DateField, DatePicker, DateRangeField, DateRangePicker, Dialog, DropdownMenu, Label, LinkPreview, Menubar, NavigationMenu, Pagination, PinInput, Popover, Progress, RadioGroup, RangeCalendar, ScrollArea, Select, Separator, Slider, Switch, Tabs, Toggle, ToggleGroup, Toolbar, Tooltip, Portal, IsUsingKeyboard, computeCommandScore, } from "./bits/index.js";
|
|
2
2
|
export * from "./shared/index.js";
|
|
3
3
|
export * from "./types.js";
|
package/dist/internal/focus.js
CHANGED
|
@@ -33,6 +33,8 @@ export function focusWithoutScroll(element) {
|
|
|
33
33
|
export function focus(element, { select = false } = {}) {
|
|
34
34
|
if (!(element && element.focus))
|
|
35
35
|
return;
|
|
36
|
+
if (document.activeElement === element)
|
|
37
|
+
return;
|
|
36
38
|
const previouslyFocusedElement = document.activeElement;
|
|
37
39
|
// prevent scroll on focus
|
|
38
40
|
element.focus({ preventScroll: true });
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function commandScore(string: string, abbreviation: string, aliases?: string[]): number;
|