bits-ui 2.14.0 → 2.14.2

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.
Files changed (55) hide show
  1. package/dist/bits/accordion/accordion.svelte.d.ts +5 -1
  2. package/dist/bits/accordion/accordion.svelte.js +21 -2
  3. package/dist/bits/accordion/components/accordion-content.svelte +12 -24
  4. package/dist/bits/alert-dialog/components/alert-dialog-content.svelte +50 -62
  5. package/dist/bits/collapsible/collapsible.svelte.d.ts +4 -1
  6. package/dist/bits/collapsible/collapsible.svelte.js +15 -2
  7. package/dist/bits/collapsible/components/collapsible-content.svelte +12 -24
  8. package/dist/bits/context-menu/components/context-menu-content-static.svelte +2 -0
  9. package/dist/bits/context-menu/components/context-menu-content.svelte +2 -0
  10. package/dist/bits/date-field/date-field.svelte.js +5 -3
  11. package/dist/bits/dialog/components/dialog-content.svelte +44 -57
  12. package/dist/bits/dialog/components/dialog-overlay.svelte +9 -12
  13. package/dist/bits/dialog/dialog.svelte.d.ts +6 -0
  14. package/dist/bits/dialog/dialog.svelte.js +17 -3
  15. package/dist/bits/dropdown-menu/components/dropdown-menu-content-static.svelte +2 -0
  16. package/dist/bits/dropdown-menu/components/dropdown-menu-content.svelte +2 -0
  17. package/dist/bits/link-preview/components/link-preview-content-static.svelte +2 -0
  18. package/dist/bits/link-preview/components/link-preview-content.svelte +2 -0
  19. package/dist/bits/link-preview/link-preview.svelte.d.ts +3 -0
  20. package/dist/bits/link-preview/link-preview.svelte.js +6 -2
  21. package/dist/bits/menu/components/menu-content-static.svelte +2 -0
  22. package/dist/bits/menu/components/menu-content.svelte +2 -0
  23. package/dist/bits/menu/components/menu-sub-content-static.svelte +2 -0
  24. package/dist/bits/menu/components/menu-sub-content.svelte +2 -0
  25. package/dist/bits/menu/menu.svelte.d.ts +3 -0
  26. package/dist/bits/menu/menu.svelte.js +6 -2
  27. package/dist/bits/menubar/components/menubar.svelte +1 -1
  28. package/dist/bits/menubar/components/menubar.svelte.d.ts +1 -1
  29. package/dist/bits/popover/components/popover-content-static.svelte +2 -0
  30. package/dist/bits/popover/components/popover-content.svelte +2 -0
  31. package/dist/bits/popover/components/popover-overlay.svelte +9 -12
  32. package/dist/bits/popover/popover.svelte.d.ts +6 -0
  33. package/dist/bits/popover/popover.svelte.js +16 -3
  34. package/dist/bits/select/components/select-content-static.svelte +2 -0
  35. package/dist/bits/select/components/select-content.svelte +2 -0
  36. package/dist/bits/select/select.svelte.d.ts +3 -0
  37. package/dist/bits/select/select.svelte.js +6 -2
  38. package/dist/bits/tooltip/components/tooltip-content-static.svelte +2 -0
  39. package/dist/bits/tooltip/components/tooltip-content.svelte +2 -0
  40. package/dist/bits/tooltip/tooltip.svelte.d.ts +4 -0
  41. package/dist/bits/tooltip/tooltip.svelte.js +39 -4
  42. package/dist/bits/utilities/popper-layer/popper-layer-inner.svelte +1 -1
  43. package/dist/bits/utilities/popper-layer/popper-layer-inner.svelte.d.ts +1 -1
  44. package/dist/bits/utilities/popper-layer/popper-layer.svelte +43 -45
  45. package/dist/bits/utilities/popper-layer/types.d.ts +4 -0
  46. package/dist/internal/animations-complete.js +7 -15
  47. package/dist/internal/date-time/field/helpers.js +3 -1
  48. package/dist/internal/date-time/field/time-helpers.js +4 -1
  49. package/dist/internal/presence-manager.svelte.d.ts +14 -0
  50. package/dist/internal/presence-manager.svelte.js +34 -0
  51. package/dist/internal/should-enable-focus-trap.d.ts +1 -2
  52. package/dist/internal/should-enable-focus-trap.js +2 -2
  53. package/package.json +2 -2
  54. package/dist/internal/open-change-complete.d.ts +0 -13
  55. package/dist/internal/open-change-complete.js +0 -24
@@ -1,6 +1,7 @@
1
1
  import { DOMContext, type WritableBoxedValues, type ReadableBoxedValues } from "svelte-toolbelt";
2
2
  import type { OnChangeFn, RefAttachment, WithRefOpts } from "../../internal/types.js";
3
3
  import type { FocusEventHandler, MouseEventHandler, PointerEventHandler } from "svelte/elements";
4
+ import { PresenceManager } from "../../internal/presence-manager.svelte.js";
4
5
  export declare const tooltipAttrs: import("../../internal/attrs.js").CreateBitsAttrsReturn<readonly ["content", "trigger"]>;
5
6
  interface TooltipProviderStateOpts extends ReadableBoxedValues<{
6
7
  delayDuration: number;
@@ -44,6 +45,7 @@ export declare class TooltipRootState {
44
45
  readonly disabled: boolean;
45
46
  readonly ignoreNonKeyboardFocus: boolean;
46
47
  contentNode: HTMLElement | null;
48
+ contentPresence: PresenceManager;
47
49
  triggerNode: HTMLElement | null;
48
50
  readonly stateAttr: string;
49
51
  constructor(opts: TooltipRootStateOpts, provider: TooltipProviderState);
@@ -75,6 +77,7 @@ export declare class TooltipTriggerState {
75
77
  readonly disabled: boolean;
76
78
  readonly onpointerup: PointerEventHandler<HTMLElement>;
77
79
  readonly onpointerdown: PointerEventHandler<HTMLElement>;
80
+ readonly onpointerenter: PointerEventHandler<HTMLElement>;
78
81
  readonly onpointermove: PointerEventHandler<HTMLElement>;
79
82
  readonly onpointerleave: PointerEventHandler<HTMLElement>;
80
83
  readonly onfocus: FocusEventHandler<HTMLElement>;
@@ -97,6 +100,7 @@ export declare class TooltipContentState {
97
100
  onEscapeKeydown: (e: KeyboardEvent) => void;
98
101
  onOpenAutoFocus: (e: Event) => void;
99
102
  onCloseAutoFocus: (e: Event) => void;
103
+ get shouldRender(): boolean;
100
104
  readonly snippetProps: {
101
105
  open: boolean;
102
106
  };
@@ -5,7 +5,7 @@ import { isElement, isFocusVisible } from "../../internal/is.js";
5
5
  import { createBitsAttrs, boolToEmptyStrOrUndef } from "../../internal/attrs.js";
6
6
  import { TimeoutFn } from "../../internal/timeout-fn.js";
7
7
  import { GraceArea } from "../../internal/grace-area.svelte.js";
8
- import { OpenChangeComplete } from "../../internal/open-change-complete.js";
8
+ import { PresenceManager } from "../../internal/presence-manager.svelte.js";
9
9
  export const tooltipAttrs = createBitsAttrs({
10
10
  component: "tooltip",
11
11
  parts: ["content", "trigger"],
@@ -72,6 +72,7 @@ export class TooltipRootState {
72
72
  ignoreNonKeyboardFocus = $derived.by(() => this.opts.ignoreNonKeyboardFocus.current ??
73
73
  this.provider.opts.ignoreNonKeyboardFocus.current);
74
74
  contentNode = $state(null);
75
+ contentPresence;
75
76
  triggerNode = $state(null);
76
77
  #wasOpenDelayed = $state(false);
77
78
  #timerFn;
@@ -87,7 +88,7 @@ export class TooltipRootState {
87
88
  this.#wasOpenDelayed = true;
88
89
  this.opts.open.current = true;
89
90
  }, this.delayDuration ?? 0);
90
- new OpenChangeComplete({
91
+ this.contentPresence = new PresenceManager({
91
92
  open: this.opts.open,
92
93
  ref: boxWith(() => this.contentNode),
93
94
  onComplete: () => {
@@ -158,12 +159,19 @@ export class TooltipTriggerState {
158
159
  #hasPointerMoveOpened = $state(false);
159
160
  #isDisabled = $derived.by(() => this.opts.disabled.current || this.root.disabled);
160
161
  domContext;
162
+ #transitCheckTimeout = null;
161
163
  constructor(opts, root) {
162
164
  this.opts = opts;
163
165
  this.root = root;
164
166
  this.domContext = new DOMContext(opts.ref);
165
167
  this.attachment = attachRef(this.opts.ref, (v) => (this.root.triggerNode = v));
166
168
  }
169
+ #clearTransitCheck = () => {
170
+ if (this.#transitCheckTimeout !== null) {
171
+ clearTimeout(this.#transitCheckTimeout);
172
+ this.#transitCheckTimeout = null;
173
+ }
174
+ };
167
175
  handlePointerUp = () => {
168
176
  this.#isPointerDown.current = false;
169
177
  };
@@ -180,6 +188,27 @@ export class TooltipTriggerState {
180
188
  this.handlePointerUp();
181
189
  }, { once: true });
182
190
  };
191
+ #onpointerenter = (e) => {
192
+ if (this.#isDisabled)
193
+ return;
194
+ if (e.pointerType === "touch")
195
+ return;
196
+ // if in transit, wait briefly to see if user is actually heading to old content or staying here
197
+ if (this.root.provider.isPointerInTransit.current) {
198
+ this.#clearTransitCheck();
199
+ this.#transitCheckTimeout = window.setTimeout(() => {
200
+ // if still in transit after delay, user is likely staying on this trigger
201
+ if (this.root.provider.isPointerInTransit.current) {
202
+ this.root.provider.isPointerInTransit.current = false;
203
+ this.root.onTriggerEnter();
204
+ this.#hasPointerMoveOpened = true;
205
+ }
206
+ }, 250);
207
+ return;
208
+ }
209
+ this.root.onTriggerEnter();
210
+ this.#hasPointerMoveOpened = true;
211
+ };
183
212
  #onpointermove = (e) => {
184
213
  if (this.#isDisabled)
185
214
  return;
@@ -187,14 +216,16 @@ export class TooltipTriggerState {
187
216
  return;
188
217
  if (this.#hasPointerMoveOpened)
189
218
  return;
190
- if (this.root.provider.isPointerInTransit.current)
191
- return;
219
+ // moving within trigger means we're definitely not in transit anymore
220
+ this.#clearTransitCheck();
221
+ this.root.provider.isPointerInTransit.current = false;
192
222
  this.root.onTriggerEnter();
193
223
  this.#hasPointerMoveOpened = true;
194
224
  };
195
225
  #onpointerleave = () => {
196
226
  if (this.#isDisabled)
197
227
  return;
228
+ this.#clearTransitCheck();
198
229
  this.root.onTriggerLeave();
199
230
  this.#hasPointerMoveOpened = false;
200
231
  };
@@ -228,6 +259,7 @@ export class TooltipTriggerState {
228
259
  disabled: this.opts.disabled.current,
229
260
  onpointerup: this.#onpointerup,
230
261
  onpointerdown: this.#onpointerdown,
262
+ onpointerenter: this.#onpointerenter,
231
263
  onpointermove: this.#onpointermove,
232
264
  onpointerleave: this.#onpointerleave,
233
265
  onfocus: this.#onfocus,
@@ -294,6 +326,9 @@ export class TooltipContentState {
294
326
  onCloseAutoFocus = (e) => {
295
327
  e.preventDefault();
296
328
  };
329
+ get shouldRender() {
330
+ return this.root.contentPresence.shouldRender;
331
+ }
297
332
  snippetProps = $derived.by(() => ({ open: this.root.opts.open.current }));
298
333
  props = $derived.by(() => ({
299
334
  id: this.opts.id.current,
@@ -47,7 +47,7 @@
47
47
  ref,
48
48
  tooltip = false,
49
49
  ...restProps
50
- }: Omit<PopperLayerImplProps, "open" | "children"> & {
50
+ }: Omit<PopperLayerImplProps, "open" | "children" | "shouldRender"> & {
51
51
  enabled: boolean;
52
52
  } = $props();
53
53
  </script>
@@ -1,5 +1,5 @@
1
1
  import type { PopperLayerImplProps } from "./types.js";
2
- type $$ComponentProps = Omit<PopperLayerImplProps, "open" | "children"> & {
2
+ type $$ComponentProps = Omit<PopperLayerImplProps, "open" | "children" | "shouldRender"> & {
3
3
  enabled: boolean;
4
4
  };
5
5
  declare const PopperLayerInner: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -1,7 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { PopperLayerImplProps } from "./types.js";
3
3
  import PopperLayerInner from "./popper-layer-inner.svelte";
4
- import PresenceLayer from "../presence-layer/presence-layer.svelte";
5
4
 
6
5
  let {
7
6
  popper,
@@ -40,51 +39,50 @@
40
39
  customAnchor = null,
41
40
  isStatic = false,
42
41
  ref,
42
+ shouldRender,
43
43
  ...restProps
44
44
  }: PopperLayerImplProps = $props();
45
45
  </script>
46
46
 
47
- <PresenceLayer {open} {ref}>
48
- {#snippet presence()}
49
- <PopperLayerInner
50
- {popper}
51
- {onEscapeKeydown}
52
- {escapeKeydownBehavior}
53
- {preventOverflowTextSelection}
54
- {id}
55
- {onPointerDown}
56
- {onPointerUp}
57
- {side}
58
- {sideOffset}
59
- {align}
60
- {alignOffset}
61
- {arrowPadding}
62
- {avoidCollisions}
63
- {collisionBoundary}
64
- {collisionPadding}
65
- {sticky}
66
- {hideWhenDetached}
67
- {updatePositionStrategy}
68
- {strategy}
69
- {dir}
70
- {preventScroll}
71
- {wrapperId}
72
- {style}
73
- {onPlaced}
74
- {customAnchor}
75
- {isStatic}
76
- enabled={open}
77
- {onInteractOutside}
78
- {onCloseAutoFocus}
79
- {onOpenAutoFocus}
80
- {interactOutsideBehavior}
81
- {loop}
82
- {trapFocus}
83
- {isValidEvent}
84
- {onFocusOutside}
85
- forceMount={false}
86
- {ref}
87
- {...restProps}
88
- />
89
- {/snippet}
90
- </PresenceLayer>
47
+ {#if shouldRender}
48
+ <PopperLayerInner
49
+ {popper}
50
+ {onEscapeKeydown}
51
+ {escapeKeydownBehavior}
52
+ {preventOverflowTextSelection}
53
+ {id}
54
+ {onPointerDown}
55
+ {onPointerUp}
56
+ {side}
57
+ {sideOffset}
58
+ {align}
59
+ {alignOffset}
60
+ {arrowPadding}
61
+ {avoidCollisions}
62
+ {collisionBoundary}
63
+ {collisionPadding}
64
+ {sticky}
65
+ {hideWhenDetached}
66
+ {updatePositionStrategy}
67
+ {strategy}
68
+ {dir}
69
+ {preventScroll}
70
+ {wrapperId}
71
+ {style}
72
+ {onPlaced}
73
+ {customAnchor}
74
+ {isStatic}
75
+ enabled={open}
76
+ {onInteractOutside}
77
+ {onCloseAutoFocus}
78
+ {onOpenAutoFocus}
79
+ {interactOutsideBehavior}
80
+ {loop}
81
+ {trapFocus}
82
+ {isValidEvent}
83
+ {onFocusOutside}
84
+ forceMount={false}
85
+ {ref}
86
+ {...restProps}
87
+ />
88
+ {/if}
@@ -31,4 +31,8 @@ export type PopperLayerImplProps = Omit<EscapeLayerImplProps & DismissibleLayerI
31
31
  * symbol so that conflicts don't occur.
32
32
  */
33
33
  tooltip?: boolean;
34
+ /**
35
+ * Whether the popper layer should be rendered.
36
+ */
37
+ shouldRender: boolean;
34
38
  }, "enabled">;
@@ -1,30 +1,23 @@
1
1
  import { afterTick, onDestroyEffect } from "svelte-toolbelt";
2
2
  export class AnimationsComplete {
3
3
  #opts;
4
- #currentFrame = undefined;
5
- #isRunning = false;
4
+ #currentFrame = null;
6
5
  constructor(opts) {
7
6
  this.#opts = opts;
8
7
  onDestroyEffect(() => this.#cleanup());
9
8
  }
10
9
  #cleanup() {
11
- if (this.#currentFrame) {
12
- window.cancelAnimationFrame(this.#currentFrame);
13
- this.#currentFrame = undefined;
14
- }
15
- this.#isRunning = false;
10
+ if (!this.#currentFrame)
11
+ return;
12
+ window.cancelAnimationFrame(this.#currentFrame);
13
+ this.#currentFrame = null;
16
14
  }
17
15
  run(fn) {
18
- // prevent multiple concurrent runs
19
- if (this.#isRunning)
20
- return;
16
+ // if already running, cleanup and restart
21
17
  this.#cleanup();
22
- this.#isRunning = true;
23
18
  const node = this.#opts.ref.current;
24
- if (!node) {
25
- this.#isRunning = false;
19
+ if (!node)
26
20
  return;
27
- }
28
21
  if (typeof node.getAnimations !== "function") {
29
22
  this.#executeCallback(fn);
30
23
  return;
@@ -43,7 +36,6 @@ export class AnimationsComplete {
43
36
  #executeCallback(fn) {
44
37
  const execute = () => {
45
38
  fn();
46
- this.#isRunning = false;
47
39
  };
48
40
  if (this.#opts.afterTick) {
49
41
  afterTick(execute);
@@ -60,7 +60,9 @@ function createContentObj(props) {
60
60
  * If we're operating in a 12 hour clock and the part is an hour, we handle
61
61
  * the conversion to 12 hour format with 2 digit hours and leading zeros here.
62
62
  */
63
- if (part === "hour" && "dayPeriod" in segmentValues && props.hourCycle !== 24) {
63
+ const is12HourMode = props.hourCycle === 12 ||
64
+ (props.hourCycle === undefined && getDefaultHourCycle(locale) === 12);
65
+ if (part === "hour" && is12HourMode) {
64
66
  /**
65
67
  * If the value is over 12, we convert to 12 hour format and add leading
66
68
  * zeroes if the value is less than 10.
@@ -6,6 +6,7 @@ import { styleToString } from "svelte-toolbelt";
6
6
  import { useId } from "../../use-id.js";
7
7
  import { getPlaceholder } from "../placeholders.js";
8
8
  import { isZonedDateTime } from "../utils.js";
9
+ import { getDefaultHourCycle } from "./helpers.js";
9
10
  export function initializeSegmentValues() {
10
11
  const initialParts = EDITABLE_TIME_SEGMENT_PARTS.map((part) => {
11
12
  if (part === "dayPeriod") {
@@ -50,7 +51,9 @@ function createTimeContentObj(props) {
50
51
  * If we're operating in a 12 hour clock and the part is an hour, we handle
51
52
  * the conversion to 12 hour format with 2 digit hours and leading zeros here.
52
53
  */
53
- if (part === "hour" && "dayPeriod" in segmentValues && props.hourCycle !== 24) {
54
+ const is12HourMode = props.hourCycle === 12 ||
55
+ (props.hourCycle === undefined && getDefaultHourCycle(locale) === 12);
56
+ if (part === "hour" && is12HourMode) {
54
57
  /**
55
58
  * If the value is over 12, we convert to 12 hour format and add leading
56
59
  * zeroes if the value is less than 10.
@@ -0,0 +1,14 @@
1
+ import type { ReadableBoxedValues } from "svelte-toolbelt";
2
+ interface PresenceManagerOpts extends ReadableBoxedValues<{
3
+ open: boolean;
4
+ ref: HTMLElement | null;
5
+ }> {
6
+ onComplete?: () => void;
7
+ enabled?: boolean;
8
+ }
9
+ export declare class PresenceManager {
10
+ #private;
11
+ constructor(opts: PresenceManagerOpts);
12
+ get shouldRender(): boolean;
13
+ }
14
+ export {};
@@ -0,0 +1,34 @@
1
+ import { watch } from "runed";
2
+ import { AnimationsComplete } from "./animations-complete.js";
3
+ export class PresenceManager {
4
+ #opts;
5
+ #enabled;
6
+ #afterAnimations;
7
+ #shouldRender = $state(false);
8
+ constructor(opts) {
9
+ this.#opts = opts;
10
+ this.#shouldRender = opts.open.current;
11
+ this.#enabled = opts.enabled ?? true;
12
+ this.#afterAnimations = new AnimationsComplete({
13
+ ref: this.#opts.ref,
14
+ afterTick: this.#opts.open,
15
+ });
16
+ watch(() => this.#opts.open.current, (isOpen) => {
17
+ if (isOpen)
18
+ this.#shouldRender = true;
19
+ if (!this.#enabled)
20
+ return;
21
+ this.#afterAnimations.run(() => {
22
+ if (isOpen === this.#opts.open.current) {
23
+ if (!this.#opts.open.current) {
24
+ this.#shouldRender = false;
25
+ }
26
+ this.#opts.onComplete?.();
27
+ }
28
+ });
29
+ });
30
+ }
31
+ get shouldRender() {
32
+ return this.#shouldRender;
33
+ }
34
+ }
@@ -1,5 +1,4 @@
1
- export declare function shouldEnableFocusTrap({ forceMount, present, open, }: {
1
+ export declare function shouldEnableFocusTrap({ forceMount, open, }: {
2
2
  forceMount: boolean;
3
- present: boolean;
4
3
  open: boolean;
5
4
  }): boolean;
@@ -1,5 +1,5 @@
1
- export function shouldEnableFocusTrap({ forceMount, present, open, }) {
1
+ export function shouldEnableFocusTrap({ forceMount, open, }) {
2
2
  if (forceMount)
3
3
  return open;
4
- return present && open;
4
+ return open;
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "2.14.0",
3
+ "version": "2.14.2",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",
@@ -28,7 +28,7 @@
28
28
  "csstype": "^3.1.3",
29
29
  "jsdom": "^24.1.3",
30
30
  "publint": "^0.2.12",
31
- "svelte": "^5.38.10",
31
+ "svelte": "5.38.1",
32
32
  "svelte-check": "^4.3.1",
33
33
  "typescript": "^5.9.2",
34
34
  "vite": "^7.1.5",
@@ -1,13 +0,0 @@
1
- import type { ReadableBoxedValues } from "svelte-toolbelt";
2
- interface OpenChangeCompleteOpts extends ReadableBoxedValues<{
3
- open: boolean;
4
- ref: HTMLElement | null;
5
- }> {
6
- onComplete: () => void;
7
- enabled?: boolean;
8
- }
9
- export declare class OpenChangeComplete {
10
- #private;
11
- constructor(opts: OpenChangeCompleteOpts);
12
- }
13
- export {};
@@ -1,24 +0,0 @@
1
- import { watch } from "runed";
2
- import { AnimationsComplete } from "./animations-complete.js";
3
- export class OpenChangeComplete {
4
- #opts;
5
- #enabled;
6
- #afterAnimations;
7
- constructor(opts) {
8
- this.#opts = opts;
9
- this.#enabled = opts.enabled ?? true;
10
- this.#afterAnimations = new AnimationsComplete({
11
- ref: this.#opts.ref,
12
- afterTick: this.#opts.open,
13
- });
14
- watch([() => this.#opts.open.current], ([open]) => {
15
- if (!this.#enabled)
16
- return;
17
- this.#afterAnimations.run(() => {
18
- if (open === this.#opts.open.current) {
19
- this.#opts.onComplete();
20
- }
21
- });
22
- });
23
- }
24
- }