bits-ui 1.5.3 → 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.
@@ -28,16 +28,14 @@
28
28
  const mergedProps = $derived(mergeProps(restProps, contentState.props));
29
29
  </script>
30
30
 
31
- {#if contentState.context.viewportRef.current}
32
- <Portal to={contentState.context.viewportRef.current}>
33
- <PresenceLayer
34
- {id}
35
- present={forceMount || contentState.open || contentState.isLastActiveValue}
36
- >
37
- {#snippet presence()}
38
- <NavigationMenuContentImpl {...mergedProps} {children} {child} />
39
- <Mounted bind:mounted={contentState.mounted} />
40
- {/snippet}
41
- </PresenceLayer>
42
- </Portal>
43
- {/if}
31
+ <Portal
32
+ to={contentState.context.viewportRef.current || undefined}
33
+ disabled={!contentState.context.viewportRef.current}
34
+ >
35
+ <PresenceLayer {id} present={forceMount || contentState.open || contentState.isLastActiveValue}>
36
+ {#snippet presence()}
37
+ <NavigationMenuContentImpl {...mergedProps} {children} {child} />
38
+ <Mounted bind:mounted={contentState.mounted} />
39
+ {/snippet}
40
+ </PresenceLayer>
41
+ </Portal>
@@ -173,7 +173,7 @@ class NavigationMenuListState {
173
173
  });
174
174
  this.rovingFocusGroup = useRovingFocus({
175
175
  rootNodeId: opts.id,
176
- candidateSelector: `:is([${NAVIGATION_MENU_TRIGGER_ATTR}], [${NAVIGATION_MENU_LINK_ATTR}]):not([data-disabled])`,
176
+ candidateSelector: `[${NAVIGATION_MENU_TRIGGER_ATTR}]:not([data-disabled]), [${NAVIGATION_MENU_LINK_ATTR}]:not([data-disabled])`,
177
177
  loop: box.with(() => false),
178
178
  orientation: this.context.opts.orientation,
179
179
  });
@@ -297,13 +297,14 @@ class NavigationMenuTriggerState {
297
297
  // if opened via pointer move, we prevent the click event
298
298
  if (this.hasPointerMoveOpened.current)
299
299
  return;
300
- if (this.open) {
300
+ const shouldClose = this.open && this.context.opts.isRootMenu;
301
+ if (shouldClose) {
301
302
  this.context.onItemSelect("");
302
303
  }
303
- else {
304
+ else if (!this.open) {
304
305
  this.context.onItemSelect(this.itemContext.opts.value.current);
305
306
  }
306
- this.wasClickClose = this.open;
307
+ this.wasClickClose = shouldClose;
307
308
  };
308
309
  onkeydown = (e) => {
309
310
  const verticalEntryKey = this.context.opts.dir.current === "rtl" ? kbd.ARROW_LEFT : kbd.ARROW_RIGHT;
@@ -28,9 +28,14 @@
28
28
  </script>
29
29
 
30
30
  {#if child}
31
- {@render child({ props: mergedProps })}
31
+ {@render child({
32
+ active: thumbState.root.isThumbActive(thumbState.opts.index.current),
33
+ props: mergedProps,
34
+ })}
32
35
  {:else}
33
36
  <span {...mergedProps}>
34
- {@render children?.()}
37
+ {@render children?.({
38
+ active: thumbState.root.isThumbActive(thumbState.opts.index.current),
39
+ })}
35
40
  </span>
36
41
  {/if}
@@ -17,6 +17,7 @@ declare class SliderBaseRootState {
17
17
  isActive: boolean;
18
18
  direction: "rl" | "lr" | "tb" | "bt";
19
19
  constructor(opts: SliderBaseRootStateProps);
20
+ isThumbActive(_index: number): boolean;
20
21
  getAllThumbs: () => HTMLElement[];
21
22
  getThumbScale: () => [number, number];
22
23
  getPositionFromValue: (thumbValue: number) => number;
@@ -90,6 +91,7 @@ declare class SliderMultiRootState extends SliderBaseRootState {
90
91
  } | null;
91
92
  currentThumbIdx: number;
92
93
  constructor(opts: SliderMultiRootStateProps);
94
+ isThumbActive(index: number): boolean;
93
95
  applyPosition({ clientXY, activeThumbIdx, start, end, }: {
94
96
  clientXY: number;
95
97
  activeThumbIdx: number;
@@ -1807,6 +1809,7 @@ declare class SliderThumbState {
1807
1809
  props: {
1808
1810
  readonly id: string;
1809
1811
  readonly onkeydown: (e: BitsKeyboardEvent) => void;
1812
+ readonly "data-active": "" | undefined;
1810
1813
  readonly role: "slider";
1811
1814
  readonly "aria-valuemin": number;
1812
1815
  readonly "aria-valuemax": number;
@@ -1820,6 +1823,7 @@ declare class SliderThumbState {
1820
1823
  } | {
1821
1824
  readonly id: string;
1822
1825
  readonly onkeydown: (e: BitsKeyboardEvent) => void;
1826
+ readonly "data-active": "" | undefined;
1823
1827
  readonly role: "slider";
1824
1828
  readonly "aria-valuemin": number;
1825
1829
  readonly "aria-valuemax": number;
@@ -31,6 +31,9 @@ class SliderBaseRootState {
31
31
  this.opts = opts;
32
32
  useRefById(opts);
33
33
  }
34
+ isThumbActive(_index) {
35
+ return this.isActive;
36
+ }
34
37
  #touchAction = $derived.by(() => {
35
38
  if (this.opts.disabled.current)
36
39
  return undefined;
@@ -284,6 +287,9 @@ class SliderMultiRootState extends SliderBaseRootState {
284
287
  }
285
288
  });
286
289
  }
290
+ isThumbActive(index) {
291
+ return this.isActive && this.activeThumb?.idx === index;
292
+ }
287
293
  applyPosition({ clientXY, activeThumbIdx, start, end, }) {
288
294
  const min = this.opts.min.current;
289
295
  const max = this.opts.max.current;
@@ -658,6 +664,7 @@ class SliderThumbState {
658
664
  ...this.root.thumbsPropsArr[this.opts.index.current],
659
665
  id: this.opts.id.current,
660
666
  onkeydown: this.onkeydown,
667
+ "data-active": this.root.isThumbActive(this.opts.index.current) ? "" : undefined,
661
668
  }));
662
669
  }
663
670
  class SliderTickState {
@@ -106,6 +106,9 @@ export type SliderMultipleRootProps = SliderMultiRootPropsWithoutHTML & Without<
106
106
  export type SliderRootProps = SliderRootPropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, SliderRootPropsWithoutHTML>;
107
107
  export type SliderRangePropsWithoutHTML = WithChild;
108
108
  export type SliderRangeProps = SliderRangePropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, SliderRangePropsWithoutHTML>;
109
+ export type SliderThumbSnippetProps = {
110
+ active: boolean;
111
+ };
109
112
  export type SliderThumbPropsWithoutHTML = WithChild<{
110
113
  /**
111
114
  * Whether the thumb is disabled or not.
@@ -118,7 +121,7 @@ export type SliderThumbPropsWithoutHTML = WithChild<{
118
121
  * `Slider.Root` component.
119
122
  */
120
123
  index: number;
121
- }>;
124
+ }, SliderThumbSnippetProps>;
122
125
  export type SliderThumbProps = SliderThumbPropsWithoutHTML & Without<BitsPrimitiveSpanAttributes, SliderThumbPropsWithoutHTML>;
123
126
  export type SliderTickPropsWithoutHTML = WithChild<{
124
127
  /**
@@ -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.5.3",
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",