bits-ui 2.9.1 → 2.9.3
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/link-preview/components/link-preview-content-static.svelte +2 -0
- package/dist/bits/link-preview/components/link-preview-content.svelte +1 -0
- package/dist/bits/link-preview/link-preview.svelte.js +1 -1
- package/dist/internal/body-scroll-lock.svelte.js +116 -30
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import PopperLayer from "../../utilities/popper-layer/popper-layer.svelte";
|
|
7
7
|
import { getFloatingContentCSSVars } from "../../../internal/floating-svelte/floating-utils.svelte.js";
|
|
8
8
|
import PopperLayerForceMount from "../../utilities/popper-layer/popper-layer-force-mount.svelte";
|
|
9
|
+
import Mounted from "../../utilities/mounted.svelte";
|
|
9
10
|
import { noop } from "../../../internal/noop.js";
|
|
10
11
|
|
|
11
12
|
const uid = $props.id();
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
{@render children?.()}
|
|
59
60
|
</div>
|
|
60
61
|
{/if}
|
|
62
|
+
<Mounted bind:mounted={contentState.root.contentMounted} />
|
|
61
63
|
{/snippet}
|
|
62
64
|
</PopperLayerForceMount>
|
|
63
65
|
{:else if !forceMount}
|
|
@@ -1,32 +1,94 @@
|
|
|
1
1
|
import { SvelteMap } from "svelte/reactivity";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { afterTick, box, onDestroyEffect } from "svelte-toolbelt";
|
|
3
|
+
import { isIOS } from "./is.js";
|
|
4
4
|
import { addEventListener } from "./events.js";
|
|
5
5
|
import { useId } from "./use-id.js";
|
|
6
6
|
import { watch } from "runed";
|
|
7
7
|
import { SharedState } from "./shared-state.svelte.js";
|
|
8
|
+
import { BROWSER } from "esm-env";
|
|
9
|
+
/** A map of lock ids to their `locked` state. */
|
|
10
|
+
const lockMap = new SvelteMap();
|
|
11
|
+
let initialBodyStyle = $state(null);
|
|
12
|
+
let stopTouchMoveListener = null;
|
|
13
|
+
let cleanupTimeoutId = null;
|
|
14
|
+
const anyLocked = box.with(() => {
|
|
15
|
+
for (const value of lockMap.values()) {
|
|
16
|
+
if (value)
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* We track the time we scheduled the cleanup to prevent race conditions
|
|
23
|
+
* when multiple locks are created/destroyed in the same tick, ensuring
|
|
24
|
+
* only the last one to schedule the cleanup will run.
|
|
25
|
+
*
|
|
26
|
+
* reference: https://github.com/huntabyte/bits-ui/issues/1639
|
|
27
|
+
*/
|
|
28
|
+
let cleanupScheduledAt = null;
|
|
8
29
|
const bodyLockStackCount = new SharedState(() => {
|
|
9
|
-
const map = new SvelteMap();
|
|
10
|
-
const locked = $derived.by(() => {
|
|
11
|
-
for (const value of map.values()) {
|
|
12
|
-
if (value)
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
15
|
-
return false;
|
|
16
|
-
});
|
|
17
|
-
let initialBodyStyle = $state(null);
|
|
18
|
-
let stopTouchMoveListener = null;
|
|
19
30
|
function resetBodyStyle() {
|
|
20
|
-
if (!
|
|
31
|
+
if (!BROWSER)
|
|
21
32
|
return;
|
|
22
33
|
document.body.setAttribute("style", initialBodyStyle ?? "");
|
|
23
34
|
document.body.style.removeProperty("--scrollbar-width");
|
|
24
35
|
isIOS && stopTouchMoveListener?.();
|
|
36
|
+
// reset initialBodyStyle so next locker captures the correct styles
|
|
37
|
+
initialBodyStyle = null;
|
|
38
|
+
hasEverBeenLocked = false;
|
|
25
39
|
}
|
|
26
|
-
|
|
27
|
-
if (
|
|
40
|
+
function cancelPendingCleanup() {
|
|
41
|
+
if (cleanupTimeoutId === null)
|
|
28
42
|
return;
|
|
29
|
-
|
|
43
|
+
window.clearTimeout(cleanupTimeoutId);
|
|
44
|
+
cleanupTimeoutId = null;
|
|
45
|
+
}
|
|
46
|
+
function scheduleCleanupIfNoNewLocks(delay, callback) {
|
|
47
|
+
cancelPendingCleanup();
|
|
48
|
+
cleanupScheduledAt = Date.now();
|
|
49
|
+
const currentCleanupId = cleanupScheduledAt;
|
|
50
|
+
/**
|
|
51
|
+
* We schedule the cleanup to run after a delay to allow new locks to register
|
|
52
|
+
* that might have been added in the same tick as the current cleanup.
|
|
53
|
+
*
|
|
54
|
+
* If a new lock is added in the same tick, the cleanup will be cancelled and
|
|
55
|
+
* a new cleanup will be scheduled.
|
|
56
|
+
*
|
|
57
|
+
* This is to prevent the cleanup from running too early and resetting the body
|
|
58
|
+
* style before the new lock has had a chance to apply its styles.
|
|
59
|
+
*/
|
|
60
|
+
const cleanupFn = () => {
|
|
61
|
+
cleanupTimeoutId = null;
|
|
62
|
+
// check if this cleanup is still valid (no newer cleanups scheduled)
|
|
63
|
+
if (cleanupScheduledAt !== currentCleanupId)
|
|
64
|
+
return;
|
|
65
|
+
// ensure no new locks were added during the delay
|
|
66
|
+
if (!isAnyLocked(lockMap)) {
|
|
67
|
+
callback();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
if (delay === null) {
|
|
71
|
+
// use a small delay even when no restoreScrollDelay is set
|
|
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
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// track if we've ever applied lock styles in this session
|
|
80
|
+
let hasEverBeenLocked = false;
|
|
81
|
+
function ensureInitialStyleCaptured() {
|
|
82
|
+
if (!hasEverBeenLocked && initialBodyStyle === null) {
|
|
83
|
+
initialBodyStyle = document.body.getAttribute("style");
|
|
84
|
+
hasEverBeenLocked = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
watch(() => anyLocked.current, () => {
|
|
88
|
+
if (!anyLocked.current)
|
|
89
|
+
return;
|
|
90
|
+
// ensure we've captured the initial style before applying any lock styles
|
|
91
|
+
ensureInitialStyleCaptured();
|
|
30
92
|
const bodyStyle = getComputedStyle(document.body);
|
|
31
93
|
// TODO: account for RTL direction, etc.
|
|
32
94
|
const verticalScrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
@@ -42,6 +104,7 @@ const bodyLockStackCount = new SharedState(() => {
|
|
|
42
104
|
document.body.style.overflow = "hidden";
|
|
43
105
|
}
|
|
44
106
|
if (isIOS) {
|
|
107
|
+
// IOS devices are special and require a touchmove listener to prevent scrolling
|
|
45
108
|
stopTouchMoveListener = addEventListener(document, "touchmove", (e) => {
|
|
46
109
|
if (e.target !== document.documentElement)
|
|
47
110
|
return;
|
|
@@ -50,6 +113,14 @@ const bodyLockStackCount = new SharedState(() => {
|
|
|
50
113
|
e.preventDefault();
|
|
51
114
|
}, { passive: false });
|
|
52
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* We ensure pointer-events: none is applied _after_ DOM updates, so that any focus/
|
|
118
|
+
* interaction changes from opening overlays/menus complete _before_ we block pointer
|
|
119
|
+
* events.
|
|
120
|
+
*
|
|
121
|
+
* this avoids race conditions where pointer-events could be set too early and break
|
|
122
|
+
* focus/interaction.
|
|
123
|
+
*/
|
|
53
124
|
afterTick(() => {
|
|
54
125
|
document.body.style.pointerEvents = "none";
|
|
55
126
|
document.body.style.overflow = "hidden";
|
|
@@ -61,10 +132,13 @@ const bodyLockStackCount = new SharedState(() => {
|
|
|
61
132
|
};
|
|
62
133
|
});
|
|
63
134
|
return {
|
|
64
|
-
get
|
|
65
|
-
return
|
|
135
|
+
get lockMap() {
|
|
136
|
+
return lockMap;
|
|
66
137
|
},
|
|
67
138
|
resetBodyStyle,
|
|
139
|
+
scheduleCleanupIfNoNewLocks,
|
|
140
|
+
cancelPendingCleanup,
|
|
141
|
+
ensureInitialStyleCaptured,
|
|
68
142
|
};
|
|
69
143
|
});
|
|
70
144
|
export class BodyScrollLock {
|
|
@@ -79,21 +153,33 @@ export class BodyScrollLock {
|
|
|
79
153
|
this.#countState = bodyLockStackCount.get();
|
|
80
154
|
if (!this.#countState)
|
|
81
155
|
return;
|
|
82
|
-
|
|
83
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Since a new lock is being created, we cancel any pending cleanup to
|
|
158
|
+
* prevent the cleanup from running too early and resetting the body style
|
|
159
|
+
* before the new lock has had a chance to apply its styles.
|
|
160
|
+
*
|
|
161
|
+
* reference: https://github.com/huntabyte/bits-ui/issues/1639
|
|
162
|
+
*/
|
|
163
|
+
this.#countState.cancelPendingCleanup();
|
|
164
|
+
// capture initial style before this lock is registered
|
|
165
|
+
this.#countState.ensureInitialStyleCaptured();
|
|
166
|
+
this.#countState.lockMap.set(this.#id, this.#initialState ?? false);
|
|
167
|
+
this.locked = box.with(() => this.#countState.lockMap.get(this.#id) ?? false, (v) => this.#countState.lockMap.set(this.#id, v));
|
|
84
168
|
onDestroyEffect(() => {
|
|
85
|
-
this.#countState.
|
|
86
|
-
// if
|
|
87
|
-
if (isAnyLocked(this.#countState.
|
|
169
|
+
this.#countState.lockMap.delete(this.#id);
|
|
170
|
+
// if not the last lock, we don't need to do anything
|
|
171
|
+
if (isAnyLocked(this.#countState.lockMap))
|
|
88
172
|
return;
|
|
89
173
|
const restoreScrollDelay = this.#restoreScrollDelay();
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
174
|
+
/**
|
|
175
|
+
* We schedule the cleanup to run after a delay to handle same-tick
|
|
176
|
+
* destroy/create scenarios.
|
|
177
|
+
*
|
|
178
|
+
* reference: https://github.com/huntabyte/bits-ui/issues/1639
|
|
179
|
+
*/
|
|
180
|
+
this.#countState.scheduleCleanupIfNoNewLocks(restoreScrollDelay, () => {
|
|
181
|
+
this.#countState.resetBodyStyle();
|
|
182
|
+
});
|
|
97
183
|
});
|
|
98
184
|
}
|
|
99
185
|
}
|