bits-ui 2.16.5 → 2.17.1
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/accordion/accordion.svelte.d.ts +4 -2
- package/dist/bits/accordion/accordion.svelte.js +2 -1
- package/dist/bits/collapsible/collapsible.svelte.d.ts +3 -1
- package/dist/bits/collapsible/collapsible.svelte.js +2 -1
- package/dist/bits/context-menu/components/context-menu.svelte +3 -1
- package/dist/bits/dialog/dialog.svelte.d.ts +4 -0
- package/dist/bits/dialog/dialog.svelte.js +3 -1
- package/dist/bits/link-preview/link-preview.svelte.d.ts +5 -3
- package/dist/bits/link-preview/link-preview.svelte.js +2 -1
- package/dist/bits/menu/components/menu-sub-content-static.svelte +12 -3
- package/dist/bits/menu/components/menu-sub-content.svelte +12 -3
- package/dist/bits/menu/components/menu-sub-trigger.svelte +1 -1
- package/dist/bits/menu/components/menu.svelte +5 -0
- package/dist/bits/menu/components/menu.svelte.d.ts +1 -0
- package/dist/bits/menu/menu.svelte.d.ts +8 -4
- package/dist/bits/menu/menu.svelte.js +642 -13
- package/dist/bits/menubar/components/menubar-menu.svelte +3 -0
- package/dist/bits/menubar/menubar.svelte.d.ts +2 -0
- package/dist/bits/menubar/menubar.svelte.js +22 -2
- package/dist/bits/navigation-menu/components/navigation-menu-content.svelte +7 -2
- package/dist/bits/navigation-menu/components/navigation-menu-indicator.svelte +9 -2
- package/dist/bits/navigation-menu/components/navigation-menu-viewport.svelte +5 -3
- package/dist/bits/popover/popover.svelte.d.ts +7 -3
- package/dist/bits/popover/popover.svelte.js +3 -1
- package/dist/bits/select/select.svelte.d.ts +6 -4
- package/dist/bits/select/select.svelte.js +2 -1
- package/dist/bits/tooltip/tooltip.svelte.d.ts +5 -3
- package/dist/bits/tooltip/tooltip.svelte.js +2 -1
- package/dist/bits/utilities/floating-layer/use-floating-layer.svelte.d.ts +5 -5
- package/dist/bits/utilities/presence-layer/presence-layer.svelte +5 -1
- package/dist/bits/utilities/presence-layer/presence.svelte.d.ts +3 -38
- package/dist/bits/utilities/presence-layer/presence.svelte.js +49 -146
- package/dist/bits/utilities/presence-layer/types.d.ts +7 -3
- package/dist/internal/animations-complete.js +64 -9
- package/dist/internal/attrs.d.ts +5 -0
- package/dist/internal/attrs.js +8 -2
- package/dist/internal/presence-manager.svelte.d.ts +4 -1
- package/dist/internal/presence-manager.svelte.js +42 -1
- package/package.json +1 -1
|
@@ -18,6 +18,8 @@ export declare class MenubarRootState {
|
|
|
18
18
|
readonly attachment: RefAttachment;
|
|
19
19
|
wasOpenedByKeyboard: boolean;
|
|
20
20
|
triggerIds: string[];
|
|
21
|
+
/** Outgoing menu id when swapping to another top-level menu... skip exit animation wait only then */
|
|
22
|
+
skipExitAnimationForMenuValue: string | null;
|
|
21
23
|
valueToChangeHandler: Map<string, ReadableBox<OnChangeFn<boolean>>>;
|
|
22
24
|
constructor(opts: MenubarRootStateOpts);
|
|
23
25
|
/**
|
|
@@ -21,6 +21,8 @@ export class MenubarRootState {
|
|
|
21
21
|
attachment;
|
|
22
22
|
wasOpenedByKeyboard = $state(false);
|
|
23
23
|
triggerIds = $state([]);
|
|
24
|
+
/** Outgoing menu id when swapping to another top-level menu... skip exit animation wait only then */
|
|
25
|
+
skipExitAnimationForMenuValue = $state(null);
|
|
24
26
|
valueToChangeHandler = new Map();
|
|
25
27
|
constructor(opts) {
|
|
26
28
|
this.opts = opts;
|
|
@@ -55,6 +57,10 @@ export class MenubarRootState {
|
|
|
55
57
|
};
|
|
56
58
|
updateValue = (value) => {
|
|
57
59
|
const currValue = this.opts.value.current;
|
|
60
|
+
const switchingMenus = Boolean(currValue && value && currValue !== value);
|
|
61
|
+
if (switchingMenus) {
|
|
62
|
+
this.skipExitAnimationForMenuValue = currValue;
|
|
63
|
+
}
|
|
58
64
|
const currHandler = this.valueToChangeHandler.get(currValue)?.current;
|
|
59
65
|
const nextHandler = this.valueToChangeHandler.get(value)?.current;
|
|
60
66
|
this.opts.value.current = value;
|
|
@@ -64,6 +70,11 @@ export class MenubarRootState {
|
|
|
64
70
|
if (nextHandler) {
|
|
65
71
|
nextHandler(true);
|
|
66
72
|
}
|
|
73
|
+
if (switchingMenus) {
|
|
74
|
+
afterTick(() => {
|
|
75
|
+
this.skipExitAnimationForMenuValue = null;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
67
78
|
};
|
|
68
79
|
getTriggers = () => {
|
|
69
80
|
const node = this.opts.ref.current;
|
|
@@ -271,13 +282,22 @@ export class MenubarContentState {
|
|
|
271
282
|
if (isPrevKey)
|
|
272
283
|
candidates.reverse();
|
|
273
284
|
const candidateValues = candidates.map(({ value }) => value);
|
|
274
|
-
|
|
285
|
+
// use the root's open menu id — during rapid switching, stale content can still be
|
|
286
|
+
// focused while another menu is already open; per-menu value would navigate from the wrong index
|
|
287
|
+
const openMenuValue = this.root.opts.value.current;
|
|
288
|
+
if (!openMenuValue)
|
|
289
|
+
return;
|
|
290
|
+
const currentIndex = candidateValues.indexOf(openMenuValue);
|
|
291
|
+
if (currentIndex === -1)
|
|
292
|
+
return;
|
|
275
293
|
candidates = this.root.opts.loop.current
|
|
276
294
|
? wrapArray(candidates, currentIndex + 1)
|
|
277
295
|
: candidates.slice(currentIndex + 1);
|
|
278
296
|
const [nextValue] = candidates;
|
|
279
|
-
if (nextValue)
|
|
297
|
+
if (nextValue) {
|
|
280
298
|
this.menu.root.onMenuOpen(nextValue.value, nextValue.triggerId);
|
|
299
|
+
e.preventDefault();
|
|
300
|
+
}
|
|
281
301
|
};
|
|
282
302
|
props = $derived.by(() => ({
|
|
283
303
|
id: this.opts.id.current,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { NavigationMenuContentState } from "../navigation-menu.svelte.js";
|
|
4
4
|
import NavigationMenuContentImpl from "./navigation-menu-content-impl.svelte";
|
|
5
5
|
import { createId } from "../../../internal/create-id.js";
|
|
6
|
+
import { getDataTransitionAttrs } from "../../../internal/attrs.js";
|
|
6
7
|
import type { NavigationMenuContentProps } from "../../../types.js";
|
|
7
8
|
import Portal from "../../utilities/portal/portal.svelte";
|
|
8
9
|
import PresenceLayer from "../../utilities/presence-layer/presence-layer.svelte";
|
|
@@ -38,8 +39,12 @@
|
|
|
38
39
|
open={forceMount || contentState.open || contentState.isLastActiveValue}
|
|
39
40
|
ref={contentState.opts.ref}
|
|
40
41
|
>
|
|
41
|
-
{#snippet presence()}
|
|
42
|
-
<NavigationMenuContentImpl
|
|
42
|
+
{#snippet presence({ transitionStatus })}
|
|
43
|
+
<NavigationMenuContentImpl
|
|
44
|
+
{...mergeProps(mergedProps, getDataTransitionAttrs(transitionStatus))}
|
|
45
|
+
{children}
|
|
46
|
+
{child}
|
|
47
|
+
/>
|
|
43
48
|
<Mounted bind:mounted={contentState.mounted} />
|
|
44
49
|
{/snippet}
|
|
45
50
|
</PresenceLayer>
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { NavigationMenuIndicatorState } from "../navigation-menu.svelte.js";
|
|
5
5
|
import NavigationMenuIndicatorImpl from "./navigation-menu-indicator-impl.svelte";
|
|
6
6
|
import { createId } from "../../../internal/create-id.js";
|
|
7
|
+
import { getDataTransitionAttrs } from "../../../internal/attrs.js";
|
|
7
8
|
import PresenceLayer from "../../utilities/presence-layer/presence-layer.svelte";
|
|
8
9
|
import Portal from "../../utilities/portal/portal.svelte";
|
|
9
10
|
|
|
@@ -25,8 +26,14 @@
|
|
|
25
26
|
{#if indicatorState.context.indicatorTrackRef.current}
|
|
26
27
|
<Portal to={indicatorState.context.indicatorTrackRef.current}>
|
|
27
28
|
<PresenceLayer open={forceMount || indicatorState.isVisible} ref={boxWith(() => ref)}>
|
|
28
|
-
{#snippet presence()}
|
|
29
|
-
<NavigationMenuIndicatorImpl
|
|
29
|
+
{#snippet presence({ transitionStatus })}
|
|
30
|
+
<NavigationMenuIndicatorImpl
|
|
31
|
+
{...mergeProps(mergedProps, getDataTransitionAttrs(transitionStatus))}
|
|
32
|
+
{children}
|
|
33
|
+
{child}
|
|
34
|
+
{id}
|
|
35
|
+
bind:ref
|
|
36
|
+
/>
|
|
30
37
|
{/snippet}
|
|
31
38
|
</PresenceLayer>
|
|
32
39
|
</Portal>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { NavigationMenuViewportProps } from "../types.js";
|
|
3
3
|
import { NavigationMenuViewportState } from "../navigation-menu.svelte.js";
|
|
4
4
|
import { createId } from "../../../internal/create-id.js";
|
|
5
|
+
import { getDataTransitionAttrs } from "../../../internal/attrs.js";
|
|
5
6
|
import PresenceLayer from "../../utilities/presence-layer/presence-layer.svelte";
|
|
6
7
|
import { boxWith, mergeProps } from "svelte-toolbelt";
|
|
7
8
|
import { Mounted } from "../../utilities/index.js";
|
|
@@ -29,11 +30,12 @@
|
|
|
29
30
|
</script>
|
|
30
31
|
|
|
31
32
|
<PresenceLayer open={forceMount || viewportState.open} ref={viewportState.opts.ref}>
|
|
32
|
-
{#snippet presence()}
|
|
33
|
+
{#snippet presence({ transitionStatus })}
|
|
34
|
+
{@const presenceProps = getDataTransitionAttrs(transitionStatus)}
|
|
33
35
|
{#if child}
|
|
34
|
-
{@render child({ props: mergedProps })}
|
|
36
|
+
{@render child({ props: mergeProps(mergedProps, presenceProps) })}
|
|
35
37
|
{:else}
|
|
36
|
-
<div {...mergedProps}>
|
|
38
|
+
<div {...mergeProps(mergedProps, presenceProps)}>
|
|
37
39
|
{@render children?.()}
|
|
38
40
|
</div>
|
|
39
41
|
{/if}
|
|
@@ -87,9 +87,6 @@ export declare class PopoverContentState {
|
|
|
87
87
|
open: boolean;
|
|
88
88
|
};
|
|
89
89
|
readonly props: {
|
|
90
|
-
readonly id: string;
|
|
91
|
-
readonly tabindex: -1;
|
|
92
|
-
readonly "data-state": "open" | "closed";
|
|
93
90
|
readonly style: {
|
|
94
91
|
readonly pointerEvents: "auto";
|
|
95
92
|
readonly contain: "layout style";
|
|
@@ -98,6 +95,11 @@ export declare class PopoverContentState {
|
|
|
98
95
|
readonly onfocusin: (e: BitsFocusEvent) => void;
|
|
99
96
|
readonly onpointerenter: (e: BitsPointerEvent) => void;
|
|
100
97
|
readonly onpointerleave: (e: BitsPointerEvent) => void;
|
|
98
|
+
readonly "data-starting-style"?: "";
|
|
99
|
+
readonly "data-ending-style"?: "";
|
|
100
|
+
readonly id: string;
|
|
101
|
+
readonly tabindex: -1;
|
|
102
|
+
readonly "data-state": "open" | "closed";
|
|
101
103
|
};
|
|
102
104
|
readonly popperProps: {
|
|
103
105
|
onInteractOutside: (e: PointerEvent) => void;
|
|
@@ -134,6 +136,8 @@ export declare class PopoverOverlayState {
|
|
|
134
136
|
open: boolean;
|
|
135
137
|
};
|
|
136
138
|
readonly props: {
|
|
139
|
+
readonly "data-starting-style"?: "";
|
|
140
|
+
readonly "data-ending-style"?: "";
|
|
137
141
|
readonly id: string;
|
|
138
142
|
readonly style: {
|
|
139
143
|
readonly pointerEvents: "auto";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { attachRef, boxWith, DOMContext, } from "svelte-toolbelt";
|
|
2
2
|
import { Context, watch } from "runed";
|
|
3
3
|
import { kbd } from "../../internal/kbd.js";
|
|
4
|
-
import { createBitsAttrs, boolToStr, getDataOpenClosed } from "../../internal/attrs.js";
|
|
4
|
+
import { createBitsAttrs, boolToStr, getDataOpenClosed, getDataTransitionAttrs, } from "../../internal/attrs.js";
|
|
5
5
|
import { isElement, isTouch } from "../../internal/is.js";
|
|
6
6
|
import { PresenceManager } from "../../internal/presence-manager.svelte.js";
|
|
7
7
|
import { SafePolygon } from "../../internal/safe-polygon.svelte.js";
|
|
@@ -325,6 +325,7 @@ export class PopoverContentState {
|
|
|
325
325
|
id: this.opts.id.current,
|
|
326
326
|
tabindex: -1,
|
|
327
327
|
"data-state": getDataOpenClosed(this.root.opts.open.current),
|
|
328
|
+
...getDataTransitionAttrs(this.root.contentPresence.transitionStatus),
|
|
328
329
|
[popoverAttrs.content]: "",
|
|
329
330
|
style: {
|
|
330
331
|
pointerEvents: "auto",
|
|
@@ -397,6 +398,7 @@ export class PopoverOverlayState {
|
|
|
397
398
|
pointerEvents: "auto",
|
|
398
399
|
},
|
|
399
400
|
"data-state": getDataOpenClosed(this.root.opts.open.current),
|
|
401
|
+
...getDataTransitionAttrs(this.root.overlayPresence.transitionStatus),
|
|
400
402
|
...this.attachment,
|
|
401
403
|
}));
|
|
402
404
|
}
|
|
@@ -213,10 +213,6 @@ export declare class SelectContentState {
|
|
|
213
213
|
open: boolean;
|
|
214
214
|
};
|
|
215
215
|
readonly props: {
|
|
216
|
-
readonly id: string;
|
|
217
|
-
readonly role: "listbox";
|
|
218
|
-
readonly "aria-multiselectable": "true" | undefined;
|
|
219
|
-
readonly "data-state": "open" | "closed";
|
|
220
216
|
readonly style: {
|
|
221
217
|
readonly display: "flex";
|
|
222
218
|
readonly flexDirection: "column";
|
|
@@ -225,6 +221,12 @@ export declare class SelectContentState {
|
|
|
225
221
|
readonly pointerEvents: "auto";
|
|
226
222
|
};
|
|
227
223
|
readonly onpointermove: (_: BitsPointerEvent) => void;
|
|
224
|
+
readonly "data-starting-style"?: "";
|
|
225
|
+
readonly "data-ending-style"?: "";
|
|
226
|
+
readonly id: string;
|
|
227
|
+
readonly role: "listbox";
|
|
228
|
+
readonly "aria-multiselectable": "true" | undefined;
|
|
229
|
+
readonly "data-state": "open" | "closed";
|
|
228
230
|
};
|
|
229
231
|
readonly popperProps: {
|
|
230
232
|
onInteractOutside: (e: PointerEvent) => void;
|
|
@@ -2,7 +2,7 @@ import { Context, Previous, watch } from "runed";
|
|
|
2
2
|
import { afterSleep, afterTick, onDestroyEffect, attachRef, DOMContext, boxWith, } from "svelte-toolbelt";
|
|
3
3
|
import { on } from "svelte/events";
|
|
4
4
|
import { backward, forward, next, prev } from "../../internal/arrays.js";
|
|
5
|
-
import { boolToStr, boolToStrTrueOrUndef, boolToEmptyStrOrUndef, getDataOpenClosed, boolToTrueOrUndef, } from "../../internal/attrs.js";
|
|
5
|
+
import { boolToStr, boolToStrTrueOrUndef, boolToEmptyStrOrUndef, getDataOpenClosed, boolToTrueOrUndef, getDataTransitionAttrs, } from "../../internal/attrs.js";
|
|
6
6
|
import { kbd } from "../../internal/kbd.js";
|
|
7
7
|
import { noop } from "../../internal/noop.js";
|
|
8
8
|
import { isIOS } from "../../internal/is.js";
|
|
@@ -772,6 +772,7 @@ export class SelectContentState {
|
|
|
772
772
|
role: "listbox",
|
|
773
773
|
"aria-multiselectable": this.root.isMulti ? "true" : undefined,
|
|
774
774
|
"data-state": getDataOpenClosed(this.root.opts.open.current),
|
|
775
|
+
...getDataTransitionAttrs(this.root.contentPresence.transitionStatus),
|
|
775
776
|
[this.root.getBitsAttr("content")]: "",
|
|
776
777
|
style: {
|
|
777
778
|
display: "flex",
|
|
@@ -153,12 +153,14 @@ export declare class TooltipContentState {
|
|
|
153
153
|
open: boolean;
|
|
154
154
|
};
|
|
155
155
|
readonly props: {
|
|
156
|
-
readonly id: string;
|
|
157
|
-
readonly "data-state": "closed" | "delayed-open" | "instant-open";
|
|
158
|
-
readonly "data-disabled": "" | undefined;
|
|
159
156
|
readonly style: {
|
|
160
157
|
readonly outline: "none";
|
|
161
158
|
};
|
|
159
|
+
readonly "data-starting-style"?: "";
|
|
160
|
+
readonly "data-ending-style"?: "";
|
|
161
|
+
readonly id: string;
|
|
162
|
+
readonly "data-state": "closed" | "delayed-open" | "instant-open";
|
|
163
|
+
readonly "data-disabled": "" | undefined;
|
|
162
164
|
};
|
|
163
165
|
readonly popperProps: {
|
|
164
166
|
onInteractOutside: (e: PointerEvent) => void;
|
|
@@ -2,7 +2,7 @@ import { onMountEffect, attachRef, DOMContext, simpleBox, boxWith, } from "svelt
|
|
|
2
2
|
import { on } from "svelte/events";
|
|
3
3
|
import { Context, watch } from "runed";
|
|
4
4
|
import { isElement, isFocusVisible } from "../../internal/is.js";
|
|
5
|
-
import { createBitsAttrs, boolToEmptyStrOrUndef } from "../../internal/attrs.js";
|
|
5
|
+
import { createBitsAttrs, boolToEmptyStrOrUndef, getDataTransitionAttrs, } from "../../internal/attrs.js";
|
|
6
6
|
import { TimeoutFn } from "../../internal/timeout-fn.js";
|
|
7
7
|
import { SafePolygon } from "../../internal/safe-polygon.svelte.js";
|
|
8
8
|
import { PresenceManager } from "../../internal/presence-manager.svelte.js";
|
|
@@ -665,6 +665,7 @@ export class TooltipContentState {
|
|
|
665
665
|
id: this.opts.id.current,
|
|
666
666
|
"data-state": this.root.stateAttr,
|
|
667
667
|
"data-disabled": boolToEmptyStrOrUndef(this.root.disabled),
|
|
668
|
+
...getDataTransitionAttrs(this.root.contentPresence.transitionStatus),
|
|
668
669
|
style: {
|
|
669
670
|
outline: "none",
|
|
670
671
|
},
|
|
@@ -348,7 +348,7 @@ export declare class FloatingContentState {
|
|
|
348
348
|
readonly perspective?: import("csstype").Property.Perspective<0 | (string & {})> | undefined;
|
|
349
349
|
readonly perspectiveOrigin?: import("csstype").Property.PerspectiveOrigin<0 | (string & {})> | undefined;
|
|
350
350
|
readonly pointerEvents?: import("csstype").Property.PointerEvents | undefined;
|
|
351
|
-
position: "relative" | "absolute" | "fixed" | "sticky" | "-moz-initial" | "
|
|
351
|
+
position: "relative" | "absolute" | "inherit" | "fixed" | "sticky" | "-moz-initial" | "initial" | "revert" | "revert-layer" | "unset" | "-webkit-sticky" | "static";
|
|
352
352
|
readonly printColorAdjust?: import("csstype").Property.PrintColorAdjust | undefined;
|
|
353
353
|
readonly quotes?: import("csstype").Property.Quotes | undefined;
|
|
354
354
|
readonly resize?: import("csstype").Property.Resize | undefined;
|
|
@@ -901,10 +901,10 @@ export declare class FloatingContentState {
|
|
|
901
901
|
readonly vectorEffect?: import("csstype").Property.VectorEffect | undefined;
|
|
902
902
|
readonly "pointer-events"?: string | undefined;
|
|
903
903
|
readonly "--bits-floating-transform-origin": `${any} ${any}`;
|
|
904
|
-
readonly "--bits-floating-available-width":
|
|
905
|
-
readonly "--bits-floating-available-height":
|
|
906
|
-
readonly "--bits-floating-anchor-width":
|
|
907
|
-
readonly "--bits-floating-anchor-height":
|
|
904
|
+
readonly "--bits-floating-available-width": `${number}px` | "undefinedpx";
|
|
905
|
+
readonly "--bits-floating-available-height": `${number}px` | "undefinedpx";
|
|
906
|
+
readonly "--bits-floating-anchor-width": `${number}px` | "undefinedpx";
|
|
907
|
+
readonly "--bits-floating-anchor-height": `${number}px` | "undefinedpx";
|
|
908
908
|
};
|
|
909
909
|
readonly dir: Direction;
|
|
910
910
|
};
|
|
@@ -12,5 +12,9 @@
|
|
|
12
12
|
</script>
|
|
13
13
|
|
|
14
14
|
{#if forceMount || open || presenceState.isPresent}
|
|
15
|
-
{@render
|
|
15
|
+
{@render
|
|
16
|
+
presence?.({
|
|
17
|
+
present: presenceState.isPresent,
|
|
18
|
+
transitionStatus: presenceState.transitionStatus,
|
|
19
|
+
})}
|
|
16
20
|
{/if}
|
|
@@ -1,50 +1,15 @@
|
|
|
1
1
|
import { type ReadableBox, type ReadableBoxedValues } from "svelte-toolbelt";
|
|
2
|
-
import {
|
|
3
|
-
import { StateMachine } from "../../../internal/state-machine.js";
|
|
2
|
+
import type { TransitionState } from "../../../internal/attrs.js";
|
|
4
3
|
export interface PresenceOptions extends ReadableBoxedValues<{
|
|
5
4
|
open: boolean;
|
|
6
5
|
ref: HTMLElement | null;
|
|
7
6
|
}> {
|
|
8
7
|
}
|
|
9
|
-
type PresenceStatus = "unmounted" | "mounted" | "unmountSuspended";
|
|
10
|
-
/**
|
|
11
|
-
* Cached style properties to avoid storing live CSSStyleDeclaration
|
|
12
|
-
* which triggers style recalculations when accessed.
|
|
13
|
-
*/
|
|
14
|
-
interface CachedStyles {
|
|
15
|
-
display: string;
|
|
16
|
-
animationName: string;
|
|
17
|
-
}
|
|
18
|
-
declare const presenceMachine: {
|
|
19
|
-
readonly mounted: {
|
|
20
|
-
readonly UNMOUNT: "unmounted";
|
|
21
|
-
readonly ANIMATION_OUT: "unmountSuspended";
|
|
22
|
-
};
|
|
23
|
-
readonly unmountSuspended: {
|
|
24
|
-
readonly MOUNT: "mounted";
|
|
25
|
-
readonly ANIMATION_END: "unmounted";
|
|
26
|
-
};
|
|
27
|
-
readonly unmounted: {
|
|
28
|
-
readonly MOUNT: "mounted";
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
type PresenceMachine = StateMachine<typeof presenceMachine>;
|
|
32
8
|
export declare class Presence {
|
|
9
|
+
#private;
|
|
33
10
|
readonly opts: PresenceOptions;
|
|
34
|
-
prevAnimationNameState: string;
|
|
35
|
-
styles: CachedStyles;
|
|
36
|
-
initialStatus: PresenceStatus;
|
|
37
|
-
previousPresent: Previous<boolean>;
|
|
38
|
-
machine: PresenceMachine;
|
|
39
11
|
present: ReadableBox<boolean>;
|
|
40
12
|
constructor(opts: PresenceOptions);
|
|
41
|
-
/**
|
|
42
|
-
* Triggering an ANIMATION_OUT during an ANIMATION_IN will fire an `animationcancel`
|
|
43
|
-
* event for ANIMATION_IN after we have entered `unmountSuspended` state. So, we
|
|
44
|
-
* make sure we only trigger ANIMATION_END for the currently active animation.
|
|
45
|
-
*/
|
|
46
|
-
handleAnimationEnd(event: AnimationEvent): void;
|
|
47
|
-
handleAnimationStart(event: AnimationEvent): void;
|
|
48
13
|
isPresent: boolean;
|
|
14
|
+
get transitionStatus(): TransitionState;
|
|
49
15
|
}
|
|
50
|
-
export {};
|
|
@@ -1,158 +1,61 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { StateMachine } from "../../../internal/state-machine.js";
|
|
5
|
-
/**
|
|
6
|
-
* Cache for animation names with TTL to reduce getComputedStyle calls.
|
|
7
|
-
* Uses WeakMap to avoid memory leaks when elements are removed.
|
|
8
|
-
*/
|
|
9
|
-
const animationNameCache = new WeakMap();
|
|
10
|
-
const ANIMATION_NAME_CACHE_TTL_MS = 16; // One frame at 60fps
|
|
11
|
-
const presenceMachine = {
|
|
12
|
-
mounted: {
|
|
13
|
-
UNMOUNT: "unmounted",
|
|
14
|
-
ANIMATION_OUT: "unmountSuspended",
|
|
15
|
-
},
|
|
16
|
-
unmountSuspended: {
|
|
17
|
-
MOUNT: "mounted",
|
|
18
|
-
ANIMATION_END: "unmounted",
|
|
19
|
-
},
|
|
20
|
-
unmounted: {
|
|
21
|
-
MOUNT: "mounted",
|
|
22
|
-
},
|
|
23
|
-
};
|
|
1
|
+
import { onDestroyEffect } from "svelte-toolbelt";
|
|
2
|
+
import { watch } from "runed";
|
|
3
|
+
import { AnimationsComplete } from "../../../internal/animations-complete.js";
|
|
24
4
|
export class Presence {
|
|
25
5
|
opts;
|
|
26
|
-
prevAnimationNameState = $state("none");
|
|
27
|
-
styles = $state({ display: "", animationName: "none" });
|
|
28
|
-
initialStatus;
|
|
29
|
-
previousPresent;
|
|
30
|
-
machine;
|
|
31
6
|
present;
|
|
7
|
+
#afterAnimations;
|
|
8
|
+
#isPresent = $state(false);
|
|
9
|
+
#hasMounted = false;
|
|
10
|
+
#transitionStatus = $state(undefined);
|
|
11
|
+
#transitionFrame = null;
|
|
32
12
|
constructor(opts) {
|
|
33
13
|
this.opts = opts;
|
|
34
14
|
this.present = this.opts.open;
|
|
35
|
-
this
|
|
36
|
-
this
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
* Triggering an ANIMATION_OUT during an ANIMATION_IN will fire an `animationcancel`
|
|
46
|
-
* event for ANIMATION_IN after we have entered `unmountSuspended` state. So, we
|
|
47
|
-
* make sure we only trigger ANIMATION_END for the currently active animation.
|
|
48
|
-
*/
|
|
49
|
-
handleAnimationEnd(event) {
|
|
50
|
-
if (!this.opts.ref.current)
|
|
51
|
-
return;
|
|
52
|
-
// Use cached animation name from styles when available to avoid getComputedStyle
|
|
53
|
-
const currAnimationName = this.styles.animationName || getAnimationName(this.opts.ref.current);
|
|
54
|
-
const isCurrentAnimation = currAnimationName.includes(event.animationName) || currAnimationName === "none";
|
|
55
|
-
if (event.target === this.opts.ref.current && isCurrentAnimation) {
|
|
56
|
-
this.machine.dispatch("ANIMATION_END");
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
handleAnimationStart(event) {
|
|
60
|
-
if (!this.opts.ref.current)
|
|
61
|
-
return;
|
|
62
|
-
if (event.target === this.opts.ref.current) {
|
|
63
|
-
// Force refresh cache on animation start to get accurate animation name
|
|
64
|
-
const animationName = getAnimationName(this.opts.ref.current, true);
|
|
65
|
-
this.prevAnimationNameState = animationName;
|
|
66
|
-
// Update styles cache for subsequent reads
|
|
67
|
-
this.styles.animationName = animationName;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
isPresent = $derived.by(() => {
|
|
71
|
-
return ["mounted", "unmountSuspended"].includes(this.machine.state.current);
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
function watchPresenceChange(state) {
|
|
75
|
-
watch(() => state.present.current, () => {
|
|
76
|
-
if (!state.opts.ref.current)
|
|
77
|
-
return;
|
|
78
|
-
const hasPresentChanged = state.present.current !== state.previousPresent.current;
|
|
79
|
-
if (!hasPresentChanged)
|
|
80
|
-
return;
|
|
81
|
-
const prevAnimationName = state.prevAnimationNameState;
|
|
82
|
-
// Force refresh on state change to get accurate current animation
|
|
83
|
-
const currAnimationName = getAnimationName(state.opts.ref.current, true);
|
|
84
|
-
// Update styles cache for subsequent reads
|
|
85
|
-
state.styles.animationName = currAnimationName;
|
|
86
|
-
if (state.present.current) {
|
|
87
|
-
state.machine.dispatch("MOUNT");
|
|
88
|
-
}
|
|
89
|
-
else if (currAnimationName === "none" || state.styles.display === "none") {
|
|
90
|
-
// If there is no exit animation or the element is hidden, animations won't run
|
|
91
|
-
// so we unmount instantly
|
|
92
|
-
state.machine.dispatch("UNMOUNT");
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
/**
|
|
96
|
-
* When `present` changes to `false`, we check changes to animation-name to
|
|
97
|
-
* determine whether an animation has started. We chose this approach (reading
|
|
98
|
-
* computed styles) because there is no `animationrun` event and `animationstart`
|
|
99
|
-
* fires after `animation-delay` has expired which would be too late.
|
|
100
|
-
*/
|
|
101
|
-
const isAnimating = prevAnimationName !== currAnimationName;
|
|
102
|
-
if (state.previousPresent.current && isAnimating) {
|
|
103
|
-
state.machine.dispatch("ANIMATION_OUT");
|
|
15
|
+
this.#isPresent = opts.open.current;
|
|
16
|
+
this.#afterAnimations = new AnimationsComplete({
|
|
17
|
+
ref: this.opts.ref,
|
|
18
|
+
afterTick: this.opts.open,
|
|
19
|
+
});
|
|
20
|
+
onDestroyEffect(() => this.#clearTransitionFrame());
|
|
21
|
+
watch(() => this.present.current, (isOpen) => {
|
|
22
|
+
if (!this.#hasMounted) {
|
|
23
|
+
this.#hasMounted = true;
|
|
24
|
+
return;
|
|
104
25
|
}
|
|
105
|
-
|
|
106
|
-
|
|
26
|
+
this.#clearTransitionFrame();
|
|
27
|
+
if (isOpen) {
|
|
28
|
+
this.#isPresent = true;
|
|
107
29
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
30
|
+
this.#transitionStatus = isOpen ? "starting" : "ending";
|
|
31
|
+
if (isOpen) {
|
|
32
|
+
this.#transitionFrame = window.requestAnimationFrame(() => {
|
|
33
|
+
this.#transitionFrame = null;
|
|
34
|
+
if (this.present.current) {
|
|
35
|
+
this.#transitionStatus = undefined;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
this.#afterAnimations.run(() => {
|
|
40
|
+
if (isOpen !== this.present.current)
|
|
41
|
+
return;
|
|
42
|
+
if (!isOpen) {
|
|
43
|
+
this.#isPresent = false;
|
|
44
|
+
}
|
|
45
|
+
this.#transitionStatus = undefined;
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
isPresent = $derived.by(() => {
|
|
50
|
+
return this.#isPresent;
|
|
122
51
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
52
|
+
get transitionStatus() {
|
|
53
|
+
return this.#transitionStatus;
|
|
54
|
+
}
|
|
55
|
+
#clearTransitionFrame() {
|
|
56
|
+
if (this.#transitionFrame === null)
|
|
127
57
|
return;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const computed = getComputedStyle(state.opts.ref.current);
|
|
131
|
-
state.styles = {
|
|
132
|
-
display: computed.display,
|
|
133
|
-
animationName: computed.animationName || "none",
|
|
134
|
-
};
|
|
135
|
-
return executeCallbacks(on(state.opts.ref.current, "animationstart", state.handleAnimationStart), on(state.opts.ref.current, "animationcancel", state.handleAnimationEnd), on(state.opts.ref.current, "animationend", state.handleAnimationEnd));
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Gets the animation name from computed styles with optional caching.
|
|
140
|
-
*
|
|
141
|
-
* @param node - The HTML element to get animation name from
|
|
142
|
-
* @param forceRefresh - If true, bypasses the cache and forces a fresh getComputedStyle call
|
|
143
|
-
* @returns The animation name or "none" if not animating
|
|
144
|
-
*/
|
|
145
|
-
function getAnimationName(node, forceRefresh = false) {
|
|
146
|
-
if (!node)
|
|
147
|
-
return "none";
|
|
148
|
-
const now = performance.now();
|
|
149
|
-
const cached = animationNameCache.get(node);
|
|
150
|
-
// Return cached value if still valid and not forced to refresh
|
|
151
|
-
if (!forceRefresh && cached && now - cached.timestamp < ANIMATION_NAME_CACHE_TTL_MS) {
|
|
152
|
-
return cached.value;
|
|
58
|
+
window.cancelAnimationFrame(this.#transitionFrame);
|
|
59
|
+
this.#transitionFrame = null;
|
|
153
60
|
}
|
|
154
|
-
// Compute and cache the new value
|
|
155
|
-
const value = getComputedStyle(node).animationName || "none";
|
|
156
|
-
animationNameCache.set(node, { value, timestamp: now });
|
|
157
|
-
return value;
|
|
158
61
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from "svelte";
|
|
2
2
|
import type { ReadableBox } from "svelte-toolbelt";
|
|
3
|
+
import type { TransitionState } from "../../../internal/attrs.js";
|
|
3
4
|
export type PresenceLayerProps = {
|
|
4
5
|
/**
|
|
5
6
|
* Whether to force mount the component.
|
|
@@ -11,8 +12,11 @@ export type PresenceLayerImplProps = PresenceLayerProps & {
|
|
|
11
12
|
* The open state of the component.
|
|
12
13
|
*/
|
|
13
14
|
open: boolean;
|
|
14
|
-
presence?: Snippet<[
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
presence?: Snippet<[
|
|
16
|
+
{
|
|
17
|
+
present: boolean;
|
|
18
|
+
transitionStatus: TransitionState;
|
|
19
|
+
}
|
|
20
|
+
]>;
|
|
17
21
|
ref: ReadableBox<HTMLElement | null>;
|
|
18
22
|
};
|