bits-ui 1.0.0-next.92 → 1.0.0-next.94
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.js +2 -2
- package/dist/bits/navigation-menu/navigation-menu.svelte.d.ts +1 -0
- package/dist/bits/navigation-menu/navigation-menu.svelte.js +2 -0
- 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/internal/focus.js +2 -0
- package/package.json +1 -1
|
@@ -44,7 +44,7 @@ class CommandRootState {
|
|
|
44
44
|
// internal state that we mutate in batches and publish to the `state` at once
|
|
45
45
|
_commandState = $state(null);
|
|
46
46
|
#snapshot() {
|
|
47
|
-
return this._commandState;
|
|
47
|
+
return $state.snapshot(this._commandState);
|
|
48
48
|
}
|
|
49
49
|
#scheduleUpdate() {
|
|
50
50
|
if (this.#updateScheduled)
|
|
@@ -56,7 +56,7 @@ class CommandRootState {
|
|
|
56
56
|
const hasStateChanged = !Object.is(this.commandState, currentState);
|
|
57
57
|
if (hasStateChanged) {
|
|
58
58
|
this.commandState = currentState;
|
|
59
|
-
this.opts.onStateChange?.current?.(
|
|
59
|
+
this.opts.onStateChange?.current?.(currentState);
|
|
60
60
|
}
|
|
61
61
|
});
|
|
62
62
|
}
|
|
@@ -319,6 +319,7 @@ declare class NavigationMenuViewportState {
|
|
|
319
319
|
readonly "--bits-navigation-menu-viewport-width": string | undefined;
|
|
320
320
|
readonly "--bits-navigation-menu-viewport-height": string | undefined;
|
|
321
321
|
};
|
|
322
|
+
readonly "data-navigation-menu-viewport": "";
|
|
322
323
|
readonly onpointerenter: () => void;
|
|
323
324
|
readonly onpointerleave: () => void;
|
|
324
325
|
};
|
|
@@ -26,6 +26,7 @@ const NAVIGATION_MENU_LIST_ATTR = "data-navigation-menu-list";
|
|
|
26
26
|
const NAVIGATION_MENU_TRIGGER_ATTR = "data-navigation-menu-trigger";
|
|
27
27
|
const NAVIGATION_MENU_CONTENT_ATTR = "data-navigation-menu-content";
|
|
28
28
|
const NAVIGATION_MENU_LINK_ATTR = "data-navigation-menu-link";
|
|
29
|
+
const NAVIGATION_MENU_VIEWPORT_ATTR = "data-navigation-menu-viewport";
|
|
29
30
|
class NavigationMenuProviderState {
|
|
30
31
|
opts;
|
|
31
32
|
indicatorTrackRef = box(null);
|
|
@@ -704,6 +705,7 @@ class NavigationMenuViewportState {
|
|
|
704
705
|
"--bits-navigation-menu-viewport-width": this.viewportWidth,
|
|
705
706
|
"--bits-navigation-menu-viewport-height": this.viewportHeight,
|
|
706
707
|
},
|
|
708
|
+
[NAVIGATION_MENU_VIEWPORT_ATTR]: "",
|
|
707
709
|
onpointerenter: this.context.onContentEnter,
|
|
708
710
|
onpointerleave: this.context.onContentLeave,
|
|
709
711
|
}));
|
|
@@ -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/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 });
|