bits-ui 1.6.0 → 1.6.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.
@@ -5,6 +5,7 @@ import { kbd } from "../../internal/kbd.js";
5
5
  import { getAriaDisabled, getAriaExpanded, getAriaSelected, getDataDisabled, getDataSelected, } from "../../internal/attrs.js";
6
6
  import { getFirstNonCommentChild } from "../../internal/dom.js";
7
7
  import { computeCommandScore } from "./index.js";
8
+ import cssesc from "css.escape";
8
9
  // attributes
9
10
  const COMMAND_ROOT_ATTR = "data-command-root";
10
11
  const COMMAND_LIST_ATTR = "data-command-list";
@@ -178,7 +179,7 @@ class CommandRootState {
178
179
  }
179
180
  const sortedGroups = groups.sort((a, b) => b[1] - a[1]);
180
181
  for (const group of sortedGroups) {
181
- const element = listInsertionElement?.querySelector(`${COMMAND_GROUP_SELECTOR}[${COMMAND_VALUE_ATTR}="${encodeURIComponent(group[0])}"]`);
182
+ const element = listInsertionElement?.querySelector(`${COMMAND_GROUP_SELECTOR}[${COMMAND_VALUE_ATTR}="${cssesc(group[0])}"]`);
182
183
  element?.parentElement?.appendChild(element);
183
184
  }
184
185
  this.#selectFirstItem();
@@ -683,7 +684,7 @@ class CommandInputState {
683
684
  opts;
684
685
  root;
685
686
  #selectedItemId = $derived.by(() => {
686
- const item = this.root.viewportNode?.querySelector(`${COMMAND_ITEM_SELECTOR}[${COMMAND_VALUE_ATTR}="${encodeURIComponent(this.root.opts.value.current)}"]`);
687
+ const item = this.root.viewportNode?.querySelector(`${COMMAND_ITEM_SELECTOR}[${COMMAND_VALUE_ATTR}="${cssesc(this.root.opts.value.current)}"]`);
687
688
  if (!item)
688
689
  return;
689
690
  return item?.getAttribute("id") ?? undefined;
@@ -1,7 +1,6 @@
1
1
  import type { ReadableBoxedValues, WritableBoxedValues } from "../../internal/box.svelte.js";
2
2
  import type { WithRefProps } from "../../internal/types.js";
3
- import { CustomEventDispatcher } from "../../internal/events.js";
4
- export declare const TooltipOpenEvent: CustomEventDispatcher<unknown>;
3
+ import type { PointerEventHandler } from "svelte/elements";
5
4
  type TooltipProviderStateProps = ReadableBoxedValues<{
6
5
  delayDuration: number;
7
6
  disableHoverableContent: boolean;
@@ -16,8 +15,9 @@ declare class TooltipProviderState {
16
15
  isOpenDelayed: boolean;
17
16
  isPointerInTransit: import("svelte-toolbelt").WritableBox<boolean>;
18
17
  constructor(opts: TooltipProviderStateProps);
19
- onOpen: () => void;
20
- onClose: () => void;
18
+ onOpen: (tooltip: TooltipRootState) => void;
19
+ onClose: (tooltip: TooltipRootState) => void;
20
+ isTooltipOpen: (tooltip: TooltipRootState) => boolean;
21
21
  }
22
22
  type TooltipRootStateProps = ReadableBoxedValues<{
23
23
  delayDuration: number | undefined;
@@ -66,7 +66,7 @@ declare class TooltipTriggerState {
66
66
  disabled: boolean;
67
67
  onpointerup: () => void;
68
68
  onpointerdown: () => void;
69
- onpointermove: (e: PointerEvent) => void;
69
+ onpointermove: PointerEventHandler<HTMLElement>;
70
70
  onpointerleave: () => void;
71
71
  onfocus: (e: FocusEvent & {
72
72
  currentTarget: HTMLElement;
@@ -1,22 +1,18 @@
1
- import { box, executeCallbacks, onMountEffect, useRefById } from "svelte-toolbelt";
1
+ import { box, onMountEffect, useRefById } from "svelte-toolbelt";
2
2
  import { on } from "svelte/events";
3
3
  import { Context, watch } from "runed";
4
4
  import { useTimeoutFn } from "../../internal/use-timeout-fn.svelte.js";
5
5
  import { isElement, isFocusVisible } from "../../internal/is.js";
6
6
  import { useGraceArea } from "../../internal/use-grace-area.svelte.js";
7
7
  import { getDataDisabled } from "../../internal/attrs.js";
8
- import { CustomEventDispatcher } from "../../internal/events.js";
9
8
  const TOOLTIP_CONTENT_ATTR = "data-tooltip-content";
10
9
  const TOOLTIP_TRIGGER_ATTR = "data-tooltip-trigger";
11
- export const TooltipOpenEvent = new CustomEventDispatcher("bits.tooltip.open", {
12
- bubbles: false,
13
- cancelable: false,
14
- });
15
10
  class TooltipProviderState {
16
11
  opts;
17
12
  isOpenDelayed = $state(true);
18
13
  isPointerInTransit = box(false);
19
14
  #timerFn;
15
+ #openTooltip = $state(null);
20
16
  constructor(opts) {
21
17
  this.opts = opts;
22
18
  this.#timerFn = useTimeoutFn(() => {
@@ -24,18 +20,34 @@ class TooltipProviderState {
24
20
  }, this.opts.skipDelayDuration.current, { immediate: false });
25
21
  }
26
22
  #startTimer = () => {
27
- this.#timerFn.start();
23
+ const skipDuration = this.opts.skipDelayDuration.current;
24
+ if (skipDuration === 0) {
25
+ return;
26
+ }
27
+ else {
28
+ this.#timerFn.start();
29
+ }
28
30
  };
29
31
  #clearTimer = () => {
30
32
  this.#timerFn.stop();
31
33
  };
32
- onOpen = () => {
34
+ onOpen = (tooltip) => {
35
+ if (this.#openTooltip && this.#openTooltip !== tooltip) {
36
+ this.#openTooltip.handleClose();
37
+ }
33
38
  this.#clearTimer();
34
39
  this.isOpenDelayed = false;
40
+ this.#openTooltip = tooltip;
35
41
  };
36
- onClose = () => {
42
+ onClose = (tooltip) => {
43
+ if (this.#openTooltip === tooltip) {
44
+ this.#openTooltip = null;
45
+ }
37
46
  this.#startTimer();
38
47
  };
48
+ isTooltipOpen = (tooltip) => {
49
+ return this.#openTooltip === tooltip;
50
+ };
39
51
  }
40
52
  class TooltipRootState {
41
53
  opts;
@@ -73,14 +85,11 @@ class TooltipRootState {
73
85
  }, this.delayDuration, { immediate: false });
74
86
  });
75
87
  watch(() => this.opts.open.current, (isOpen) => {
76
- if (!this.provider.onClose)
77
- return;
78
88
  if (isOpen) {
79
- this.provider.onOpen();
80
- TooltipOpenEvent.dispatch(document);
89
+ this.provider.onOpen(this);
81
90
  }
82
91
  else {
83
- this.provider.onClose();
92
+ this.provider.onClose(this);
84
93
  }
85
94
  });
86
95
  }
@@ -94,7 +103,19 @@ class TooltipRootState {
94
103
  this.opts.open.current = false;
95
104
  };
96
105
  #handleDelayedOpen = () => {
97
- this.#timerFn.start();
106
+ this.#timerFn.stop();
107
+ const shouldSkipDelay = !this.provider.isOpenDelayed;
108
+ const delayDuration = this.delayDuration ?? 0;
109
+ // if no delay needed (either skip delay active or delay is 0), open immediately
110
+ if (shouldSkipDelay || delayDuration === 0) {
111
+ // set wasOpenDelayed based on whether we actually had a delay
112
+ this.#wasOpenDelayed = delayDuration > 0 && shouldSkipDelay;
113
+ this.opts.open.current = true;
114
+ }
115
+ else {
116
+ // use timer for actual delays
117
+ this.#timerFn.start();
118
+ }
98
119
  };
99
120
  onTriggerEnter = () => {
100
121
  this.#handleDelayedOpen();
@@ -145,7 +166,9 @@ class TooltipTriggerState {
145
166
  return;
146
167
  if (e.pointerType === "touch")
147
168
  return;
148
- if (this.#hasPointerMoveOpened || this.root.provider.isPointerInTransit.current)
169
+ if (this.#hasPointerMoveOpened)
170
+ return;
171
+ if (this.root.provider.isPointerInTransit.current)
149
172
  return;
150
173
  this.root.onTriggerEnter();
151
174
  this.#hasPointerMoveOpened = true;
@@ -209,20 +232,23 @@ class TooltipContentState {
209
232
  contentNode: () => this.root.contentNode,
210
233
  enabled: () => this.root.opts.open.current && !this.root.disableHoverableContent,
211
234
  onPointerExit: () => {
212
- this.root.handleClose();
235
+ if (this.root.provider.isTooltipOpen(this.root)) {
236
+ this.root.handleClose();
237
+ }
213
238
  },
214
239
  setIsPointerInTransit: (value) => {
215
240
  this.root.provider.isPointerInTransit.current = value;
216
241
  },
242
+ transitTimeout: this.root.provider.opts.skipDelayDuration.current,
217
243
  });
218
- onMountEffect(() => executeCallbacks(on(window, "scroll", (e) => {
244
+ onMountEffect(() => on(window, "scroll", (e) => {
219
245
  const target = e.target;
220
246
  if (!target)
221
247
  return;
222
248
  if (target.contains(this.root.triggerNode)) {
223
249
  this.root.handleClose();
224
250
  }
225
- }), TooltipOpenEvent.listen(window, this.root.handleClose)));
251
+ }));
226
252
  }
227
253
  onInteractOutside = (e) => {
228
254
  if (isElement(e.target) &&
@@ -5,6 +5,7 @@ interface UseGraceAreaOpts {
5
5
  contentNode: Getter<HTMLElement | null>;
6
6
  onPointerExit: () => void;
7
7
  setIsPointerInTransit?: (value: boolean) => void;
8
+ transitTimeout?: number;
8
9
  }
9
10
  export declare function useGraceArea(opts: UseGraceAreaOpts): {
10
11
  isPointerInTransit: import("svelte-toolbelt").WritableBox<boolean>;
@@ -5,7 +5,7 @@ import { boxAutoReset } from "./box-auto-reset.svelte.js";
5
5
  import { isElement, isHTMLElement } from "./is.js";
6
6
  export function useGraceArea(opts) {
7
7
  const enabled = $derived(opts.enabled());
8
- const isPointerInTransit = boxAutoReset(false, 300, (value) => {
8
+ const isPointerInTransit = boxAutoReset(false, opts.transitTimeout ?? 300, (value) => {
9
9
  if (enabled) {
10
10
  opts.setIsPointerInTransit?.(value);
11
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",
@@ -21,6 +21,7 @@
21
21
  "@sveltejs/kit": "^2.16.1",
22
22
  "@sveltejs/package": "^2.3.9",
23
23
  "@sveltejs/vite-plugin-svelte": "4.0.0",
24
+ "@types/css.escape": "^1.5.2",
24
25
  "@types/node": "^20.17.6",
25
26
  "@types/resize-observer-browser": "^0.1.11",
26
27
  "csstype": "^3.1.3",
@@ -41,6 +42,7 @@
41
42
  "@floating-ui/core": "^1.6.4",
42
43
  "@floating-ui/dom": "^1.6.7",
43
44
  "@internationalized/date": "^3.5.6",
45
+ "css.escape": "^1.5.1",
44
46
  "esm-env": "^1.1.2",
45
47
  "runed": "^0.23.2",
46
48
  "svelte-toolbelt": "^0.7.1",