bits-ui 2.9.4 → 2.9.6
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 -5
- package/dist/bits/dialog/components/dialog-content.svelte +2 -6
- package/dist/bits/link-preview/components/link-preview.svelte +2 -0
- package/dist/bits/link-preview/link-preview.svelte.d.ts +1 -0
- package/dist/bits/link-preview/link-preview.svelte.js +1 -1
- package/dist/bits/popover/components/popover-content-static.svelte +2 -2
- package/dist/bits/popover/components/popover-content.svelte +2 -1
- package/dist/bits/popover/popover.svelte.d.ts +0 -3
- package/dist/bits/popover/popover.svelte.js +0 -8
- package/dist/bits/utilities/focus-scope/focus-scope-manager.d.ts +3 -0
- package/dist/bits/utilities/focus-scope/focus-scope-manager.js +15 -0
- package/dist/bits/utilities/focus-scope/focus-scope.svelte.js +12 -4
- package/dist/internal/body-scroll-lock.svelte.js +12 -13
- package/package.json +1 -1
|
@@ -57,11 +57,7 @@
|
|
|
57
57
|
present: contentState.root.opts.open.current,
|
|
58
58
|
open: contentState.root.opts.open.current,
|
|
59
59
|
})}
|
|
60
|
-
onCloseAutoFocus
|
|
61
|
-
onCloseAutoFocus(e);
|
|
62
|
-
if (e.defaultPrevented) return;
|
|
63
|
-
afterSleep(0, () => contentState.root.triggerNode?.focus());
|
|
64
|
-
}}
|
|
60
|
+
{onCloseAutoFocus}
|
|
65
61
|
onOpenAutoFocus={(e) => {
|
|
66
62
|
onOpenAutoFocus(e);
|
|
67
63
|
if (e.defaultPrevented) return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { box, mergeProps } from "svelte-toolbelt";
|
|
3
3
|
import { DialogContentState } from "../dialog.svelte.js";
|
|
4
4
|
import type { DialogContentProps } from "../types.js";
|
|
5
5
|
import DismissibleLayer from "../../utilities/dismissible-layer/dismissible-layer.svelte";
|
|
@@ -57,11 +57,7 @@
|
|
|
57
57
|
open: contentState.root.opts.open.current,
|
|
58
58
|
})}
|
|
59
59
|
{onOpenAutoFocus}
|
|
60
|
-
onCloseAutoFocus
|
|
61
|
-
onCloseAutoFocus(e);
|
|
62
|
-
if (e.defaultPrevented) return;
|
|
63
|
-
afterSleep(1, () => contentState.root.triggerNode?.focus());
|
|
64
|
-
}}
|
|
60
|
+
{onCloseAutoFocus}
|
|
65
61
|
>
|
|
66
62
|
{#snippet focusScope({ props: focusScopeProps })}
|
|
67
63
|
<EscapeLayer
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { FloatingLayer } from "../../utilities/floating-layer/index.js";
|
|
7
7
|
|
|
8
8
|
let {
|
|
9
|
+
disabled = false,
|
|
9
10
|
open = $bindable(false),
|
|
10
11
|
onOpenChange = noop,
|
|
11
12
|
onOpenChangeComplete = noop,
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
}: LinkPreviewRootProps = $props();
|
|
16
17
|
|
|
17
18
|
LinkPreviewRootState.create({
|
|
19
|
+
disabled: box.with(() => disabled),
|
|
18
20
|
open: box.with(
|
|
19
21
|
() => open,
|
|
20
22
|
(v) => {
|
|
@@ -3,6 +3,7 @@ import type { BitsFocusEvent, BitsPointerEvent, OnChangeFn, RefAttachment, WithR
|
|
|
3
3
|
interface LinkPreviewRootStateOpts extends WritableBoxedValues<{
|
|
4
4
|
open: boolean;
|
|
5
5
|
}>, ReadableBoxedValues<{
|
|
6
|
+
disabled: boolean;
|
|
6
7
|
openDelay: number;
|
|
7
8
|
closeDelay: number;
|
|
8
9
|
onOpenChangeComplete: OnChangeFn<boolean>;
|
|
@@ -76,7 +76,7 @@ export class LinkPreviewRootState {
|
|
|
76
76
|
}
|
|
77
77
|
handleOpen() {
|
|
78
78
|
this.clearTimeout();
|
|
79
|
-
if (this.opts.open.current)
|
|
79
|
+
if (this.opts.open.current || this.opts.disabled.current)
|
|
80
80
|
return;
|
|
81
81
|
this.isOpening = true;
|
|
82
82
|
this.timeout = this.domContext.setTimeout(() => {
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
onInteractOutside = noop,
|
|
22
22
|
trapFocus = true,
|
|
23
23
|
preventScroll = false,
|
|
24
|
-
|
|
25
24
|
...restProps
|
|
26
25
|
}: PopoverContentStaticProps = $props();
|
|
27
26
|
|
|
@@ -33,7 +32,6 @@
|
|
|
33
32
|
),
|
|
34
33
|
onInteractOutside: box.with(() => onInteractOutside),
|
|
35
34
|
onEscapeKeydown: box.with(() => onEscapeKeydown),
|
|
36
|
-
onCloseAutoFocus: box.with(() => onCloseAutoFocus),
|
|
37
35
|
customAnchor: box.with(() => null),
|
|
38
36
|
});
|
|
39
37
|
|
|
@@ -52,6 +50,7 @@
|
|
|
52
50
|
{preventScroll}
|
|
53
51
|
loop
|
|
54
52
|
forceMount={true}
|
|
53
|
+
{onCloseAutoFocus}
|
|
55
54
|
>
|
|
56
55
|
{#snippet popper({ props })}
|
|
57
56
|
{@const finalProps = mergeProps(props, {
|
|
@@ -78,6 +77,7 @@
|
|
|
78
77
|
{preventScroll}
|
|
79
78
|
loop
|
|
80
79
|
forceMount={false}
|
|
80
|
+
{onCloseAutoFocus}
|
|
81
81
|
>
|
|
82
82
|
{#snippet popper({ props })}
|
|
83
83
|
{@const finalProps = mergeProps(props, {
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
),
|
|
34
34
|
onInteractOutside: box.with(() => onInteractOutside),
|
|
35
35
|
onEscapeKeydown: box.with(() => onEscapeKeydown),
|
|
36
|
-
onCloseAutoFocus: box.with(() => onCloseAutoFocus),
|
|
37
36
|
customAnchor: box.with(() => customAnchor),
|
|
38
37
|
});
|
|
39
38
|
|
|
@@ -52,6 +51,7 @@
|
|
|
52
51
|
loop
|
|
53
52
|
forceMount={true}
|
|
54
53
|
{customAnchor}
|
|
54
|
+
{onCloseAutoFocus}
|
|
55
55
|
>
|
|
56
56
|
{#snippet popper({ props, wrapperProps })}
|
|
57
57
|
{@const finalProps = mergeProps(props, {
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
loop
|
|
81
81
|
forceMount={false}
|
|
82
82
|
{customAnchor}
|
|
83
|
+
{onCloseAutoFocus}
|
|
83
84
|
>
|
|
84
85
|
{#snippet popper({ props, wrapperProps })}
|
|
85
86
|
{@const finalProps = mergeProps(props, {
|
|
@@ -43,7 +43,6 @@ export declare class PopoverTriggerState {
|
|
|
43
43
|
interface PopoverContentStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
|
44
44
|
onInteractOutside: (e: PointerEvent) => void;
|
|
45
45
|
onEscapeKeydown: (e: KeyboardEvent) => void;
|
|
46
|
-
onCloseAutoFocus: (e: Event) => void;
|
|
47
46
|
customAnchor: string | HTMLElement | null | Measurable;
|
|
48
47
|
}> {
|
|
49
48
|
}
|
|
@@ -55,7 +54,6 @@ export declare class PopoverContentState {
|
|
|
55
54
|
constructor(opts: PopoverContentStateOpts, root: PopoverRootState);
|
|
56
55
|
onInteractOutside: (e: PointerEvent) => void;
|
|
57
56
|
onEscapeKeydown: (e: KeyboardEvent) => void;
|
|
58
|
-
onCloseAutoFocus: (e: Event) => void;
|
|
59
57
|
readonly snippetProps: {
|
|
60
58
|
open: boolean;
|
|
61
59
|
};
|
|
@@ -70,7 +68,6 @@ export declare class PopoverContentState {
|
|
|
70
68
|
readonly popperProps: {
|
|
71
69
|
onInteractOutside: (e: PointerEvent) => void;
|
|
72
70
|
onEscapeKeydown: (e: KeyboardEvent) => void;
|
|
73
|
-
onCloseAutoFocus: (e: Event) => void;
|
|
74
71
|
};
|
|
75
72
|
}
|
|
76
73
|
interface PopoverCloseStateOpts extends WithRefOpts {
|
|
@@ -124,13 +124,6 @@ export class PopoverContentState {
|
|
|
124
124
|
return;
|
|
125
125
|
this.root.handleClose();
|
|
126
126
|
};
|
|
127
|
-
onCloseAutoFocus = (e) => {
|
|
128
|
-
this.opts.onCloseAutoFocus.current?.(e);
|
|
129
|
-
if (e.defaultPrevented)
|
|
130
|
-
return;
|
|
131
|
-
e.preventDefault();
|
|
132
|
-
this.root.triggerNode?.focus();
|
|
133
|
-
};
|
|
134
127
|
snippetProps = $derived.by(() => ({ open: this.root.opts.open.current }));
|
|
135
128
|
props = $derived.by(() => ({
|
|
136
129
|
id: this.opts.id.current,
|
|
@@ -145,7 +138,6 @@ export class PopoverContentState {
|
|
|
145
138
|
popperProps = {
|
|
146
139
|
onInteractOutside: this.onInteractOutside,
|
|
147
140
|
onEscapeKeydown: this.onEscapeKeydown,
|
|
148
|
-
onCloseAutoFocus: this.onCloseAutoFocus,
|
|
149
141
|
};
|
|
150
142
|
}
|
|
151
143
|
export class PopoverCloseState {
|
|
@@ -9,4 +9,7 @@ export declare class FocusScopeManager {
|
|
|
9
9
|
setFocusMemory(scope: FocusScope, element: HTMLElement): void;
|
|
10
10
|
getFocusMemory(scope: FocusScope): HTMLElement | undefined;
|
|
11
11
|
isActiveScope(scope: FocusScope): boolean;
|
|
12
|
+
setPreFocusMemory(scope: FocusScope, element: HTMLElement): void;
|
|
13
|
+
getPreFocusMemory(scope: FocusScope): HTMLElement | undefined;
|
|
14
|
+
clearPreFocusMemory(scope: FocusScope): void;
|
|
12
15
|
}
|
|
@@ -4,6 +4,7 @@ export class FocusScopeManager {
|
|
|
4
4
|
static instance;
|
|
5
5
|
#scopeStack = box([]);
|
|
6
6
|
#focusHistory = new WeakMap();
|
|
7
|
+
#preFocusHistory = new WeakMap();
|
|
7
8
|
static getInstance() {
|
|
8
9
|
if (!this.instance) {
|
|
9
10
|
this.instance = new FocusScopeManager();
|
|
@@ -15,6 +16,11 @@ export class FocusScopeManager {
|
|
|
15
16
|
if (current && current !== scope) {
|
|
16
17
|
current.pause();
|
|
17
18
|
}
|
|
19
|
+
// capture the currently focused element before this scope becomes active
|
|
20
|
+
const activeElement = document.activeElement;
|
|
21
|
+
if (activeElement && activeElement !== document.body) {
|
|
22
|
+
this.#preFocusHistory.set(scope, activeElement);
|
|
23
|
+
}
|
|
18
24
|
this.#scopeStack.current = this.#scopeStack.current.filter((s) => s !== scope);
|
|
19
25
|
this.#scopeStack.current.unshift(scope);
|
|
20
26
|
}
|
|
@@ -37,4 +43,13 @@ export class FocusScopeManager {
|
|
|
37
43
|
isActiveScope(scope) {
|
|
38
44
|
return this.getActive() === scope;
|
|
39
45
|
}
|
|
46
|
+
setPreFocusMemory(scope, element) {
|
|
47
|
+
this.#preFocusHistory.set(scope, element);
|
|
48
|
+
}
|
|
49
|
+
getPreFocusMemory(scope) {
|
|
50
|
+
return this.#preFocusHistory.get(scope);
|
|
51
|
+
}
|
|
52
|
+
clearPreFocusMemory(scope) {
|
|
53
|
+
this.#preFocusHistory.delete(scope);
|
|
54
|
+
}
|
|
40
55
|
}
|
|
@@ -43,6 +43,7 @@ export class FocusScope {
|
|
|
43
43
|
// handle close auto-focus
|
|
44
44
|
this.#handleCloseAutoFocus();
|
|
45
45
|
this.#manager.unregister(this);
|
|
46
|
+
this.#manager.clearPreFocusMemory(this);
|
|
46
47
|
this.#container = null;
|
|
47
48
|
}
|
|
48
49
|
#handleOpenAutoFocus() {
|
|
@@ -75,10 +76,17 @@ export class FocusScope {
|
|
|
75
76
|
});
|
|
76
77
|
this.#opts.onCloseAutoFocus.current?.(event);
|
|
77
78
|
if (!event.defaultPrevented) {
|
|
78
|
-
// return focus to
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
81
|
-
|
|
79
|
+
// return focus to the element that was focused before this scope opened
|
|
80
|
+
const preFocusedElement = this.#manager.getPreFocusMemory(this);
|
|
81
|
+
if (preFocusedElement && document.contains(preFocusedElement)) {
|
|
82
|
+
// ensure the element is still focusable and in the document
|
|
83
|
+
try {
|
|
84
|
+
preFocusedElement.focus();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// fallback if focus fails
|
|
88
|
+
document.body.focus();
|
|
89
|
+
}
|
|
82
90
|
}
|
|
83
91
|
}
|
|
84
92
|
}
|
|
@@ -11,6 +11,7 @@ const lockMap = new SvelteMap();
|
|
|
11
11
|
let initialBodyStyle = $state(null);
|
|
12
12
|
let stopTouchMoveListener = null;
|
|
13
13
|
let cleanupTimeoutId = null;
|
|
14
|
+
let isInCleanupTransition = false;
|
|
14
15
|
const anyLocked = box.with(() => {
|
|
15
16
|
for (const value of lockMap.values()) {
|
|
16
17
|
if (value)
|
|
@@ -35,7 +36,6 @@ const bodyLockStackCount = new SharedState(() => {
|
|
|
35
36
|
isIOS && stopTouchMoveListener?.();
|
|
36
37
|
// reset initialBodyStyle so next locker captures the correct styles
|
|
37
38
|
initialBodyStyle = null;
|
|
38
|
-
hasEverBeenLocked = false;
|
|
39
39
|
}
|
|
40
40
|
function cancelPendingCleanup() {
|
|
41
41
|
if (cleanupTimeoutId === null)
|
|
@@ -45,6 +45,7 @@ const bodyLockStackCount = new SharedState(() => {
|
|
|
45
45
|
}
|
|
46
46
|
function scheduleCleanupIfNoNewLocks(delay, callback) {
|
|
47
47
|
cancelPendingCleanup();
|
|
48
|
+
isInCleanupTransition = true;
|
|
48
49
|
cleanupScheduledAt = Date.now();
|
|
49
50
|
const currentCleanupId = cleanupScheduledAt;
|
|
50
51
|
/**
|
|
@@ -64,24 +65,20 @@ const bodyLockStackCount = new SharedState(() => {
|
|
|
64
65
|
return;
|
|
65
66
|
// ensure no new locks were added during the delay
|
|
66
67
|
if (!isAnyLocked(lockMap)) {
|
|
68
|
+
isInCleanupTransition = false;
|
|
67
69
|
callback();
|
|
68
70
|
}
|
|
71
|
+
else {
|
|
72
|
+
isInCleanupTransition = false;
|
|
73
|
+
}
|
|
69
74
|
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// to handle same-tick destroy/create scenarios (~1 frame)
|
|
73
|
-
cleanupTimeoutId = window.setTimeout(cleanupFn, 16);
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
cleanupTimeoutId = window.setTimeout(cleanupFn, delay);
|
|
77
|
-
}
|
|
75
|
+
const actualDelay = delay === null ? 24 : delay;
|
|
76
|
+
cleanupTimeoutId = window.setTimeout(cleanupFn, actualDelay);
|
|
78
77
|
}
|
|
79
|
-
// track if we've ever applied lock styles in this session
|
|
80
|
-
let hasEverBeenLocked = false;
|
|
81
78
|
function ensureInitialStyleCaptured() {
|
|
82
|
-
|
|
79
|
+
// only capture initial style once, when no locks exist and no cleanup is in progress
|
|
80
|
+
if (initialBodyStyle === null && lockMap.size === 0 && !isInCleanupTransition) {
|
|
83
81
|
initialBodyStyle = document.body.getAttribute("style");
|
|
84
|
-
hasEverBeenLocked = true;
|
|
85
82
|
}
|
|
86
83
|
}
|
|
87
84
|
watch(() => anyLocked.current, () => {
|
|
@@ -89,6 +86,8 @@ const bodyLockStackCount = new SharedState(() => {
|
|
|
89
86
|
return;
|
|
90
87
|
// ensure we've captured the initial style before applying any lock styles
|
|
91
88
|
ensureInitialStyleCaptured();
|
|
89
|
+
// if we're applying lock styles, we're no longer in a cleanup transition
|
|
90
|
+
isInCleanupTransition = false;
|
|
92
91
|
const bodyStyle = getComputedStyle(document.body);
|
|
93
92
|
// TODO: account for RTL direction, etc.
|
|
94
93
|
const verticalScrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|