bits-ui 1.7.0 → 1.8.0

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.
@@ -10,6 +10,7 @@
10
10
  ref = $bindable(null),
11
11
  child,
12
12
  children,
13
+ openOnHover = true,
13
14
  ...restProps
14
15
  }: NavigationMenuItemProps = $props();
15
16
 
@@ -20,6 +21,7 @@
20
21
  (v) => (ref = v)
21
22
  ),
22
23
  value: box.with(() => value),
24
+ openOnHover: box.with(() => openOnHover),
23
25
  });
24
26
 
25
27
  const mergedProps = $derived(mergeProps(restProps, itemState.props));
@@ -4,6 +4,7 @@
4
4
  import { useId } from "../../../internal/use-id.js";
5
5
  import PresenceLayer from "../../utilities/presence-layer/presence-layer.svelte";
6
6
  import { box, mergeProps } from "svelte-toolbelt";
7
+ import { Mounted } from "../../utilities/index.js";
7
8
 
8
9
  let {
9
10
  id = useId(),
@@ -34,5 +35,6 @@
34
35
  {@render children?.()}
35
36
  </div>
36
37
  {/if}
38
+ <Mounted bind:mounted={viewportState.mounted} />
37
39
  {/snippet}
38
40
  </PresenceLayer>
@@ -9,6 +9,7 @@ import { SvelteMap } from "svelte/reactivity";
9
9
  import { type Direction, type Orientation } from "../../shared/index.js";
10
10
  import type { BitsFocusEvent, BitsKeyboardEvent, BitsMouseEvent, BitsPointerEvent } from "../../internal/types.js";
11
11
  import { useRovingFocus } from "../../internal/use-roving-focus.svelte.js";
12
+ import type { FocusEventHandler, KeyboardEventHandler, MouseEventHandler, PointerEventHandler } from "svelte/elements";
12
13
  type NavigationMenuProviderStateProps = ReadableBoxedValues<{
13
14
  dir: Direction;
14
15
  orientation: Orientation;
@@ -18,11 +19,11 @@ type NavigationMenuProviderStateProps = ReadableBoxedValues<{
18
19
  previousValue: string;
19
20
  }> & {
20
21
  isRootMenu: boolean;
21
- onTriggerEnter: (itemValue: string) => void;
22
+ onTriggerEnter: (itemValue: string, itemState: NavigationMenuItemState | null) => void;
22
23
  onTriggerLeave?: () => void;
23
24
  onContentEnter?: () => void;
24
25
  onContentLeave?: () => void;
25
- onItemSelect: (itemValue: string) => void;
26
+ onItemSelect: (itemValue: string, itemState: NavigationMenuItemState | null) => void;
26
27
  onItemDismiss: () => void;
27
28
  };
28
29
  declare class NavigationMenuProviderState {
@@ -36,7 +37,10 @@ declare class NavigationMenuProviderState {
36
37
  onContentLeave: () => void;
37
38
  onItemSelect: NavigationMenuProviderStateProps["onItemSelect"];
38
39
  onItemDismiss: NavigationMenuProviderStateProps["onItemDismiss"];
40
+ activeItem: NavigationMenuItemState | null;
41
+ prevActiveItem: NavigationMenuItemState | null;
39
42
  constructor(opts: NavigationMenuProviderStateProps);
43
+ setActiveItem: (item: NavigationMenuItemState | null) => void;
40
44
  }
41
45
  type NavigationMenuRootStateProps = WithRefProps<WritableBoxedValues<{
42
46
  value: string;
@@ -53,7 +57,7 @@ declare class NavigationMenuRootState {
53
57
  previousValue: WritableBox<string>;
54
58
  isDelaySkipped: WritableBox<boolean>;
55
59
  constructor(opts: NavigationMenuRootStateProps);
56
- setValue: (newValue: string) => void;
60
+ setValue: (newValue: string, itemState: NavigationMenuItemState | null) => void;
57
61
  props: {
58
62
  readonly id: string;
59
63
  readonly "data-orientation": "horizontal" | "vertical";
@@ -71,8 +75,9 @@ declare class NavigationMenuSubState {
71
75
  readonly opts: NavigationMenuSubStateProps;
72
76
  readonly context: NavigationMenuProviderState;
73
77
  previousValue: WritableBox<string>;
78
+ subProvider: NavigationMenuProviderState;
74
79
  constructor(opts: NavigationMenuSubStateProps, context: NavigationMenuProviderState);
75
- setValue: (newValue: string) => void;
80
+ setValue: (newValue: string, itemState: NavigationMenuItemState | null) => void;
76
81
  props: {
77
82
  readonly id: string;
78
83
  readonly "data-orientation": "horizontal" | "vertical";
@@ -102,6 +107,7 @@ declare class NavigationMenuListState {
102
107
  }
103
108
  type NavigationMenuItemStateProps = WithRefProps<ReadableBoxedValues<{
104
109
  value: string;
110
+ openOnHover: boolean;
105
111
  }>>;
106
112
  export declare class NavigationMenuItemState {
107
113
  #private;
@@ -147,13 +153,14 @@ declare class NavigationMenuTriggerState {
147
153
  provider: NavigationMenuProviderState;
148
154
  item: NavigationMenuItemState;
149
155
  list: NavigationMenuListState;
156
+ sub: NavigationMenuSubState | null;
150
157
  });
151
158
  onpointerenter: (_: BitsPointerEvent<HTMLButtonElement>) => void;
152
- onpointermove: BitsPointerEventHandler<HTMLElement>;
153
- onpointerleave: BitsPointerEventHandler<HTMLElement>;
154
- onclick: (_: BitsMouseEvent<HTMLButtonElement>) => void;
155
- onkeydown: (e: BitsKeyboardEvent<HTMLButtonElement>) => void;
156
- focusProxyOnFocus: (e: BitsFocusEvent) => void;
159
+ onpointermove: PointerEventHandler<HTMLElement>;
160
+ onpointerleave: PointerEventHandler<HTMLElement>;
161
+ onclick: MouseEventHandler<HTMLButtonElement>;
162
+ onkeydown: KeyboardEventHandler<HTMLButtonElement>;
163
+ focusProxyOnFocus: FocusEventHandler<HTMLElement>;
157
164
  props: {
158
165
  readonly id: string;
159
166
  readonly disabled: boolean | null | undefined;
@@ -163,16 +170,16 @@ declare class NavigationMenuTriggerState {
163
170
  readonly "aria-expanded": "true" | "false";
164
171
  readonly "aria-controls": string | undefined;
165
172
  readonly "data-navigation-menu-trigger": "";
166
- readonly onpointermove: BitsPointerEventHandler<HTMLElement>;
167
- readonly onpointerleave: BitsPointerEventHandler<HTMLElement>;
173
+ readonly onpointermove: PointerEventHandler<HTMLElement>;
174
+ readonly onpointerleave: PointerEventHandler<HTMLElement>;
168
175
  readonly onpointerenter: (_: BitsPointerEvent<HTMLButtonElement>) => void;
169
- readonly onclick: (_: BitsMouseEvent<HTMLButtonElement>) => void;
170
- readonly onkeydown: (e: BitsKeyboardEvent<HTMLButtonElement>) => void;
176
+ readonly onclick: MouseEventHandler<HTMLButtonElement>;
177
+ readonly onkeydown: KeyboardEventHandler<HTMLButtonElement>;
171
178
  };
172
179
  focusProxyProps: {
173
180
  readonly id: string;
174
181
  readonly tabindex: 0;
175
- readonly onfocus: (e: BitsFocusEvent) => void;
182
+ readonly onfocus: FocusEventHandler<HTMLElement>;
176
183
  };
177
184
  restructureSpanProps: {
178
185
  readonly "aria-owns": string | undefined;
@@ -183,6 +190,7 @@ type NavigationMenuLinkStateProps = WithRefProps & ReadableBoxedValues<{
183
190
  onSelect: (e: Event) => void;
184
191
  }>;
185
192
  declare class NavigationMenuLinkState {
193
+ #private;
186
194
  readonly opts: NavigationMenuLinkStateProps;
187
195
  readonly context: {
188
196
  provider: NavigationMenuProviderState;
@@ -197,6 +205,8 @@ declare class NavigationMenuLinkState {
197
205
  onkeydown: (e: BitsKeyboardEvent) => void;
198
206
  onfocus: (_: BitsFocusEvent) => void;
199
207
  onblur: (_: BitsFocusEvent) => void;
208
+ onpointerenter: PointerEventHandler<HTMLAnchorElement>;
209
+ onpointermove: PointerEventHandler<HTMLElement>;
200
210
  props: {
201
211
  readonly id: string;
202
212
  readonly "data-active": "" | undefined;
@@ -206,6 +216,8 @@ declare class NavigationMenuLinkState {
206
216
  readonly onkeydown: (e: BitsKeyboardEvent) => void;
207
217
  readonly onfocus: (_: BitsFocusEvent) => void;
208
218
  readonly onblur: (_: BitsFocusEvent) => void;
219
+ readonly onpointerenter: PointerEventHandler<HTMLAnchorElement>;
220
+ readonly onpointermove: PointerEventHandler<HTMLElement>;
209
221
  readonly "data-navigation-menu-link": "";
210
222
  };
211
223
  }
@@ -266,11 +278,11 @@ declare class NavigationMenuContentState {
266
278
  list: NavigationMenuListState;
267
279
  });
268
280
  onpointerenter: (_: BitsPointerEvent) => void;
269
- onpointerleave: BitsPointerEventHandler<HTMLElement>;
281
+ onpointerleave: PointerEventHandler<HTMLElement>;
270
282
  props: {
271
283
  readonly id: string;
272
284
  readonly onpointerenter: (_: BitsPointerEvent) => void;
273
- readonly onpointerleave: BitsPointerEventHandler<HTMLElement>;
285
+ readonly onpointerleave: PointerEventHandler<HTMLElement>;
274
286
  };
275
287
  }
276
288
  type MotionAttribute = "to-start" | "to-end" | "from-start" | "from-end";
@@ -309,6 +321,7 @@ declare class NavigationMenuViewportState {
309
321
  viewportWidth: string | undefined;
310
322
  viewportHeight: string | undefined;
311
323
  activeContentValue: string;
324
+ mounted: boolean;
312
325
  constructor(opts: NavigationMenuViewportImplStateProps, context: NavigationMenuProviderState);
313
326
  props: {
314
327
  readonly id: string;
@@ -338,5 +351,4 @@ export declare function useNavigationMenuLink(props: NavigationMenuLinkStateProp
338
351
  export declare function useNavigationMenuContentImpl(props: NavigationMenuContentImplStateProps, itemState?: NavigationMenuItemState): NavigationMenuContentImplState;
339
352
  export declare function useNavigationMenuViewport(props: NavigationMenuViewportImplStateProps): NavigationMenuViewportState;
340
353
  export declare function useNavigationMenuIndicator(): NavigationMenuIndicatorState;
341
- type BitsPointerEventHandler<T extends HTMLElement = HTMLElement> = (e: BitsPointerEvent<T>) => void;
342
354
  export {};
@@ -38,6 +38,8 @@ class NavigationMenuProviderState {
38
38
  onContentLeave = noop;
39
39
  onItemSelect;
40
40
  onItemDismiss;
41
+ activeItem = null;
42
+ prevActiveItem = null;
41
43
  constructor(opts) {
42
44
  this.opts = opts;
43
45
  this.onTriggerEnter = opts.onTriggerEnter;
@@ -47,6 +49,10 @@ class NavigationMenuProviderState {
47
49
  this.onItemDismiss = opts.onItemDismiss;
48
50
  this.onItemSelect = opts.onItemSelect;
49
51
  }
52
+ setActiveItem = (item) => {
53
+ this.prevActiveItem = this.activeItem;
54
+ this.activeItem = item;
55
+ };
50
56
  }
51
57
  class NavigationMenuRootState {
52
58
  opts;
@@ -74,8 +80,8 @@ class NavigationMenuRootState {
74
80
  orientation: this.opts.orientation,
75
81
  rootNavigationMenuRef: this.opts.ref,
76
82
  isRootMenu: true,
77
- onTriggerEnter: (itemValue) => {
78
- this.#onTriggerEnter(itemValue);
83
+ onTriggerEnter: (itemValue, itemState) => {
84
+ this.#onTriggerEnter(itemValue, itemState);
79
85
  },
80
86
  onTriggerLeave: this.#onTriggerLeave,
81
87
  onContentEnter: this.#onContentEnter,
@@ -84,34 +90,44 @@ class NavigationMenuRootState {
84
90
  onItemDismiss: this.#onItemDismiss,
85
91
  });
86
92
  }
87
- #debouncedFn = useDebounce((val) => {
93
+ #debouncedFn = useDebounce((val, itemState) => {
88
94
  // passing `undefined` meant to reset the debounce timer
89
95
  if (typeof val === "string") {
90
- this.setValue(val);
96
+ this.setValue(val, itemState);
91
97
  }
92
98
  }, () => this.#derivedDelay);
93
- #onTriggerEnter = (itemValue) => {
94
- this.#debouncedFn(itemValue);
99
+ #onTriggerEnter = (itemValue, itemState) => {
100
+ this.#debouncedFn(itemValue, itemState);
95
101
  };
96
102
  #onTriggerLeave = () => {
97
103
  this.isDelaySkipped.current = false;
98
- this.#debouncedFn("");
104
+ this.#debouncedFn("", null);
99
105
  };
100
106
  #onContentEnter = () => {
101
- this.#debouncedFn();
107
+ this.#debouncedFn(undefined, null);
102
108
  };
103
109
  #onContentLeave = () => {
104
- this.#debouncedFn("");
110
+ if (this.provider.activeItem &&
111
+ this.provider.activeItem.opts.openOnHover.current === false) {
112
+ return;
113
+ }
114
+ this.#debouncedFn("", null);
105
115
  };
106
- #onItemSelect = (itemValue) => {
107
- this.setValue(itemValue);
116
+ #onItemSelect = (itemValue, itemState) => {
117
+ this.setValue(itemValue, itemState);
108
118
  };
109
119
  #onItemDismiss = () => {
110
- this.setValue("");
120
+ this.setValue("", null);
111
121
  };
112
- setValue = (newValue) => {
122
+ setValue = (newValue, itemState) => {
113
123
  this.previousValue.current = this.opts.value.current;
114
124
  this.opts.value.current = newValue;
125
+ this.provider.setActiveItem(itemState);
126
+ // When all menus are closed, we want to reset previousValue to prevent
127
+ // weird transitions from old positions when opening fresh
128
+ if (newValue === "") {
129
+ this.previousValue.current = "";
130
+ }
115
131
  };
116
132
  props = $derived.by(() => ({
117
133
  id: this.opts.id.current,
@@ -125,11 +141,12 @@ class NavigationMenuSubState {
125
141
  opts;
126
142
  context;
127
143
  previousValue = box("");
144
+ subProvider;
128
145
  constructor(opts, context) {
129
146
  this.opts = opts;
130
147
  this.context = context;
131
148
  useRefById(opts);
132
- useNavigationMenuProvider({
149
+ this.subProvider = useNavigationMenuProvider({
133
150
  isRootMenu: false,
134
151
  value: this.opts.value,
135
152
  dir: this.context.opts.dir,
@@ -137,12 +154,19 @@ class NavigationMenuSubState {
137
154
  rootNavigationMenuRef: this.opts.ref,
138
155
  onTriggerEnter: this.setValue,
139
156
  onItemSelect: this.setValue,
140
- onItemDismiss: () => this.setValue(""),
157
+ onItemDismiss: () => this.setValue("", null),
141
158
  previousValue: this.previousValue,
142
159
  });
143
160
  }
144
- setValue = (newValue) => {
161
+ setValue = (newValue, itemState) => {
162
+ this.previousValue.current = this.opts.value.current;
145
163
  this.opts.value.current = newValue;
164
+ this.subProvider.setActiveItem(itemState);
165
+ // When all menus are closed, we want to reset previousValue to prevent
166
+ // weird transitions from old positions when opening fresh
167
+ if (newValue === "") {
168
+ this.previousValue.current = "";
169
+ }
146
170
  };
147
171
  props = $derived.by(() => ({
148
172
  id: this.opts.id.current,
@@ -281,28 +305,30 @@ class NavigationMenuTriggerState {
281
305
  if (this.opts.disabled.current ||
282
306
  this.wasClickClose ||
283
307
  this.itemContext.wasEscapeClose ||
284
- this.hasPointerMoveOpened.current) {
308
+ this.hasPointerMoveOpened.current ||
309
+ !this.itemContext.opts.openOnHover.current) {
285
310
  return;
286
311
  }
287
- this.context.onTriggerEnter(this.itemContext.opts.value.current);
312
+ this.context.onTriggerEnter(this.itemContext.opts.value.current, this.itemContext);
288
313
  this.hasPointerMoveOpened.current = true;
289
314
  });
290
315
  onpointerleave = whenMouse(() => {
291
- if (this.opts.disabled.current)
316
+ if (this.opts.disabled.current || !this.itemContext.opts.openOnHover.current)
292
317
  return;
293
318
  this.context.onTriggerLeave();
294
319
  this.hasPointerMoveOpened.current = false;
295
320
  });
296
- onclick = (_) => {
321
+ onclick = () => {
297
322
  // if opened via pointer move, we prevent the click event
298
323
  if (this.hasPointerMoveOpened.current)
299
324
  return;
300
- const shouldClose = this.open && this.context.opts.isRootMenu;
325
+ const shouldClose = this.open &&
326
+ (!this.itemContext.opts.openOnHover.current || this.context.opts.isRootMenu);
301
327
  if (shouldClose) {
302
- this.context.onItemSelect("");
328
+ this.context.onItemSelect("", null);
303
329
  }
304
330
  else if (!this.open) {
305
- this.context.onItemSelect(this.itemContext.opts.value.current);
331
+ this.context.onItemSelect(this.itemContext.opts.value.current, this.itemContext);
306
332
  }
307
333
  this.wasClickClose = shouldClose;
308
334
  };
@@ -386,6 +412,23 @@ class NavigationMenuLinkState {
386
412
  onblur = (_) => {
387
413
  this.isFocused = false;
388
414
  };
415
+ #handlePointerDismiss = () => {
416
+ // only close submenu if this link is not inside the currently open submenu content
417
+ const currentlyOpenValue = this.context.provider.opts.value.current;
418
+ const isInsideOpenSubmenu = this.context.item.opts.value.current === currentlyOpenValue;
419
+ const activeItem = this.context.item.listContext.context.activeItem;
420
+ if (activeItem && !activeItem.opts.openOnHover.current)
421
+ return;
422
+ if (currentlyOpenValue && !isInsideOpenSubmenu) {
423
+ this.context.provider.onItemDismiss();
424
+ }
425
+ };
426
+ onpointerenter = () => {
427
+ this.#handlePointerDismiss();
428
+ };
429
+ onpointermove = whenMouse(() => {
430
+ this.#handlePointerDismiss();
431
+ });
389
432
  props = $derived.by(() => ({
390
433
  id: this.opts.id.current,
391
434
  "data-active": this.opts.active.current ? "" : undefined,
@@ -395,6 +438,8 @@ class NavigationMenuLinkState {
395
438
  onkeydown: this.onkeydown,
396
439
  onfocus: this.onfocus,
397
440
  onblur: this.onblur,
441
+ onpointerenter: this.onpointerenter,
442
+ onpointermove: this.onpointermove,
398
443
  [NAVIGATION_MENU_LINK_ATTR]: "",
399
444
  }));
400
445
  }
@@ -497,6 +542,8 @@ class NavigationMenuContentState {
497
542
  this.context.onContentEnter();
498
543
  };
499
544
  onpointerleave = whenMouse(() => {
545
+ if (!this.itemContext.opts.openOnHover.current)
546
+ return;
500
547
  this.context.onContentLeave();
501
548
  });
502
549
  props = $derived.by(() => ({
@@ -520,6 +567,11 @@ class NavigationMenuContentImplState {
520
567
  const prevIndex = values.indexOf(this.context.opts.previousValue.current);
521
568
  const isSelected = this.itemContext.opts.value.current === this.context.opts.value.current;
522
569
  const wasSelected = prevIndex === values.indexOf(this.itemContext.opts.value.current);
570
+ // When all menus are closed, we want to reset motion state to prevent residual animations
571
+ if (!this.context.opts.value.current && !this.context.opts.previousValue.current) {
572
+ untrack(() => (this.prevMotionAttribute = null));
573
+ return null;
574
+ }
523
575
  // We only want to update selected and the last selected content
524
576
  // this avoids animations being interrupted outside of that range
525
577
  if (!isSelected && !wasSelected)
@@ -585,8 +637,17 @@ class NavigationMenuContentImplState {
585
637
  const target = e.target;
586
638
  const isTrigger = this.listContext.listTriggers.some((trigger) => trigger.contains(target));
587
639
  const isRootViewport = this.context.opts.isRootMenu && this.context.viewportRef.current?.contains(target);
588
- if (isTrigger || isRootViewport || !this.context.opts.isRootMenu)
640
+ if (!this.context.opts.isRootMenu && !isTrigger) {
641
+ this.context.onItemDismiss();
642
+ return;
643
+ }
644
+ if (isTrigger || isRootViewport) {
589
645
  e.preventDefault();
646
+ return;
647
+ }
648
+ if (!this.itemContext.opts.openOnHover.current) {
649
+ this.context.onItemSelect("", null);
650
+ }
590
651
  };
591
652
  onkeydown = (e) => {
592
653
  // prevent parent menus handling sub-menu keydown events
@@ -661,6 +722,7 @@ class NavigationMenuViewportState {
661
722
  viewportWidth = $derived.by(() => (this.size ? `${this.size.width}px` : undefined));
662
723
  viewportHeight = $derived.by(() => (this.size ? `${this.size.height}px` : undefined));
663
724
  activeContentValue = $derived.by(() => this.context.opts.value.current);
725
+ mounted = $state(false);
664
726
  constructor(opts, context) {
665
727
  this.opts = opts;
666
728
  this.context = context;
@@ -695,6 +757,12 @@ class NavigationMenuViewportState {
695
757
  };
696
758
  }
697
759
  });
760
+ // reset size when viewport closes to prevent residual size animations
761
+ watch(() => this.mounted, () => {
762
+ if (!this.mounted && this.size) {
763
+ this.size = null;
764
+ }
765
+ });
698
766
  }
699
767
  props = $derived.by(() => ({
700
768
  id: this.opts.id.current,
@@ -714,6 +782,7 @@ const NavigationMenuProviderContext = new Context("NavigationMenu.Root");
714
782
  export const NavigationMenuItemContext = new Context("NavigationMenu.Item");
715
783
  const NavigationMenuListContext = new Context("NavigationMenu.List");
716
784
  const NavigationMenuContentContext = new Context("NavigationMenu.Content");
785
+ const NavigationMenuSubContext = new Context("NavigationMenu.Sub");
717
786
  export function useNavigationMenuRoot(props) {
718
787
  return new NavigationMenuRootState(props);
719
788
  }
@@ -740,6 +809,7 @@ export function useNavigationMenuTrigger(props) {
740
809
  provider: NavigationMenuProviderContext.get(),
741
810
  item: NavigationMenuItemContext.get(),
742
811
  list: NavigationMenuListContext.get(),
812
+ sub: NavigationMenuSubContext.getOr(null),
743
813
  });
744
814
  }
745
815
  export function useNavigationMenuContent(props) {
@@ -15,21 +15,22 @@ export type NavigationMenuRootPropsWithoutHTML = WithChild<{
15
15
  */
16
16
  onValueChange?: OnChangeFn<string>;
17
17
  /**
18
- * The duration from when the mouse enters a trigger until the content opens.
18
+ * The amount of time in ms from when the mouse enters a trigger until the content opens.
19
19
  *
20
- * @defaultValue 200
20
+ * @default 200
21
21
  */
22
22
  delayDuration?: number;
23
23
  /**
24
- * How much time a user has to enter another trigger without incurring a delay again.
24
+ * The amount of time in ms that a user has to enter another trigger without
25
+ * incurring a delay again.
25
26
  *
26
- * @defaultValue 300
27
+ * @default 300
27
28
  */
28
29
  skipDelayDuration?: number;
29
30
  /**
30
31
  * The reading direction of the content.
31
32
  *
32
- * @defaultValue "ltr"
33
+ * @default "ltr"
33
34
  */
34
35
  dir?: Direction;
35
36
  /**
@@ -68,6 +69,13 @@ export type NavigationMenuItemPropsWithoutHTML = WithChild<{
68
69
  * The value of the menu item.
69
70
  */
70
71
  value?: string;
72
+ /**
73
+ * Whether to open the menu associated with the item when the item's trigger
74
+ * is hovered.
75
+ *
76
+ * @default true
77
+ */
78
+ openOnHover?: boolean;
71
79
  }>;
72
80
  export type NavigationMenuItemProps = NavigationMenuItemPropsWithoutHTML & Without<BitsPrimitiveLiAttributes, NavigationMenuItemPropsWithoutHTML>;
73
81
  export type NavigationMenuTriggerPropsWithoutHTML = WithChild<{
@@ -82,7 +90,6 @@ export type NavigationMenuContentPropsWithoutHTML = WithChild<{
82
90
  /**
83
91
  * Callback fired when an interaction occurs outside the content.
84
92
  * Default behavior can be prevented with `event.preventDefault()`
85
- *
86
93
  */
87
94
  onInteractOutside?: (event: PointerEvent) => void;
88
95
  /**
@@ -108,7 +115,7 @@ export type NavigationMenuContentPropsWithoutHTML = WithChild<{
108
115
  * This is useful when wanting to use more custom transition and animation
109
116
  * libraries.
110
117
  *
111
- * @defaultValue false
118
+ * @default false
112
119
  */
113
120
  forceMount?: boolean;
114
121
  }>;
@@ -22,6 +22,7 @@
22
22
  dir = "ltr",
23
23
  autoSort = true,
24
24
  orientation = "horizontal",
25
+ thumbPositioning = "contain",
25
26
  ...restProps
26
27
  }: SliderRootProps = $props();
27
28
 
@@ -63,6 +64,7 @@
63
64
  dir: box.with(() => dir),
64
65
  autoSort: box.with(() => autoSort),
65
66
  orientation: box.with(() => orientation),
67
+ thumbPositioning: box.with(() => thumbPositioning),
66
68
  type,
67
69
  });
68
70
 
@@ -1,7 +1,7 @@
1
1
  import { type Box, type ReadableBox } from "svelte-toolbelt";
2
2
  import type { ReadableBoxedValues, WritableBoxedValues } from "../../internal/box.svelte.js";
3
3
  import type { BitsKeyboardEvent, OnChangeFn, WithRefProps } from "../../internal/types.js";
4
- import type { Direction, Orientation } from "../../shared/index.js";
4
+ import type { Direction, Orientation, SliderThumbPositioning } from "../../shared/index.js";
5
5
  type SliderBaseRootStateProps = WithRefProps<ReadableBoxedValues<{
6
6
  disabled: boolean;
7
7
  orientation: Orientation;
@@ -10,6 +10,7 @@ type SliderBaseRootStateProps = WithRefProps<ReadableBoxedValues<{
10
10
  step: number;
11
11
  dir: Direction;
12
12
  autoSort: boolean;
13
+ thumbPositioning: SliderThumbPositioning;
13
14
  }>>;
14
15
  declare class SliderBaseRootState {
15
16
  #private;
@@ -46,6 +46,10 @@ class SliderBaseRootState {
46
46
  return Array.from(node.querySelectorAll(`[${SLIDER_THUMB_ATTR}]`));
47
47
  };
48
48
  getThumbScale = () => {
49
+ if (this.opts.thumbPositioning.current === "exact") {
50
+ // User opted out of containment
51
+ return [0, 100];
52
+ }
49
53
  const isVertical = this.opts.orientation.current === "vertical";
50
54
  // this assumes all thumbs are the same width
51
55
  const activeThumb = this.getAllThumbs()[0];
@@ -1,6 +1,6 @@
1
1
  import type { OnChangeFn, WithChild, Without } from "../../internal/types.js";
2
2
  import type { BitsPrimitiveSpanAttributes } from "../../shared/attributes.js";
3
- import type { Direction, Orientation } from "../../shared/index.js";
3
+ import type { Direction, Orientation, SliderThumbPositioning } from "../../shared/index.js";
4
4
  export type SliderRootSnippetProps = {
5
5
  ticks: number[];
6
6
  thumbs: number[];
@@ -53,6 +53,12 @@ export type BaseSliderRootPropsWithoutHTML = {
53
53
  * @defaultValue false
54
54
  */
55
55
  disabled?: boolean;
56
+ /**
57
+ * The positioning of the slider thumb.
58
+ *
59
+ * @defaultValue "contain"
60
+ */
61
+ thumbPositioning?: SliderThumbPositioning;
56
62
  };
57
63
  export type SliderSingleRootPropsWithoutHTML = BaseSliderRootPropsWithoutHTML & {
58
64
  /**
@@ -12,6 +12,13 @@ export type StyleProperties = CSS.Properties & {
12
12
  };
13
13
  export type Orientation = "horizontal" | "vertical";
14
14
  export type Direction = "ltr" | "rtl";
15
+ /**
16
+ * Controls positioning of the slider thumb.
17
+ *
18
+ * - `exact`: The thumb is centered exactly at the value of the slider.
19
+ * - `contain`: The thumb is centered exactly at the value of the slider, but will be contained within the slider track at the ends.
20
+ */
21
+ export type SliderThumbPositioning = "exact" | "contain";
15
22
  export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
16
23
  export type WithoutChildren<T> = T extends {
17
24
  children?: any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",