bits-ui 2.16.3 → 2.16.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.
@@ -135,6 +135,7 @@ export declare class DateFieldRootState {
135
135
  style: {
136
136
  caretColor: string;
137
137
  };
138
+ onbeforeinput: (e: InputEvent) => void;
138
139
  };
139
140
  updateSegment<T extends keyof DateAndTimeSegmentObj>(part: T, cb: T extends DateSegmentPart ? Updater<DateSegmentObj[T]> : T extends EditableTimeSegmentPart ? Updater<TimeSegmentObj[T]> : Updater<DateAndTimeSegmentObj[T]>): void;
140
141
  handleSegmentClick(e: BitsMouseEvent): void;
@@ -256,6 +257,7 @@ declare abstract class BaseNumericSegmentState {
256
257
  style: {
257
258
  caretColor: string;
258
259
  };
260
+ onbeforeinput: (e: InputEvent) => void;
259
261
  } | {
260
262
  "aria-labelledby": string;
261
263
  contenteditable: string | undefined;
@@ -285,6 +287,7 @@ declare abstract class BaseNumericSegmentState {
285
287
  style: {
286
288
  caretColor: string;
287
289
  };
290
+ onbeforeinput: (e: InputEvent) => void;
288
291
  };
289
292
  }
290
293
  declare class DateFieldYearSegmentState extends BaseNumericSegmentState {
@@ -345,6 +348,7 @@ export declare class DateFieldDayPeriodSegmentState {
345
348
  style: {
346
349
  caretColor: string;
347
350
  };
351
+ onbeforeinput: (e: InputEvent) => void;
348
352
  } | {
349
353
  "aria-labelledby": string;
350
354
  contenteditable: string | undefined;
@@ -373,6 +377,7 @@ export declare class DateFieldDayPeriodSegmentState {
373
377
  style: {
374
378
  caretColor: string;
375
379
  };
380
+ onbeforeinput: (e: InputEvent) => void;
376
381
  } | undefined;
377
382
  }
378
383
  interface DateFieldLiteralSegmentStateOpts extends WithRefOpts {
@@ -414,6 +414,11 @@ export class DateFieldRootState {
414
414
  style: {
415
415
  caretColor: "transparent",
416
416
  },
417
+ onbeforeinput: (e) => {
418
+ if (!e.data || e.data.length <= 1) {
419
+ e.preventDefault();
420
+ }
421
+ },
417
422
  };
418
423
  #getLabelledBy(segmentId) {
419
424
  return `${segmentId} ${this.getLabelNode()?.id ?? ""}`;
@@ -183,6 +183,10 @@ export class MenuContentState {
183
183
  this.opts.onCloseAutoFocus.current?.(e);
184
184
  if (e.defaultPrevented || this.#isSub)
185
185
  return;
186
+ if (this.parentMenu.root.ignoreCloseAutoFocus) {
187
+ e.preventDefault();
188
+ return;
189
+ }
186
190
  if (this.parentMenu.triggerNode && isTabbable(this.parentMenu.triggerNode)) {
187
191
  e.preventDefault();
188
192
  this.parentMenu.triggerNode.focus();
@@ -306,7 +310,17 @@ export class MenuContentState {
306
310
  }
307
311
  if (e.target.closest(`#${triggerId}`)) {
308
312
  e.preventDefault();
313
+ return;
309
314
  }
315
+ /**
316
+ * when the menu closes due to an outside pointer interaction (for example,
317
+ * clicking another dropdown trigger), avoid focusing this menu's trigger
318
+ * to prevent stealing focus from the new interaction target.
319
+ */
320
+ this.parentMenu.root.ignoreCloseAutoFocus = true;
321
+ afterTick(() => {
322
+ this.parentMenu.root.ignoreCloseAutoFocus = false;
323
+ });
310
324
  }
311
325
  get shouldRender() {
312
326
  return this.parentMenu.contentPresence.shouldRender;
@@ -80,7 +80,7 @@ export class NavigationMenuRootState {
80
80
  const isOpen = this.opts?.value?.current !== "";
81
81
  if (isOpen || this.isDelaySkipped.current) {
82
82
  // 150 for user to switch trigger or move into content view
83
- return 100;
83
+ return 150;
84
84
  }
85
85
  else {
86
86
  return this.opts.delayDuration.current;
@@ -41,11 +41,13 @@ declare abstract class SelectBaseRootState {
41
41
  readonly highlightedValue: string | null;
42
42
  readonly highlightedId: string | undefined;
43
43
  readonly highlightedLabel: string | null;
44
+ contentIsPositioned: boolean;
44
45
  isUsingKeyboard: boolean;
45
46
  isCombobox: boolean;
46
47
  domContext: DOMContext;
47
48
  constructor(opts: SelectBaseRootStateOpts);
48
49
  setHighlightedNode(node: HTMLElement | null, initial?: boolean): void;
50
+ scrollHighlightedNodeIntoView(node: HTMLElement): void;
49
51
  getCandidateNodes(): HTMLElement[];
50
52
  setHighlightedToFirstCandidate(initial?: boolean): void;
51
53
  getNodeByValue(value: string): HTMLElement | null;
@@ -65,6 +65,7 @@ class SelectBaseRootState {
65
65
  return null;
66
66
  return this.highlightedNode.getAttribute("data-label");
67
67
  });
68
+ contentIsPositioned = $state(false);
68
69
  isUsingKeyboard = false;
69
70
  isCombobox = false;
70
71
  domContext = new DOMContext(() => null);
@@ -87,9 +88,14 @@ class SelectBaseRootState {
87
88
  setHighlightedNode(node, initial = false) {
88
89
  this.highlightedNode = node;
89
90
  if (node && (this.isUsingKeyboard || initial)) {
90
- node.scrollIntoView({ block: this.opts.scrollAlignment.current });
91
+ this.scrollHighlightedNodeIntoView(node);
91
92
  }
92
93
  }
94
+ scrollHighlightedNodeIntoView(node) {
95
+ if (!this.viewportNode || !this.contentIsPositioned)
96
+ return;
97
+ node.scrollIntoView({ block: this.opts.scrollAlignment.current });
98
+ }
93
99
  getCandidateNodes() {
94
100
  const node = this.contentNode;
95
101
  if (!node)
@@ -713,13 +719,20 @@ export class SelectContentState {
713
719
  }
714
720
  onDestroyEffect(() => {
715
721
  this.root.contentNode = null;
722
+ this.root.contentIsPositioned = false;
716
723
  this.isPositioned = false;
717
724
  });
718
725
  watch(() => this.root.opts.open.current, () => {
719
726
  if (this.root.opts.open.current)
720
727
  return;
728
+ this.root.contentIsPositioned = false;
721
729
  this.isPositioned = false;
722
730
  });
731
+ watch([() => this.isPositioned, () => this.root.highlightedNode], () => {
732
+ if (!this.isPositioned || !this.root.highlightedNode)
733
+ return;
734
+ this.root.scrollHighlightedNodeIntoView(this.root.highlightedNode);
735
+ });
723
736
  this.onpointermove = this.onpointermove.bind(this);
724
737
  }
725
738
  onpointermove(_) {
@@ -782,6 +795,7 @@ export class SelectContentState {
782
795
  // onPlaced is also called when the menu is closed, so we need to check if the menu
783
796
  // is actually open to avoid setting positioning to true when the menu is closed
784
797
  if (this.root.opts.open.current) {
798
+ this.root.contentIsPositioned = true;
785
799
  this.isPositioned = true;
786
800
  }
787
801
  },
@@ -1120,7 +1134,9 @@ export class SelectScrollDownButtonState {
1120
1134
  }
1121
1135
  this.scrollIntoViewTimer = afterSleep(5, () => {
1122
1136
  const activeItem = this.root.highlightedNode;
1123
- activeItem?.scrollIntoView({ block: this.root.opts.scrollAlignment.current });
1137
+ if (!activeItem)
1138
+ return;
1139
+ this.root.scrollHighlightedNodeIntoView(activeItem);
1124
1140
  });
1125
1141
  });
1126
1142
  }
@@ -163,8 +163,8 @@ export class TooltipProviderState {
163
163
  onClose = (tooltip) => {
164
164
  if (this.#openTooltip === tooltip) {
165
165
  this.#openTooltip = null;
166
+ this.#startTimer();
166
167
  }
167
- this.#startTimer();
168
168
  };
169
169
  isTooltipOpen = (tooltip) => {
170
170
  return this.#openTooltip === tooltip;
@@ -52,6 +52,11 @@
52
52
  enabled: boolean;
53
53
  contentPointerEvents?: "auto" | "none";
54
54
  } = $props();
55
+
56
+ const resolvedPreventScroll = $derived(preventScroll ?? true);
57
+ const effectiveStrategy = $derived(
58
+ strategy ?? (resolvedPreventScroll ? "fixed" : "absolute")
59
+ );
55
60
  </script>
56
61
 
57
62
  <PopperContent
@@ -68,7 +73,7 @@
68
73
  {sticky}
69
74
  {hideWhenDetached}
70
75
  {updatePositionStrategy}
71
- {strategy}
76
+ strategy={effectiveStrategy}
72
77
  {dir}
73
78
  {wrapperId}
74
79
  {style}
@@ -79,9 +84,9 @@
79
84
  >
80
85
  {#snippet content({ props: floatingProps, wrapperProps })}
81
86
  {#if restProps.forceMount && enabled}
82
- <ScrollLock {preventScroll} />
87
+ <ScrollLock preventScroll={resolvedPreventScroll} />
83
88
  {:else if !restProps.forceMount}
84
- <ScrollLock {preventScroll} />
89
+ <ScrollLock preventScroll={resolvedPreventScroll} />
85
90
  {/if}
86
91
  <FocusScope
87
92
  {onOpenAutoFocus}
@@ -23,6 +23,7 @@ export function useFloating(options) {
23
23
  let middlewareData = $state({});
24
24
  let isPositioned = $state(false);
25
25
  let hasWhileMountedPosition = false;
26
+ let updateRequestId = 0;
26
27
  const floatingStyles = $derived.by(() => {
27
28
  // preserve last known position when floating ref is null (during transitions)
28
29
  const xVal = floating.current ? roundByDPR(floating.current, x) : x;
@@ -50,12 +51,20 @@ export function useFloating(options) {
50
51
  function update() {
51
52
  if (reference.current === null || floating.current === null)
52
53
  return;
53
- computePosition(reference.current, floating.current, {
54
+ const referenceNode = reference.current;
55
+ const floatingNode = floating.current;
56
+ const requestId = ++updateRequestId;
57
+ computePosition(referenceNode, floatingNode, {
54
58
  middleware: middlewareOption,
55
59
  placement: placementOption,
56
60
  strategy: strategyOption,
57
61
  }).then((position) => {
58
- const referenceNode = reference.current;
62
+ // ignore stale async resolutions when newer updates were requested.
63
+ if (requestId !== updateRequestId)
64
+ return;
65
+ // ignore stale resolutions after ref replacement.
66
+ if (reference.current !== referenceNode || floating.current !== floatingNode)
67
+ return;
59
68
  const referenceHidden = isReferenceHidden(referenceNode);
60
69
  if (referenceHidden) {
61
70
  // keep last good coordinates when the anchor disappears to avoid
@@ -91,6 +100,7 @@ export function useFloating(options) {
91
100
  whileElementsMountedCleanup();
92
101
  whileElementsMountedCleanup = undefined;
93
102
  }
103
+ updateRequestId++;
94
104
  }
95
105
  function attach() {
96
106
  cleanup();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "2.16.3",
3
+ "version": "2.16.5",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",