Skip to content

Commit 37440ee

Browse files
authored
fix: Shadow DOM support (#1515)
1 parent 324873f commit 37440ee

37 files changed

+416
-394
lines changed

.changeset/major-sloths-read.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"bits-ui": patch
3+
---
4+
5+
fix: Support Shadow DOM

packages/bits-ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"css.escape": "^1.5.1",
5656
"esm-env": "^1.1.2",
5757
"runed": "^0.28.0",
58-
"svelte-toolbelt": "^0.8.2",
58+
"svelte-toolbelt": "^0.9.1",
5959
"tabbable": "^6.2.0"
6060
},
6161
"peerDependencies": {

packages/bits-ui/src/lib/bits/avatar/avatar.svelte.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { untrack } from "svelte";
2-
import { type ReadableBox, type WritableBox, attachRef } from "svelte-toolbelt";
2+
import { DOMContext, type ReadableBox, type WritableBox, attachRef } from "svelte-toolbelt";
33
import type { HTMLImgAttributes } from "svelte/elements";
44
import { Context } from "runed";
55
import type { AvatarImageLoadingStatus } from "./types.js";
@@ -25,9 +25,11 @@ type AvatarImageSrc = string | null | undefined;
2525

2626
class AvatarRootState {
2727
readonly opts: AvatarRootStateProps;
28+
readonly domContext: DOMContext;
2829

2930
constructor(opts: AvatarRootStateProps) {
3031
this.opts = opts;
32+
this.domContext = new DOMContext(this.opts.ref);
3133
this.loadImage = this.loadImage.bind(this);
3234
}
3335

@@ -42,15 +44,15 @@ class AvatarRootState {
4244

4345
this.opts.loadingStatus.current = "loading";
4446
image.onload = () => {
45-
imageTimerId = window.setTimeout(() => {
47+
imageTimerId = this.domContext.setTimeout(() => {
4648
this.opts.loadingStatus.current = "loaded";
4749
}, this.opts.delayMs.current);
4850
};
4951
image.onerror = () => {
5052
this.opts.loadingStatus.current = "error";
5153
};
5254
return () => {
53-
window.clearTimeout(imageTimerId);
55+
this.domContext.clearTimeout(imageTimerId);
5456
};
5557
}
5658

packages/bits-ui/src/lib/bits/calendar/calendar.svelte.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {
66
isToday,
77
} from "@internationalized/date";
88
import { DEV } from "esm-env";
9-
import { untrack } from "svelte";
10-
import { attachRef } from "svelte-toolbelt";
9+
import { onMount, untrack } from "svelte";
10+
import { attachRef, DOMContext } from "svelte-toolbelt";
1111
import { Context, watch } from "runed";
1212
import type { RangeCalendarRootState } from "../range-calendar/range-calendar.svelte.js";
1313
import {
@@ -87,10 +87,12 @@ export class CalendarRootState {
8787
announcer: Announcer;
8888
formatter: Formatter;
8989
accessibleHeadingId = useId();
90+
domContext: DOMContext;
9091

9192
constructor(opts: CalendarRootStateProps) {
9293
this.opts = opts;
93-
this.announcer = getAnnouncer();
94+
this.domContext = new DOMContext(opts.ref);
95+
this.announcer = getAnnouncer(null);
9496
this.formatter = createFormatter(this.opts.locale.current);
9597

9698
this.setMonths = this.setMonths.bind(this);
@@ -110,6 +112,10 @@ export class CalendarRootState {
110112
this.onkeydown = this.onkeydown.bind(this);
111113
this.getBitsAttr = this.getBitsAttr.bind(this);
112114

115+
onMount(() => {
116+
this.announcer = getAnnouncer(this.domContext.getDocument());
117+
});
118+
113119
this.months = createMonths({
114120
dateObj: this.opts.placeholder.current,
115121
weekStartsOn: this.opts.weekStartsOn.current,
@@ -153,7 +159,7 @@ export class CalendarRootState {
153159
* changes.
154160
*/
155161
$effect(() => {
156-
const node = document.getElementById(this.accessibleHeadingId);
162+
const node = this.domContext.getElementById(this.accessibleHeadingId);
157163
if (!node) return;
158164
node.textContent = this.fullCalendarLabel;
159165
});

packages/bits-ui/src/lib/bits/date-field/date-field.svelte.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Updater } from "svelte/store";
22
import type { DateValue } from "@internationalized/date";
3-
import { type WritableBox, box, onDestroyEffect, attachRef } from "svelte-toolbelt";
3+
import { type WritableBox, box, onDestroyEffect, attachRef, DOMContext } from "svelte-toolbelt";
44
import { onMount, untrack } from "svelte";
55
import { Context, watch } from "runed";
66
import type { DateRangeFieldRootState } from "../date-range-field/date-range-field.svelte.js";
@@ -195,6 +195,7 @@ export class DateFieldRootState {
195195
dayPeriodNode = $state<HTMLElement | null>(null);
196196
rangeRoot: DateRangeFieldRootState | undefined = undefined;
197197
name = $state("");
198+
domContext: DOMContext = new DOMContext(() => null);
198199

199200
constructor(props: DateFieldRootStateProps, rangeRoot?: DateRangeFieldRootState) {
200201
this.rangeRoot = rangeRoot;
@@ -224,7 +225,7 @@ export class DateFieldRootState {
224225
this.formatter = createFormatter(this.locale.current);
225226
this.initialSegments = initializeSegmentValues(this.inferredGranularity);
226227
this.segmentValues = this.initialSegments;
227-
this.announcer = getAnnouncer();
228+
this.announcer = getAnnouncer(null);
228229

229230
this.getFieldNode = this.getFieldNode.bind(this);
230231
this.updateSegment = this.updateSegment.bind(this);
@@ -238,12 +239,12 @@ export class DateFieldRootState {
238239
});
239240

240241
onMount(() => {
241-
this.announcer = getAnnouncer();
242+
this.announcer = getAnnouncer(this.domContext.getDocument());
242243
});
243244

244245
onDestroyEffect(() => {
245246
if (rangeRoot) return;
246-
removeDescriptionElement(this.descriptionId);
247+
removeDescriptionElement(this.descriptionId, this.domContext.getDocument());
247248
});
248249

249250
$effect(() => {
@@ -256,7 +257,12 @@ export class DateFieldRootState {
256257
if (rangeRoot) return;
257258
if (this.value.current) {
258259
const descriptionId = untrack(() => this.descriptionId);
259-
setDescription(descriptionId, this.formatter, this.value.current);
260+
setDescription({
261+
id: descriptionId,
262+
formatter: this.formatter,
263+
value: this.value.current,
264+
doc: this.domContext.getDocument(),
265+
});
260266
}
261267
const placeholder = untrack(() => this.placeholder.current);
262268
if (this.value.current && placeholder !== this.value.current) {
@@ -702,10 +708,13 @@ type DateFieldInputStateProps = WithRefProps &
702708
export class DateFieldInputState {
703709
readonly opts: DateFieldInputStateProps;
704710
readonly root: DateFieldRootState;
711+
readonly domContext: DOMContext;
705712

706713
constructor(opts: DateFieldInputStateProps, root: DateFieldRootState) {
707714
this.opts = opts;
708715
this.root = root;
716+
this.domContext = new DOMContext(opts.ref);
717+
this.root.domContext = this.domContext;
709718

710719
$effect(() => {
711720
this.root.setName(this.opts.name.current);
@@ -714,7 +723,7 @@ export class DateFieldInputState {
714723

715724
#ariaDescribedBy = $derived.by(() => {
716725
if (!isBrowser) return undefined;
717-
const doesDescriptionExist = document.getElementById(this.root.descriptionId);
726+
const doesDescriptionExist = this.domContext.getElementById(this.root.descriptionId);
718727
if (!doesDescriptionExist) return undefined;
719728
return this.root.descriptionId;
720729
});

packages/bits-ui/src/lib/bits/date-range-field/date-range-field.svelte.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { DateValue } from "@internationalized/date";
2-
import { box, onDestroyEffect, attachRef } from "svelte-toolbelt";
2+
import { box, onDestroyEffect, attachRef, DOMContext } from "svelte-toolbelt";
33
import { Context, watch } from "runed";
44
import type { DateFieldRootState } from "../date-field/date-field.svelte.js";
55
import { DateFieldInputState, useDateFieldRoot } from "../date-field/date-field.svelte.js";
@@ -58,13 +58,15 @@ export class DateRangeFieldRootState {
5858
startValueComplete = $derived.by(() => this.opts.startValue.current !== undefined);
5959
endValueComplete = $derived.by(() => this.opts.endValue.current !== undefined);
6060
rangeComplete = $derived(this.startValueComplete && this.endValueComplete);
61+
domContext: DOMContext;
6162

6263
constructor(opts: DateRangeFieldRootStateProps) {
6364
this.opts = opts;
6465
this.formatter = createFormatter(this.opts.locale.current);
66+
this.domContext = new DOMContext(this.opts.ref);
6567

6668
onDestroyEffect(() => {
67-
removeDescriptionElement(this.descriptionId);
69+
removeDescriptionElement(this.descriptionId, this.domContext.getDocument());
6870
});
6971

7072
$effect(() => {

packages/bits-ui/src/lib/bits/link-preview/link-preview.svelte.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { afterSleep, onDestroyEffect, attachRef } from "svelte-toolbelt";
1+
import { afterSleep, onDestroyEffect, attachRef, DOMContext } from "svelte-toolbelt";
22
import { Context, watch } from "runed";
33
import { on } from "svelte/events";
44
import { getAriaExpanded, getDataOpenClosed } from "$lib/internal/attrs.js";
@@ -29,6 +29,7 @@ class LinkPreviewRootState {
2929
contentMounted = $state(false);
3030
triggerNode = $state<HTMLElement | null>(null);
3131
isOpening = false;
32+
domContext: DOMContext = new DOMContext(() => null);
3233

3334
constructor(opts: LinkPreviewRootStateProps) {
3435
this.opts = opts;
@@ -40,13 +41,15 @@ class LinkPreviewRootState {
4041
this.hasSelection = false;
4142
return;
4243
}
44+
if (!this.domContext) return;
4345

4446
const handlePointerUp = () => {
4547
this.containsSelection = false;
4648
this.isPointerDownOnContent = false;
4749

4850
afterSleep(1, () => {
49-
const isSelection = document.getSelection()?.toString() !== "";
51+
const isSelection =
52+
this.domContext.getDocument().getSelection()?.toString() !== "";
5053

5154
if (isSelection) {
5255
this.hasSelection = true;
@@ -56,7 +59,11 @@ class LinkPreviewRootState {
5659
});
5760
};
5861

59-
const unsubListener = on(document, "pointerup", handlePointerUp);
62+
const unsubListener = on(
63+
this.domContext.getDocument(),
64+
"pointerup",
65+
handlePointerUp
66+
);
6067

6168
if (!this.contentNode) return;
6269
const tabCandidates = getTabbableCandidates(this.contentNode);
@@ -76,7 +83,7 @@ class LinkPreviewRootState {
7683

7784
clearTimeout() {
7885
if (this.timeout) {
79-
window.clearTimeout(this.timeout);
86+
this.domContext.clearTimeout(this.timeout);
8087
this.timeout = null;
8188
}
8289
}
@@ -85,7 +92,7 @@ class LinkPreviewRootState {
8592
this.clearTimeout();
8693
if (this.opts.open.current) return;
8794
this.isOpening = true;
88-
this.timeout = window.setTimeout(() => {
95+
this.timeout = this.domContext.setTimeout(() => {
8996
if (this.isOpening) {
9097
this.opts.open.current = true;
9198
this.isOpening = false;
@@ -104,7 +111,7 @@ class LinkPreviewRootState {
104111
this.clearTimeout();
105112

106113
if (!this.isPointerDownOnContent && !this.hasSelection) {
107-
this.timeout = window.setTimeout(() => {
114+
this.timeout = this.domContext.setTimeout(() => {
108115
this.opts.open.current = false;
109116
}, this.opts.closeDelay.current);
110117
}
@@ -120,6 +127,7 @@ class LinkPreviewTriggerState {
120127
constructor(opts: LinkPreviewTriggerStateProps, root: LinkPreviewRootState) {
121128
this.opts = opts;
122129
this.root = root;
130+
this.root.domContext = new DOMContext(opts.ref);
123131
this.onpointerenter = this.onpointerenter.bind(this);
124132
this.onpointerleave = this.onpointerleave.bind(this);
125133
this.onfocus = this.onfocus.bind(this);
@@ -179,6 +187,7 @@ class LinkPreviewContentState {
179187
constructor(opts: LinkPreviewContentStateProps, root: LinkPreviewRootState) {
180188
this.opts = opts;
181189
this.root = root;
190+
this.root.domContext = new DOMContext(opts.ref);
182191
this.onpointerdown = this.onpointerdown.bind(this);
183192
this.onpointerenter = this.onpointerenter.bind(this);
184193
this.onfocusout = this.onfocusout.bind(this);

0 commit comments

Comments
 (0)