bits-ui 2.15.3 → 2.15.5

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 (35) hide show
  1. package/dist/bits/accordion/accordion.svelte.d.ts +2 -1
  2. package/dist/bits/accordion/accordion.svelte.js +1 -1
  3. package/dist/bits/accordion/components/accordion-trigger.svelte +2 -0
  4. package/dist/bits/context-menu/components/context-menu-content-static.svelte +11 -6
  5. package/dist/bits/context-menu/components/context-menu-content.svelte +11 -6
  6. package/dist/bits/dialog/dialog.svelte.d.ts +1 -0
  7. package/dist/bits/dialog/dialog.svelte.js +3 -0
  8. package/dist/bits/dropdown-menu/components/dropdown-menu-content-static.svelte +11 -6
  9. package/dist/bits/dropdown-menu/components/dropdown-menu-content.svelte +11 -6
  10. package/dist/bits/link-preview/components/link-preview-content-static.svelte +15 -10
  11. package/dist/bits/link-preview/components/link-preview-content.svelte +15 -10
  12. package/dist/bits/menu/components/menu-content-static.svelte +11 -12
  13. package/dist/bits/menu/components/menu-content.svelte +11 -12
  14. package/dist/bits/menu/components/menu-sub-content-static.svelte +13 -6
  15. package/dist/bits/menu/components/menu-sub-content.svelte +13 -6
  16. package/dist/bits/menu/menu.svelte.d.ts +1 -0
  17. package/dist/bits/menu/menu.svelte.js +2 -0
  18. package/dist/bits/pin-input/pin-input.svelte.js +2 -2
  19. package/dist/bits/popover/components/popover-content-static.svelte +13 -6
  20. package/dist/bits/popover/components/popover-content.svelte +11 -6
  21. package/dist/bits/popover/popover.svelte.d.ts +2 -0
  22. package/dist/bits/popover/popover.svelte.js +14 -1
  23. package/dist/bits/scroll-area/scroll-area.svelte.d.ts +2 -0
  24. package/dist/bits/scroll-area/scroll-area.svelte.js +14 -4
  25. package/dist/bits/select/components/select-content-static.svelte +3 -2
  26. package/dist/bits/select/components/select-content.svelte +3 -2
  27. package/dist/bits/tooltip/components/tooltip-content-static.svelte +15 -10
  28. package/dist/bits/tooltip/components/tooltip-content.svelte +15 -10
  29. package/dist/bits/tooltip/components/tooltip-trigger.svelte +2 -0
  30. package/dist/bits/tooltip/tooltip.svelte.d.ts +2 -1
  31. package/dist/bits/tooltip/tooltip.svelte.js +1 -1
  32. package/dist/bits/utilities/focus-scope/focus-scope.svelte.js +1 -1
  33. package/dist/bits/utilities/presence-layer/presence.svelte.d.ts +9 -1
  34. package/dist/bits/utilities/presence-layer/presence.svelte.js +52 -10
  35. package/package.json +3 -3
@@ -26,6 +26,7 @@ interface AccordionItemStateOpts extends WithRefOpts, ReadableBoxedValues<{
26
26
  }
27
27
  interface AccordionTriggerStateOpts extends WithRefOpts, ReadableBoxedValues<{
28
28
  disabled: boolean | null | undefined;
29
+ tabindex: number;
29
30
  }> {
30
31
  }
31
32
  interface AccordionContentStateOpts extends WithRefOpts, ReadableBoxedValues<{
@@ -111,7 +112,7 @@ export declare class AccordionTriggerState {
111
112
  readonly "data-disabled": "" | undefined;
112
113
  readonly "data-state": "open" | "closed";
113
114
  readonly "data-orientation": Orientation;
114
- readonly tabindex: 0;
115
+ readonly tabindex: number;
115
116
  readonly onclick: (e: BitsMouseEvent) => void;
116
117
  readonly onkeydown: (e: BitsKeyboardEvent) => void;
117
118
  };
@@ -155,7 +155,7 @@ export class AccordionTriggerState {
155
155
  "data-state": getDataOpenClosed(this.itemState.isActive),
156
156
  "data-orientation": this.#root.opts.orientation.current,
157
157
  [accordionAttrs.trigger]: "",
158
- tabindex: 0,
158
+ tabindex: this.opts.tabindex.current,
159
159
  onclick: this.onclick,
160
160
  onkeydown: this.onkeydown,
161
161
  ...this.attachment,
@@ -10,6 +10,7 @@
10
10
  disabled = false,
11
11
  ref = $bindable(null),
12
12
  id = createId(uid),
13
+ tabindex = 0,
13
14
  children,
14
15
  child,
15
16
  ...restProps
@@ -18,6 +19,7 @@
18
19
  const triggerState = AccordionTriggerState.create({
19
20
  disabled: boxWith(() => disabled),
20
21
  id: boxWith(() => id),
22
+ tabindex: boxWith(() => tabindex ?? 0),
21
23
  ref: boxWith(
22
24
  () => ref,
23
25
  (v) => (ref = v)
@@ -21,6 +21,7 @@
21
21
  // the default menu behavior of handling outside interactions on the trigger
22
22
  onEscapeKeydown = noop,
23
23
  forceMount = false,
24
+ style,
24
25
  ...restProps
25
26
  }: ContextMenuContentStaticProps = $props();
26
27
 
@@ -82,9 +83,11 @@
82
83
  shouldRender={contentState.shouldRender}
83
84
  >
84
85
  {#snippet popper({ props })}
85
- {@const finalProps = mergeProps(props, {
86
- style: getFloatingContentCSSVars("context-menu"),
87
- })}
86
+ {@const finalProps = mergeProps(
87
+ props,
88
+ { style: getFloatingContentCSSVars("context-menu") },
89
+ { style }
90
+ )}
88
91
  {#if child}
89
92
  {@render child({ props: finalProps, ...contentState.snippetProps })}
90
93
  {:else}
@@ -115,9 +118,11 @@
115
118
  shouldRender={contentState.shouldRender}
116
119
  >
117
120
  {#snippet popper({ props })}
118
- {@const finalProps = mergeProps(props, {
119
- style: getFloatingContentCSSVars("context-menu"),
120
- })}
121
+ {@const finalProps = mergeProps(
122
+ props,
123
+ { style: getFloatingContentCSSVars("context-menu") },
124
+ { style }
125
+ )}
121
126
  {#if child}
122
127
  {@render child({ props: finalProps, ...contentState.snippetProps })}
123
128
  {:else}
@@ -26,6 +26,7 @@
26
26
  onEscapeKeydown = noop,
27
27
  forceMount = false,
28
28
  trapFocus = false,
29
+ style,
29
30
  ...restProps
30
31
  }: ContextMenuContentProps = $props();
31
32
 
@@ -95,9 +96,11 @@
95
96
  enabled={contentState.parentMenu.opts.open.current}
96
97
  >
97
98
  {#snippet popper({ props, wrapperProps })}
98
- {@const finalProps = mergeProps(props, {
99
- style: getFloatingContentCSSVars("context-menu"),
100
- })}
99
+ {@const finalProps = mergeProps(
100
+ props,
101
+ { style: getFloatingContentCSSVars("context-menu") },
102
+ { style }
103
+ )}
101
104
  {#if child}
102
105
  {@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
103
106
  {:else}
@@ -116,9 +119,11 @@
116
119
  open={contentState.parentMenu.opts.open.current}
117
120
  >
118
121
  {#snippet popper({ props, wrapperProps })}
119
- {@const finalProps = mergeProps(props, {
120
- style: getFloatingContentCSSVars("context-menu"),
121
- })}
122
+ {@const finalProps = mergeProps(
123
+ props,
124
+ { style: getFloatingContentCSSVars("context-menu") },
125
+ { style }
126
+ )}
122
127
  {#if child}
123
128
  {@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
124
129
  {:else}
@@ -148,6 +148,7 @@ export declare class DialogContentState {
148
148
  readonly outline: "none" | undefined;
149
149
  readonly "--bits-dialog-depth": number;
150
150
  readonly "--bits-dialog-nested-count": number;
151
+ readonly contain: "layout style paint";
151
152
  };
152
153
  readonly tabindex: -1 | undefined;
153
154
  readonly "data-nested-open": "" | undefined;
@@ -272,6 +272,9 @@ export class DialogContentState {
272
272
  outline: this.root.opts.variant.current === "alert-dialog" ? "none" : undefined,
273
273
  "--bits-dialog-depth": this.root.depth,
274
274
  "--bits-dialog-nested-count": this.root.nestedOpenCount,
275
+ // CSS containment isolates style/layout/paint calculations from the rest of the page,
276
+ // improving performance when there's a large DOM behind the dialog
277
+ contain: "layout style paint",
275
278
  },
276
279
  tabindex: this.root.opts.variant.current === "alert-dialog" ? -1 : undefined,
277
280
  "data-nested-open": boolToEmptyStrOrUndef(this.root.nestedOpenCount > 0),
@@ -20,6 +20,7 @@
20
20
  onEscapeKeydown = noop,
21
21
  onCloseAutoFocus = noop,
22
22
  forceMount = false,
23
+ style,
23
24
  ...restProps
24
25
  }: DropdownMenuContentStaticProps = $props();
25
26
 
@@ -65,9 +66,11 @@
65
66
  shouldRender={contentState.shouldRender}
66
67
  >
67
68
  {#snippet popper({ props })}
68
- {@const finalProps = mergeProps(props, {
69
- style: getFloatingContentCSSVars("dropdown-menu"),
70
- })}
69
+ {@const finalProps = mergeProps(
70
+ props,
71
+ { style: getFloatingContentCSSVars("dropdown-menu") },
72
+ { style }
73
+ )}
71
74
  {#if child}
72
75
  {@render child({ props: finalProps, ...contentState.snippetProps })}
73
76
  {:else}
@@ -93,9 +96,11 @@
93
96
  shouldRender={contentState.shouldRender}
94
97
  >
95
98
  {#snippet popper({ props })}
96
- {@const finalProps = mergeProps(props, {
97
- style: getFloatingContentCSSVars("dropdown-menu"),
98
- })}
99
+ {@const finalProps = mergeProps(
100
+ props,
101
+ { style: getFloatingContentCSSVars("dropdown-menu") },
102
+ { style }
103
+ )}
99
104
  {#if child}
100
105
  {@render child({ props: finalProps, ...contentState.snippetProps })}
101
106
  {:else}
@@ -21,6 +21,7 @@
21
21
  onCloseAutoFocus = noop,
22
22
  forceMount = false,
23
23
  trapFocus = false,
24
+ style,
24
25
  ...restProps
25
26
  }: DropdownMenuContentProps = $props();
26
27
 
@@ -71,9 +72,11 @@
71
72
  shouldRender={contentState.shouldRender}
72
73
  >
73
74
  {#snippet popper({ props, wrapperProps })}
74
- {@const finalProps = mergeProps(props, {
75
- style: getFloatingContentCSSVars("dropdown-menu"),
76
- })}
75
+ {@const finalProps = mergeProps(
76
+ props,
77
+ { style: getFloatingContentCSSVars("dropdown-menu") },
78
+ { style }
79
+ )}
77
80
  {#if child}
78
81
  {@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
79
82
  {:else}
@@ -100,9 +103,11 @@
100
103
  shouldRender={contentState.shouldRender}
101
104
  >
102
105
  {#snippet popper({ props, wrapperProps })}
103
- {@const finalProps = mergeProps(props, {
104
- style: getFloatingContentCSSVars("dropdown-menu"),
105
- })}
106
+ {@const finalProps = mergeProps(
107
+ props,
108
+ { style: getFloatingContentCSSVars("dropdown-menu") },
109
+ { style }
110
+ )}
106
111
  {#if child}
107
112
  {@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
108
113
  {:else}
@@ -19,6 +19,7 @@
19
19
  onInteractOutside = noop,
20
20
  onEscapeKeydown = noop,
21
21
  forceMount = false,
22
+ style,
22
23
  ...restProps
23
24
  }: LinkPreviewContentStaticProps = $props();
24
25
 
@@ -50,13 +51,15 @@
50
51
  shouldRender={contentState.shouldRender}
51
52
  >
52
53
  {#snippet popper({ props })}
53
- {@const mergedProps = mergeProps(props, {
54
- style: getFloatingContentCSSVars("link-preview"),
55
- })}
54
+ {@const finalProps = mergeProps(
55
+ props,
56
+ { style: getFloatingContentCSSVars("link-preview") },
57
+ { style }
58
+ )}
56
59
  {#if child}
57
- {@render child({ props: mergedProps, ...contentState.snippetProps })}
60
+ {@render child({ props: finalProps, ...contentState.snippetProps })}
58
61
  {:else}
59
- <div {...mergedProps}>
62
+ <div {...finalProps}>
60
63
  {@render children?.()}
61
64
  </div>
62
65
  {/if}
@@ -78,13 +81,15 @@
78
81
  shouldRender={contentState.shouldRender}
79
82
  >
80
83
  {#snippet popper({ props })}
81
- {@const mergedProps = mergeProps(props, {
82
- style: getFloatingContentCSSVars("link-preview"),
83
- })}
84
+ {@const finalProps = mergeProps(
85
+ props,
86
+ { style: getFloatingContentCSSVars("link-preview") },
87
+ { style }
88
+ )}
84
89
  {#if child}
85
- {@render child({ props: mergedProps, ...contentState.snippetProps })}
90
+ {@render child({ props: finalProps, ...contentState.snippetProps })}
86
91
  {:else}
87
- <div {...mergedProps}>
92
+ <div {...finalProps}>
88
93
  {@render children?.()}
89
94
  </div>
90
95
  {/if}
@@ -27,6 +27,7 @@
27
27
  onInteractOutside = noop,
28
28
  onEscapeKeydown = noop,
29
29
  forceMount = false,
30
+ style,
30
31
  ...restProps
31
32
  }: LinkPreviewContentProps = $props();
32
33
 
@@ -68,14 +69,16 @@
68
69
  shouldRender={contentState.shouldRender}
69
70
  >
70
71
  {#snippet popper({ props, wrapperProps })}
71
- {@const mergedProps = mergeProps(props, {
72
- style: getFloatingContentCSSVars("link-preview"),
73
- })}
72
+ {@const finalProps = mergeProps(
73
+ props,
74
+ { style: getFloatingContentCSSVars("link-preview") },
75
+ { style }
76
+ )}
74
77
  {#if child}
75
- {@render child({ props: mergedProps, wrapperProps, ...contentState.snippetProps })}
78
+ {@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
76
79
  {:else}
77
80
  <div {...wrapperProps}>
78
- <div {...mergedProps}>
81
+ <div {...finalProps}>
79
82
  {@render children?.()}
80
83
  </div>
81
84
  </div>
@@ -97,14 +100,16 @@
97
100
  shouldRender={contentState.shouldRender}
98
101
  >
99
102
  {#snippet popper({ props, wrapperProps })}
100
- {@const mergedProps = mergeProps(props, {
101
- style: getFloatingContentCSSVars("link-preview"),
102
- })}
103
+ {@const finalProps = mergeProps(
104
+ props,
105
+ { style: getFloatingContentCSSVars("link-preview") },
106
+ { style }
107
+ )}
103
108
  {#if child}
104
- {@render child({ props: mergedProps, wrapperProps, ...contentState.snippetProps })}
109
+ {@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
105
110
  {:else}
106
111
  <div {...wrapperProps}>
107
- <div {...mergedProps}>
112
+ <div {...finalProps}>
108
113
  {@render children?.()}
109
114
  </div>
110
115
  </div>
@@ -20,6 +20,7 @@
20
20
  onEscapeKeydown = noop,
21
21
  onCloseAutoFocus: onCloseAutoFocusProp = noop,
22
22
  forceMount = false,
23
+ style,
23
24
  ...restProps
24
25
  }: MenuContentStaticProps = $props();
25
26
 
@@ -68,12 +69,11 @@
68
69
  shouldRender={contentState.shouldRender}
69
70
  >
70
71
  {#snippet popper({ props })}
71
- {@const finalProps = mergeProps(props, {
72
- style: {
73
- outline: "none",
74
- ...getFloatingContentCSSVars("menu"),
75
- },
76
- })}
72
+ {@const finalProps = mergeProps(
73
+ props,
74
+ { style: { outline: "none", ...getFloatingContentCSSVars("menu") } },
75
+ { style }
76
+ )}
77
77
  {#if child}
78
78
  {@render child({ props: finalProps, ...contentState.snippetProps })}
79
79
  {:else}
@@ -99,12 +99,11 @@
99
99
  shouldRender={contentState.shouldRender}
100
100
  >
101
101
  {#snippet popper({ props })}
102
- {@const finalProps = mergeProps(props, {
103
- style: {
104
- outline: "none",
105
- ...getFloatingContentCSSVars("menu"),
106
- },
107
- })}
102
+ {@const finalProps = mergeProps(
103
+ props,
104
+ { style: { outline: "none", ...getFloatingContentCSSVars("menu") } },
105
+ { style }
106
+ )}
108
107
  {#if child}
109
108
  {@render child({ props: finalProps, ...contentState.snippetProps })}
110
109
  {:else}
@@ -20,6 +20,7 @@
20
20
  onEscapeKeydown = noop,
21
21
  onCloseAutoFocus: onCloseAutoFocusProp = noop,
22
22
  forceMount = false,
23
+ style,
23
24
  ...restProps
24
25
  }: MenuContentProps = $props();
25
26
 
@@ -72,12 +73,11 @@
72
73
  shouldRender={contentState.shouldRender}
73
74
  >
74
75
  {#snippet popper({ props, wrapperProps })}
75
- {@const finalProps = mergeProps(props, {
76
- style: {
77
- outline: "none",
78
- ...getFloatingContentCSSVars("menu"),
79
- },
80
- })}
76
+ {@const finalProps = mergeProps(
77
+ props,
78
+ { style: { outline: "none", ...getFloatingContentCSSVars("menu") } },
79
+ { style }
80
+ )}
81
81
  {#if child}
82
82
  {@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
83
83
  {:else}
@@ -104,12 +104,11 @@
104
104
  shouldRender={contentState.shouldRender}
105
105
  >
106
106
  {#snippet popper({ props, wrapperProps })}
107
- {@const finalProps = mergeProps(props, {
108
- style: {
109
- outline: "none",
110
- ...getFloatingContentCSSVars("menu"),
111
- },
112
- })}
107
+ {@const finalProps = mergeProps(
108
+ props,
109
+ { style: { outline: "none", ...getFloatingContentCSSVars("menu") } },
110
+ { style }
111
+ )}
113
112
  {#if child}
114
113
  {@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
115
114
  {:else}
@@ -27,6 +27,7 @@
27
27
  onCloseAutoFocus: onCloseAutoFocusProp = noop,
28
28
  onFocusOutside = noop,
29
29
  trapFocus = false,
30
+ style,
30
31
  ...restProps
31
32
  }: MenuSubContentStaticProps = $props();
32
33
 
@@ -123,9 +124,12 @@
123
124
  shouldRender={subContentState.shouldRender}
124
125
  >
125
126
  {#snippet popper({ props })}
126
- {@const finalProps = mergeProps(props, mergedProps, {
127
- style: getFloatingContentCSSVars("menu"),
128
- })}
127
+ {@const finalProps = mergeProps(
128
+ props,
129
+ mergedProps,
130
+ { style: getFloatingContentCSSVars("menu") },
131
+ { style }
132
+ )}
129
133
  {#if child}
130
134
  {@render child({ props: finalProps, ...subContentState.snippetProps })}
131
135
  {:else}
@@ -154,9 +158,12 @@
154
158
  shouldRender={subContentState.shouldRender}
155
159
  >
156
160
  {#snippet popper({ props })}
157
- {@const finalProps = mergeProps(props, mergedProps, {
158
- style: getFloatingContentCSSVars("menu"),
159
- })}
161
+ {@const finalProps = mergeProps(
162
+ props,
163
+ mergedProps,
164
+ { style: getFloatingContentCSSVars("menu") },
165
+ { style }
166
+ )}
160
167
  {#if child}
161
168
  {@render child({ props: finalProps, ...subContentState.snippetProps })}
162
169
  {:else}
@@ -28,6 +28,7 @@
28
28
  onFocusOutside = noop,
29
29
  side = "right",
30
30
  trapFocus = false,
31
+ style,
31
32
  ...restProps
32
33
  }: MenuSubContentProps = $props();
33
34
 
@@ -124,9 +125,12 @@
124
125
  shouldRender={subContentState.shouldRender}
125
126
  >
126
127
  {#snippet popper({ props, wrapperProps })}
127
- {@const finalProps = mergeProps(props, mergedProps, {
128
- style: getFloatingContentCSSVars("menu"),
129
- })}
128
+ {@const finalProps = mergeProps(
129
+ props,
130
+ mergedProps,
131
+ { style: getFloatingContentCSSVars("menu") },
132
+ { style }
133
+ )}
130
134
  {#if child}
131
135
  {@render child({
132
136
  props: finalProps,
@@ -160,9 +164,12 @@
160
164
  shouldRender={subContentState.shouldRender}
161
165
  >
162
166
  {#snippet popper({ props, wrapperProps })}
163
- {@const finalProps = mergeProps(props, mergedProps, {
164
- style: getFloatingContentCSSVars("menu"),
165
- })}
167
+ {@const finalProps = mergeProps(
168
+ props,
169
+ mergedProps,
170
+ { style: getFloatingContentCSSVars("menu") },
171
+ { style }
172
+ )}
166
173
  {#if child}
167
174
  {@render child({
168
175
  props: finalProps,
@@ -89,6 +89,7 @@ export declare class MenuContentState {
89
89
  readonly dir: Direction;
90
90
  readonly style: {
91
91
  readonly pointerEvents: "auto";
92
+ readonly contain: "layout style paint";
92
93
  };
93
94
  };
94
95
  readonly popperProps: {
@@ -324,6 +324,8 @@ export class MenuContentState {
324
324
  dir: this.parentMenu.root.opts.dir.current,
325
325
  style: {
326
326
  pointerEvents: "auto",
327
+ // CSS containment isolates style/layout/paint calculations from the rest of the page
328
+ contain: "layout style paint",
327
329
  },
328
330
  ...this.attachment,
329
331
  }));
@@ -249,13 +249,13 @@ export class PinInputRootState {
249
249
  else if (maxLength > 1 && val.length > 1) {
250
250
  let offset = 0;
251
251
  if (prev[0] !== null && prev[1] !== null) {
252
- direction = c < prev[0] ? "backward" : "forward";
252
+ direction = c < prev[1] ? "backward" : "forward";
253
253
  const wasPreviouslyInserting = prev[0] === prev[1] && prev[0] < maxLength;
254
254
  if (direction === "backward" && !wasPreviouslyInserting) {
255
255
  offset = -1;
256
256
  }
257
257
  }
258
- start = offset - c;
258
+ start = offset + c;
259
259
  end = offset + c + 1;
260
260
  }
261
261
  }
@@ -21,6 +21,7 @@
21
21
  onInteractOutside = noop,
22
22
  trapFocus = true,
23
23
  preventScroll = false,
24
+ style,
24
25
  ...restProps
25
26
  }: PopoverContentStaticProps = $props();
26
27
 
@@ -54,9 +55,13 @@
54
55
  shouldRender={contentState.shouldRender}
55
56
  >
56
57
  {#snippet popper({ props })}
57
- {@const finalProps = mergeProps(props, {
58
- style: getFloatingContentCSSVars("popover"),
59
- })}
58
+ {@const finalProps = mergeProps(
59
+ props,
60
+ {
61
+ style: getFloatingContentCSSVars("popover"),
62
+ },
63
+ { style }
64
+ )}
60
65
  {#if child}
61
66
  {@render child({ props: finalProps, ...contentState.snippetProps })}
62
67
  {:else}
@@ -82,9 +87,11 @@
82
87
  shouldRender={contentState.shouldRender}
83
88
  >
84
89
  {#snippet popper({ props })}
85
- {@const finalProps = mergeProps(props, {
86
- style: getFloatingContentCSSVars("popover"),
87
- })}
90
+ {@const finalProps = mergeProps(
91
+ props,
92
+ { style: getFloatingContentCSSVars("popover") },
93
+ { style }
94
+ )}
88
95
  {#if child}
89
96
  {@render child({ props: finalProps, ...contentState.snippetProps })}
90
97
  {:else}
@@ -23,6 +23,7 @@
23
23
  trapFocus = true,
24
24
  preventScroll = false,
25
25
  customAnchor = null,
26
+ style,
26
27
  ...restProps
27
28
  }: PopoverContentProps = $props();
28
29
 
@@ -68,9 +69,11 @@
68
69
  shouldRender={contentState.shouldRender}
69
70
  >
70
71
  {#snippet popper({ props, wrapperProps })}
71
- {@const finalProps = mergeProps(props, {
72
- style: getFloatingContentCSSVars("popover"),
73
- })}
72
+ {@const finalProps = mergeProps(
73
+ props,
74
+ { style: getFloatingContentCSSVars("popover") },
75
+ { style }
76
+ )}
74
77
  {#if child}
75
78
  {@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
76
79
  {:else}
@@ -99,9 +102,11 @@
99
102
  shouldRender={contentState.shouldRender}
100
103
  >
101
104
  {#snippet popper({ props, wrapperProps })}
102
- {@const finalProps = mergeProps(props, {
103
- style: getFloatingContentCSSVars("popover"),
104
- })}
105
+ {@const finalProps = mergeProps(
106
+ props,
107
+ { style: getFloatingContentCSSVars("popover") },
108
+ { style }
109
+ )}
105
110
  {#if child}
106
111
  {@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
107
112
  {:else}
@@ -19,6 +19,7 @@ export declare class PopoverRootState {
19
19
  overlayPresence: PresenceManager;
20
20
  openedViaHover: boolean;
21
21
  hasInteractedWithContent: boolean;
22
+ hoverCooldown: boolean;
22
23
  closeDelay: number;
23
24
  constructor(opts: PopoverRootStateOpts);
24
25
  setDomContext(ctx: DOMContext): void;
@@ -91,6 +92,7 @@ export declare class PopoverContentState {
91
92
  readonly "data-state": "open" | "closed";
92
93
  readonly style: {
93
94
  readonly pointerEvents: "auto";
95
+ readonly contain: "layout style paint";
94
96
  };
95
97
  readonly onpointerdown: (_: BitsPointerEvent) => void;
96
98
  readonly onfocusin: (e: BitsFocusEvent) => void;
@@ -24,6 +24,7 @@ export class PopoverRootState {
24
24
  // hover tracking state
25
25
  openedViaHover = $state(false);
26
26
  hasInteractedWithContent = $state(false);
27
+ hoverCooldown = $state(false);
27
28
  closeDelay = $state(0);
28
29
  #closeTimeout = null;
29
30
  #domContext = null;
@@ -159,7 +160,7 @@ export class PopoverTriggerState {
159
160
  this.#isHovering = true;
160
161
  this.#clearCloseTimeout();
161
162
  this.root.cancelDelayedClose();
162
- if (this.root.opts.open.current)
163
+ if (this.root.opts.open.current || this.root.hoverCooldown)
163
164
  return;
164
165
  const delay = this.opts.openDelay.current;
165
166
  if (delay <= 0) {
@@ -181,6 +182,7 @@ export class PopoverTriggerState {
181
182
  return;
182
183
  this.#isHovering = false;
183
184
  this.#clearOpenTimeout();
185
+ this.root.hoverCooldown = false;
184
186
  // let GraceArea handle the close - it will call handleHoverClose via onPointerExit
185
187
  // we just need to stop any pending open timer
186
188
  }
@@ -196,6 +198,15 @@ export class PopoverTriggerState {
196
198
  this.root.hasInteractedWithContent = true;
197
199
  return;
198
200
  }
201
+ // if closing while hovering with openOnHover enabled, set cooldown to prevent
202
+ // immediate re-open via hover
203
+ if (this.#isHovering && this.opts.openOnHover.current && this.root.opts.open.current) {
204
+ this.root.hoverCooldown = true;
205
+ }
206
+ // if clicking to open while in cooldown, reset cooldown (explicit open)
207
+ if (this.root.hoverCooldown && !this.root.opts.open.current) {
208
+ this.root.hoverCooldown = false;
209
+ }
199
210
  this.root.toggleOpen();
200
211
  }
201
212
  onkeydown(e) {
@@ -317,6 +328,8 @@ export class PopoverContentState {
317
328
  [popoverAttrs.content]: "",
318
329
  style: {
319
330
  pointerEvents: "auto",
331
+ // CSS containment isolates style/layout/paint calculations from the rest of the page
332
+ contain: "layout style paint",
320
333
  },
321
334
  onpointerdown: this.onpointerdown,
322
335
  onfocusin: this.onfocusin,
@@ -265,6 +265,7 @@ export declare class ScrollAreaScrollbarYState implements ScrollbarAxisState {
265
265
  }
266
266
  type ScrollbarAxis = ScrollAreaScrollbarXState | ScrollAreaScrollbarYState;
267
267
  export declare class ScrollAreaScrollbarSharedState {
268
+ #private;
268
269
  static create(): ScrollAreaScrollbarSharedState;
269
270
  readonly scrollbarState: ScrollbarAxis;
270
271
  readonly root: ScrollAreaRootState;
@@ -286,6 +287,7 @@ export declare class ScrollAreaScrollbarSharedState {
286
287
  onpointerdown(e: BitsPointerEvent): void;
287
288
  onpointermove(e: BitsPointerEvent): void;
288
289
  onpointerup(e: BitsPointerEvent): void;
290
+ onlostpointercapture(_: BitsPointerEvent): void;
289
291
  readonly props: never;
290
292
  }
291
293
  interface ScrollAreaThumbImplStateOpts extends WithRefOpts, ReadableBoxedValues<{
@@ -571,6 +571,7 @@ export class ScrollAreaScrollbarSharedState {
571
571
  this.onpointerdown = this.onpointerdown.bind(this);
572
572
  this.onpointermove = this.onpointermove.bind(this);
573
573
  this.onpointerup = this.onpointerup.bind(this);
574
+ this.onlostpointercapture = this.onlostpointercapture.bind(this);
574
575
  }
575
576
  handleDragScroll(e) {
576
577
  if (!this.rect)
@@ -579,6 +580,14 @@ export class ScrollAreaScrollbarSharedState {
579
580
  const y = e.clientY - this.rect.top;
580
581
  this.scrollbarState.onDragScroll({ x, y });
581
582
  }
583
+ #cleanupPointerState() {
584
+ if (this.rect === null)
585
+ return;
586
+ this.root.domContext.getDocument().body.style.webkitUserSelect = this.prevWebkitUserSelect;
587
+ if (this.root.viewportNode)
588
+ this.root.viewportNode.style.scrollBehavior = "";
589
+ this.rect = null;
590
+ }
582
591
  onpointerdown(e) {
583
592
  if (e.button !== 0)
584
593
  return;
@@ -601,10 +610,10 @@ export class ScrollAreaScrollbarSharedState {
601
610
  if (target.hasPointerCapture(e.pointerId)) {
602
611
  target.releasePointerCapture(e.pointerId);
603
612
  }
604
- this.root.domContext.getDocument().body.style.webkitUserSelect = this.prevWebkitUserSelect;
605
- if (this.root.viewportNode)
606
- this.root.viewportNode.style.scrollBehavior = "";
607
- this.rect = null;
613
+ this.#cleanupPointerState();
614
+ }
615
+ onlostpointercapture(_) {
616
+ this.#cleanupPointerState();
608
617
  }
609
618
  props = $derived.by(() => mergeProps({
610
619
  ...this.scrollbarState.props,
@@ -616,6 +625,7 @@ export class ScrollAreaScrollbarSharedState {
616
625
  onpointerdown: this.onpointerdown,
617
626
  onpointermove: this.onpointermove,
618
627
  onpointerup: this.onpointerup,
628
+ onlostpointercapture: this.onlostpointercapture,
619
629
  }));
620
630
  }
621
631
  export class ScrollAreaThumbImplState {
@@ -18,6 +18,7 @@
18
18
  children,
19
19
  child,
20
20
  preventScroll = false,
21
+ style,
21
22
  ...restProps
22
23
  }: SelectContentStaticProps = $props();
23
24
 
@@ -47,7 +48,7 @@
47
48
  shouldRender={contentState.shouldRender}
48
49
  >
49
50
  {#snippet popper({ props })}
50
- {@const finalProps = mergeProps(props, { style: contentState.props.style })}
51
+ {@const finalProps = mergeProps(props, { style: contentState.props.style }, { style })}
51
52
  {#if child}
52
53
  {@render child({ props: finalProps, ...contentState.snippetProps })}
53
54
  {:else}
@@ -70,7 +71,7 @@
70
71
  shouldRender={contentState.shouldRender}
71
72
  >
72
73
  {#snippet popper({ props })}
73
- {@const finalProps = mergeProps(props, { style: contentState.props.style })}
74
+ {@const finalProps = mergeProps(props, { style: contentState.props.style }, { style })}
74
75
  {#if child}
75
76
  {@render child({ props: finalProps, ...contentState.snippetProps })}
76
77
  {:else}
@@ -19,6 +19,7 @@
19
19
  children,
20
20
  child,
21
21
  preventScroll = false,
22
+ style,
22
23
  ...restProps
23
24
  }: SelectContentProps = $props();
24
25
 
@@ -48,7 +49,7 @@
48
49
  shouldRender={contentState.shouldRender}
49
50
  >
50
51
  {#snippet popper({ props, wrapperProps })}
51
- {@const finalProps = mergeProps(props, { style: contentState.props.style })}
52
+ {@const finalProps = mergeProps(props, { style: contentState.props.style }, { style })}
52
53
  {#if child}
53
54
  {@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
54
55
  {:else}
@@ -73,7 +74,7 @@
73
74
  shouldRender={contentState.shouldRender}
74
75
  >
75
76
  {#snippet popper({ props, wrapperProps })}
76
- {@const finalProps = mergeProps(props, { style: contentState.props.style })}
77
+ {@const finalProps = mergeProps(props, { style: contentState.props.style }, { style })}
77
78
  {#if child}
78
79
  {@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
79
80
  {:else}
@@ -18,6 +18,7 @@
18
18
  onInteractOutside = noop,
19
19
  onEscapeKeydown = noop,
20
20
  forceMount = false,
21
+ style,
21
22
  ...restProps
22
23
  }: TooltipContentStaticProps = $props();
23
24
 
@@ -50,13 +51,15 @@
50
51
  shouldRender={contentState.shouldRender}
51
52
  >
52
53
  {#snippet popper({ props })}
53
- {@const mergedProps = mergeProps(props, {
54
- style: getFloatingContentCSSVars("tooltip"),
55
- })}
54
+ {@const finalProps = mergeProps(
55
+ props,
56
+ { style: getFloatingContentCSSVars("tooltip") },
57
+ { style }
58
+ )}
56
59
  {#if child}
57
- {@render child({ props: mergedProps, ...contentState.snippetProps })}
60
+ {@render child({ props: finalProps, ...contentState.snippetProps })}
58
61
  {:else}
59
- <div {...mergedProps}>
62
+ <div {...finalProps}>
60
63
  {@render children?.()}
61
64
  </div>
62
65
  {/if}
@@ -78,13 +81,15 @@
78
81
  shouldRender={contentState.shouldRender}
79
82
  >
80
83
  {#snippet popper({ props })}
81
- {@const mergedProps = mergeProps(props, {
82
- style: getFloatingContentCSSVars("tooltip"),
83
- })}
84
+ {@const finalProps = mergeProps(
85
+ props,
86
+ { style: getFloatingContentCSSVars("tooltip") },
87
+ { style }
88
+ )}
84
89
  {#if child}
85
- {@render child({ props: mergedProps, ...contentState.snippetProps })}
90
+ {@render child({ props: finalProps, ...contentState.snippetProps })}
86
91
  {:else}
87
- <div {...mergedProps}>
92
+ <div {...finalProps}>
88
93
  {@render children?.()}
89
94
  </div>
90
95
  {/if}
@@ -27,6 +27,7 @@
27
27
  onInteractOutside = noop,
28
28
  onEscapeKeydown = noop,
29
29
  forceMount = false,
30
+ style,
30
31
  ...restProps
31
32
  }: TooltipContentProps = $props();
32
33
 
@@ -71,14 +72,16 @@
71
72
  contentPointerEvents={contentState.root.disableHoverableContent ? "none" : "auto"}
72
73
  >
73
74
  {#snippet popper({ props, wrapperProps })}
74
- {@const mergedProps = mergeProps(props, {
75
- style: getFloatingContentCSSVars("tooltip"),
76
- })}
75
+ {@const finalProps = mergeProps(
76
+ props,
77
+ { style: getFloatingContentCSSVars("tooltip") },
78
+ { style }
79
+ )}
77
80
  {#if child}
78
- {@render child({ props: mergedProps, wrapperProps, ...contentState.snippetProps })}
81
+ {@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
79
82
  {:else}
80
83
  <div {...wrapperProps}>
81
- <div {...mergedProps}>
84
+ <div {...finalProps}>
82
85
  {@render children?.()}
83
86
  </div>
84
87
  </div>
@@ -101,14 +104,16 @@
101
104
  contentPointerEvents={contentState.root.disableHoverableContent ? "none" : "auto"}
102
105
  >
103
106
  {#snippet popper({ props, wrapperProps })}
104
- {@const mergedProps = mergeProps(props, {
105
- style: getFloatingContentCSSVars("tooltip"),
106
- })}
107
+ {@const finalProps = mergeProps(
108
+ props,
109
+ { style: getFloatingContentCSSVars("tooltip") },
110
+ { style }
111
+ )}
107
112
  {#if child}
108
- {@render child({ props: mergedProps, wrapperProps, ...contentState.snippetProps })}
113
+ {@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
109
114
  {:else}
110
115
  <div {...wrapperProps}>
111
- <div {...mergedProps}>
116
+ <div {...finalProps}>
112
117
  {@render children?.()}
113
118
  </div>
114
119
  </div>
@@ -13,6 +13,7 @@
13
13
  id = createId(uid),
14
14
  disabled = false,
15
15
  type = "button",
16
+ tabindex = 0,
16
17
  ref = $bindable(null),
17
18
  ...restProps
18
19
  }: TooltipTriggerProps = $props();
@@ -20,6 +21,7 @@
20
21
  const triggerState = TooltipTriggerState.create({
21
22
  id: boxWith(() => id),
22
23
  disabled: boxWith(() => disabled ?? false),
24
+ tabindex: boxWith(() => tabindex ?? 0),
23
25
  ref: boxWith(
24
26
  () => ref,
25
27
  (v) => (ref = v)
@@ -56,6 +56,7 @@ export declare class TooltipRootState {
56
56
  }
57
57
  interface TooltipTriggerStateOpts extends WithRefOpts, ReadableBoxedValues<{
58
58
  disabled: boolean;
59
+ tabindex: number;
59
60
  }> {
60
61
  }
61
62
  export declare class TooltipTriggerState {
@@ -73,7 +74,7 @@ export declare class TooltipTriggerState {
73
74
  readonly "data-state": "closed" | "delayed-open" | "instant-open";
74
75
  readonly "data-disabled": "" | undefined;
75
76
  readonly "data-delay-duration": `${number}`;
76
- readonly tabindex: 0 | undefined;
77
+ readonly tabindex: number | undefined;
77
78
  readonly disabled: boolean;
78
79
  readonly onpointerup: PointerEventHandler<HTMLElement>;
79
80
  readonly onpointerdown: PointerEventHandler<HTMLElement>;
@@ -255,7 +255,7 @@ export class TooltipTriggerState {
255
255
  "data-disabled": boolToEmptyStrOrUndef(this.#isDisabled),
256
256
  "data-delay-duration": `${this.root.delayDuration}`,
257
257
  [tooltipAttrs.trigger]: "",
258
- tabindex: this.#isDisabled ? undefined : 0,
258
+ tabindex: this.#isDisabled ? undefined : this.opts.tabindex.current,
259
259
  disabled: this.opts.disabled.current,
260
260
  onpointerup: this.#onpointerup,
261
261
  onpointerdown: this.#onpointerdown,
@@ -127,7 +127,7 @@ export class FocusScope {
127
127
  if (!this.#manager.isActiveScope(this))
128
128
  return;
129
129
  const tabbables = this.#getTabbables();
130
- if (tabbables.length < 2)
130
+ if (tabbables.length === 0)
131
131
  return;
132
132
  const first = tabbables[0];
133
133
  const last = tabbables[tabbables.length - 1];
@@ -7,6 +7,14 @@ export interface PresenceOptions extends ReadableBoxedValues<{
7
7
  }> {
8
8
  }
9
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
+ }
10
18
  declare const presenceMachine: {
11
19
  readonly mounted: {
12
20
  readonly UNMOUNT: "unmounted";
@@ -24,7 +32,7 @@ type PresenceMachine = StateMachine<typeof presenceMachine>;
24
32
  export declare class Presence {
25
33
  readonly opts: PresenceOptions;
26
34
  prevAnimationNameState: string;
27
- styles: CSSStyleDeclaration;
35
+ styles: CachedStyles;
28
36
  initialStatus: PresenceStatus;
29
37
  previousPresent: Previous<boolean>;
30
38
  machine: PresenceMachine;
@@ -2,6 +2,12 @@ import { executeCallbacks } from "svelte-toolbelt";
2
2
  import { Previous, watch } from "runed";
3
3
  import { on } from "svelte/events";
4
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
5
11
  const presenceMachine = {
6
12
  mounted: {
7
13
  UNMOUNT: "unmounted",
@@ -18,7 +24,7 @@ const presenceMachine = {
18
24
  export class Presence {
19
25
  opts;
20
26
  prevAnimationNameState = $state("none");
21
- styles = $state({});
27
+ styles = $state({ display: "", animationName: "none" });
22
28
  initialStatus;
23
29
  previousPresent;
24
30
  machine;
@@ -43,7 +49,8 @@ export class Presence {
43
49
  handleAnimationEnd(event) {
44
50
  if (!this.opts.ref.current)
45
51
  return;
46
- const currAnimationName = getAnimationName(this.opts.ref.current);
52
+ // Use cached animation name from styles when available to avoid getComputedStyle
53
+ const currAnimationName = this.styles.animationName || getAnimationName(this.opts.ref.current);
47
54
  const isCurrentAnimation = currAnimationName.includes(event.animationName) || currAnimationName === "none";
48
55
  if (event.target === this.opts.ref.current && isCurrentAnimation) {
49
56
  this.machine.dispatch("ANIMATION_END");
@@ -53,7 +60,11 @@ export class Presence {
53
60
  if (!this.opts.ref.current)
54
61
  return;
55
62
  if (event.target === this.opts.ref.current) {
56
- this.prevAnimationNameState = getAnimationName(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;
57
68
  }
58
69
  }
59
70
  isPresent = $derived.by(() => {
@@ -68,7 +79,10 @@ function watchPresenceChange(state) {
68
79
  if (!hasPresentChanged)
69
80
  return;
70
81
  const prevAnimationName = state.prevAnimationNameState;
71
- const currAnimationName = getAnimationName(state.opts.ref.current);
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;
72
86
  if (state.present.current) {
73
87
  state.machine.dispatch("MOUNT");
74
88
  }
@@ -98,19 +112,47 @@ function watchStatusChange(state) {
98
112
  watch(() => state.machine.state.current, () => {
99
113
  if (!state.opts.ref.current)
100
114
  return;
101
- const currAnimationName = getAnimationName(state.opts.ref.current);
102
- state.prevAnimationNameState =
103
- state.machine.state.current === "mounted" ? currAnimationName : "none";
115
+ // Use cached animation name first, only force refresh if needed for mounted state
116
+ const currAnimationName = state.machine.state.current === "mounted"
117
+ ? getAnimationName(state.opts.ref.current, true)
118
+ : "none";
119
+ state.prevAnimationNameState = currAnimationName;
120
+ // Update styles cache
121
+ state.styles.animationName = currAnimationName;
104
122
  });
105
123
  }
106
124
  function watchRefChange(state) {
107
125
  watch(() => state.opts.ref.current, () => {
108
126
  if (!state.opts.ref.current)
109
127
  return;
110
- state.styles = getComputedStyle(state.opts.ref.current);
128
+ // Snapshot only needed style properties instead of storing live CSSStyleDeclaration
129
+ // This avoids triggering style recalculations when accessing the cached object
130
+ const computed = getComputedStyle(state.opts.ref.current);
131
+ state.styles = {
132
+ display: computed.display,
133
+ animationName: computed.animationName || "none",
134
+ };
111
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));
112
136
  });
113
137
  }
114
- function getAnimationName(node) {
115
- return node ? getComputedStyle(node).animationName || "none" : "none";
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;
153
+ }
154
+ // Compute and cache the new value
155
+ const value = getComputedStyle(node).animationName || "none";
156
+ animationNameCache.set(node, { value, timestamp: now });
157
+ return value;
116
158
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "2.15.3",
3
+ "version": "2.15.5",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "devDependencies": {
22
22
  "@internationalized/date": "^3.8.2",
23
- "@sveltejs/kit": "^2.42.0",
23
+ "@sveltejs/kit": "2.49.5",
24
24
  "@sveltejs/package": "2.5.0",
25
25
  "@sveltejs/vite-plugin-svelte": "^6.2.0",
26
26
  "@types/node": "^20.19.16",
@@ -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.1",
31
+ "svelte": "5.46.4",
32
32
  "svelte-check": "^4.3.1",
33
33
  "typescript": "^5.9.2",
34
34
  "vite": "^7.1.5",