bits-ui 2.9.5 → 2.9.7

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.
@@ -57,11 +57,7 @@
57
57
  present: contentState.root.opts.open.current,
58
58
  open: contentState.root.opts.open.current,
59
59
  })}
60
- onCloseAutoFocus={(e) => {
61
- onCloseAutoFocus(e);
62
- if (e.defaultPrevented) return;
63
- afterSleep(0, () => contentState.root.triggerNode?.focus());
64
- }}
60
+ {onCloseAutoFocus}
65
61
  onOpenAutoFocus={(e) => {
66
62
  onOpenAutoFocus(e);
67
63
  if (e.defaultPrevented) return;
@@ -1,7 +1,7 @@
1
1
  import { type ReadableBoxedValues, type WritableBoxedValues } from "svelte-toolbelt";
2
2
  import type { HTMLButtonAttributes } from "svelte/elements";
3
3
  import { Context } from "runed";
4
- import type { BitsKeyboardEvent, BitsMouseEvent, OnChangeFn, RefAttachment, WithRefOpts } from "../../internal/types.js";
4
+ import type { BitsFocusEvent, BitsKeyboardEvent, BitsMouseEvent, OnChangeFn, RefAttachment, WithRefOpts } from "../../internal/types.js";
5
5
  interface CheckboxGroupStateOpts extends WithRefOpts, ReadableBoxedValues<{
6
6
  name: string | undefined;
7
7
  disabled: boolean;
@@ -91,6 +91,7 @@ export declare class CheckboxInputState {
91
91
  readonly trueChecked: boolean;
92
92
  readonly shouldRender: boolean;
93
93
  constructor(root: CheckboxRootState);
94
+ onfocus(_: BitsFocusEvent): void;
94
95
  readonly props: {
95
96
  readonly type: "checkbox";
96
97
  readonly checked: boolean;
@@ -99,6 +100,7 @@ export declare class CheckboxInputState {
99
100
  readonly name: string | undefined;
100
101
  readonly value: string | undefined;
101
102
  readonly readonly: boolean;
103
+ readonly onfocus: (_: BitsFocusEvent) => void;
102
104
  };
103
105
  }
104
106
  export {};
@@ -3,6 +3,7 @@ import { Context, watch } from "runed";
3
3
  import { createBitsAttrs, getAriaChecked, getAriaReadonly, getAriaRequired, getDataDisabled, getDataReadonly, } from "../../internal/attrs.js";
4
4
  import { kbd } from "../../internal/kbd.js";
5
5
  import { arraysAreEqual } from "../../internal/arrays.js";
6
+ import { isHTMLElement } from "../../internal/is.js";
6
7
  const checkboxAttrs = createBitsAttrs({
7
8
  component: "checkbox",
8
9
  parts: ["root", "group", "group-label", "input"],
@@ -192,6 +193,12 @@ export class CheckboxInputState {
192
193
  shouldRender = $derived.by(() => Boolean(this.root.trueName));
193
194
  constructor(root) {
194
195
  this.root = root;
196
+ this.onfocus = this.onfocus.bind(this);
197
+ }
198
+ onfocus(_) {
199
+ if (!isHTMLElement(this.root.opts.ref.current))
200
+ return;
201
+ this.root.opts.ref.current.focus();
195
202
  }
196
203
  props = $derived.by(() => ({
197
204
  type: "checkbox",
@@ -201,6 +208,7 @@ export class CheckboxInputState {
201
208
  name: this.root.trueName,
202
209
  value: this.root.opts.value.current,
203
210
  readonly: this.root.trueReadonly,
211
+ onfocus: this.onfocus,
204
212
  }));
205
213
  }
206
214
  function getCheckboxDataState(checked, indeterminate) {
@@ -528,7 +528,6 @@ export class CommandRootState {
528
528
  this.#scheduleUpdate();
529
529
  return () => {
530
530
  const selectedItem = this.#getSelectedItem();
531
- this.allIds.delete(id);
532
531
  this.allItems.delete(id);
533
532
  this.commandState.filtered.items.delete(id);
534
533
  this.#filterItems();
@@ -1132,16 +1131,21 @@ export class CommandItemState {
1132
1131
  () => this.#group?.trueValue,
1133
1132
  () => this.opts.forceMount.current,
1134
1133
  ], () => {
1135
- if (this.opts.forceMount.current)
1134
+ if (this.opts.forceMount.current || !this.trueValue)
1136
1135
  return;
1137
1136
  return this.root.registerItem(this.trueValue, this.#group?.trueValue);
1138
1137
  });
1139
1138
  watch([() => this.opts.value.current, () => this.opts.ref.current], () => {
1140
- if (!this.opts.value.current && this.opts.ref.current?.textContent) {
1139
+ if (this.opts.value.current) {
1140
+ this.trueValue = this.opts.value.current;
1141
+ }
1142
+ else if (this.opts.ref.current?.textContent) {
1141
1143
  this.trueValue = this.opts.ref.current.textContent.trim();
1142
1144
  }
1143
- this.root.registerValue(this.trueValue, opts.keywords.current.map((kw) => kw.trim()));
1144
- this.opts.ref.current?.setAttribute(COMMAND_VALUE_ATTR, this.trueValue);
1145
+ if (this.trueValue) {
1146
+ this.root.registerValue(this.trueValue, opts.keywords.current.map((kw) => kw.trim()));
1147
+ this.opts.ref.current?.setAttribute(COMMAND_VALUE_ATTR, this.trueValue);
1148
+ }
1145
1149
  });
1146
1150
  // bindings
1147
1151
  this.onclick = this.onclick.bind(this);
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { afterSleep, box, mergeProps } from "svelte-toolbelt";
2
+ import { box, mergeProps } from "svelte-toolbelt";
3
3
  import { DialogContentState } from "../dialog.svelte.js";
4
4
  import type { DialogContentProps } from "../types.js";
5
5
  import DismissibleLayer from "../../utilities/dismissible-layer/dismissible-layer.svelte";
@@ -57,11 +57,7 @@
57
57
  open: contentState.root.opts.open.current,
58
58
  })}
59
59
  {onOpenAutoFocus}
60
- onCloseAutoFocus={(e) => {
61
- onCloseAutoFocus(e);
62
- if (e.defaultPrevented) return;
63
- afterSleep(1, () => contentState.root.triggerNode?.focus());
64
- }}
60
+ {onCloseAutoFocus}
65
61
  >
66
62
  {#snippet focusScope({ props: focusScopeProps })}
67
63
  <EscapeLayer
@@ -21,7 +21,6 @@
21
21
  onInteractOutside = noop,
22
22
  trapFocus = true,
23
23
  preventScroll = false,
24
-
25
24
  ...restProps
26
25
  }: PopoverContentStaticProps = $props();
27
26
 
@@ -33,7 +32,6 @@
33
32
  ),
34
33
  onInteractOutside: box.with(() => onInteractOutside),
35
34
  onEscapeKeydown: box.with(() => onEscapeKeydown),
36
- onCloseAutoFocus: box.with(() => onCloseAutoFocus),
37
35
  customAnchor: box.with(() => null),
38
36
  });
39
37
 
@@ -52,6 +50,7 @@
52
50
  {preventScroll}
53
51
  loop
54
52
  forceMount={true}
53
+ {onCloseAutoFocus}
55
54
  >
56
55
  {#snippet popper({ props })}
57
56
  {@const finalProps = mergeProps(props, {
@@ -78,6 +77,7 @@
78
77
  {preventScroll}
79
78
  loop
80
79
  forceMount={false}
80
+ {onCloseAutoFocus}
81
81
  >
82
82
  {#snippet popper({ props })}
83
83
  {@const finalProps = mergeProps(props, {
@@ -33,7 +33,6 @@
33
33
  ),
34
34
  onInteractOutside: box.with(() => onInteractOutside),
35
35
  onEscapeKeydown: box.with(() => onEscapeKeydown),
36
- onCloseAutoFocus: box.with(() => onCloseAutoFocus),
37
36
  customAnchor: box.with(() => customAnchor),
38
37
  });
39
38
 
@@ -52,6 +51,7 @@
52
51
  loop
53
52
  forceMount={true}
54
53
  {customAnchor}
54
+ {onCloseAutoFocus}
55
55
  >
56
56
  {#snippet popper({ props, wrapperProps })}
57
57
  {@const finalProps = mergeProps(props, {
@@ -80,6 +80,7 @@
80
80
  loop
81
81
  forceMount={false}
82
82
  {customAnchor}
83
+ {onCloseAutoFocus}
83
84
  >
84
85
  {#snippet popper({ props, wrapperProps })}
85
86
  {@const finalProps = mergeProps(props, {
@@ -43,7 +43,6 @@ export declare class PopoverTriggerState {
43
43
  interface PopoverContentStateOpts extends WithRefOpts, ReadableBoxedValues<{
44
44
  onInteractOutside: (e: PointerEvent) => void;
45
45
  onEscapeKeydown: (e: KeyboardEvent) => void;
46
- onCloseAutoFocus: (e: Event) => void;
47
46
  customAnchor: string | HTMLElement | null | Measurable;
48
47
  }> {
49
48
  }
@@ -55,7 +54,6 @@ export declare class PopoverContentState {
55
54
  constructor(opts: PopoverContentStateOpts, root: PopoverRootState);
56
55
  onInteractOutside: (e: PointerEvent) => void;
57
56
  onEscapeKeydown: (e: KeyboardEvent) => void;
58
- onCloseAutoFocus: (e: Event) => void;
59
57
  readonly snippetProps: {
60
58
  open: boolean;
61
59
  };
@@ -70,7 +68,6 @@ export declare class PopoverContentState {
70
68
  readonly popperProps: {
71
69
  onInteractOutside: (e: PointerEvent) => void;
72
70
  onEscapeKeydown: (e: KeyboardEvent) => void;
73
- onCloseAutoFocus: (e: Event) => void;
74
71
  };
75
72
  }
76
73
  interface PopoverCloseStateOpts extends WithRefOpts {
@@ -124,13 +124,6 @@ export class PopoverContentState {
124
124
  return;
125
125
  this.root.handleClose();
126
126
  };
127
- onCloseAutoFocus = (e) => {
128
- this.opts.onCloseAutoFocus.current?.(e);
129
- if (e.defaultPrevented)
130
- return;
131
- e.preventDefault();
132
- this.root.triggerNode?.focus();
133
- };
134
127
  snippetProps = $derived.by(() => ({ open: this.root.opts.open.current }));
135
128
  props = $derived.by(() => ({
136
129
  id: this.opts.id.current,
@@ -145,7 +138,6 @@ export class PopoverContentState {
145
138
  popperProps = {
146
139
  onInteractOutside: this.onInteractOutside,
147
140
  onEscapeKeydown: this.onEscapeKeydown,
148
- onCloseAutoFocus: this.onCloseAutoFocus,
149
141
  };
150
142
  }
151
143
  export class PopoverCloseState {
@@ -73,12 +73,14 @@ export declare class RadioGroupInputState {
73
73
  static create(): RadioGroupInputState;
74
74
  readonly root: RadioGroupRootState;
75
75
  readonly shouldRender: boolean;
76
+ constructor(root: RadioGroupRootState);
77
+ onfocus(_: BitsFocusEvent): void;
76
78
  readonly props: {
77
79
  readonly name: string | undefined;
78
80
  readonly value: string;
79
81
  readonly required: boolean;
80
82
  readonly disabled: boolean;
83
+ readonly onfocus: (_: BitsFocusEvent) => void;
81
84
  };
82
- constructor(root: RadioGroupRootState);
83
85
  }
84
86
  export {};
@@ -130,13 +130,18 @@ export class RadioGroupInputState {
130
130
  }
131
131
  root;
132
132
  shouldRender = $derived.by(() => this.root.opts.name.current !== undefined);
133
+ constructor(root) {
134
+ this.root = root;
135
+ this.onfocus = this.onfocus.bind(this);
136
+ }
137
+ onfocus(_) {
138
+ this.root.rovingFocusGroup.focusCurrentTabStop();
139
+ }
133
140
  props = $derived.by(() => ({
134
141
  name: this.root.opts.name.current,
135
142
  value: this.root.opts.value.current,
136
143
  required: this.root.opts.required.current,
137
144
  disabled: this.root.opts.disabled.current,
145
+ onfocus: this.onfocus,
138
146
  }));
139
- constructor(root) {
140
- this.root = root;
141
- }
142
147
  }
@@ -9,4 +9,7 @@ export declare class FocusScopeManager {
9
9
  setFocusMemory(scope: FocusScope, element: HTMLElement): void;
10
10
  getFocusMemory(scope: FocusScope): HTMLElement | undefined;
11
11
  isActiveScope(scope: FocusScope): boolean;
12
+ setPreFocusMemory(scope: FocusScope, element: HTMLElement): void;
13
+ getPreFocusMemory(scope: FocusScope): HTMLElement | undefined;
14
+ clearPreFocusMemory(scope: FocusScope): void;
12
15
  }
@@ -4,6 +4,7 @@ export class FocusScopeManager {
4
4
  static instance;
5
5
  #scopeStack = box([]);
6
6
  #focusHistory = new WeakMap();
7
+ #preFocusHistory = new WeakMap();
7
8
  static getInstance() {
8
9
  if (!this.instance) {
9
10
  this.instance = new FocusScopeManager();
@@ -15,6 +16,11 @@ export class FocusScopeManager {
15
16
  if (current && current !== scope) {
16
17
  current.pause();
17
18
  }
19
+ // capture the currently focused element before this scope becomes active
20
+ const activeElement = document.activeElement;
21
+ if (activeElement && activeElement !== document.body) {
22
+ this.#preFocusHistory.set(scope, activeElement);
23
+ }
18
24
  this.#scopeStack.current = this.#scopeStack.current.filter((s) => s !== scope);
19
25
  this.#scopeStack.current.unshift(scope);
20
26
  }
@@ -37,4 +43,13 @@ export class FocusScopeManager {
37
43
  isActiveScope(scope) {
38
44
  return this.getActive() === scope;
39
45
  }
46
+ setPreFocusMemory(scope, element) {
47
+ this.#preFocusHistory.set(scope, element);
48
+ }
49
+ getPreFocusMemory(scope) {
50
+ return this.#preFocusHistory.get(scope);
51
+ }
52
+ clearPreFocusMemory(scope) {
53
+ this.#preFocusHistory.delete(scope);
54
+ }
40
55
  }
@@ -43,6 +43,7 @@ export class FocusScope {
43
43
  // handle close auto-focus
44
44
  this.#handleCloseAutoFocus();
45
45
  this.#manager.unregister(this);
46
+ this.#manager.clearPreFocusMemory(this);
46
47
  this.#container = null;
47
48
  }
48
49
  #handleOpenAutoFocus() {
@@ -75,10 +76,17 @@ export class FocusScope {
75
76
  });
76
77
  this.#opts.onCloseAutoFocus.current?.(event);
77
78
  if (!event.defaultPrevented) {
78
- // return focus to previously focused element
79
- const prevFocused = document.activeElement;
80
- if (prevFocused && prevFocused !== document.body) {
81
- prevFocused.focus();
79
+ // return focus to the element that was focused before this scope opened
80
+ const preFocusedElement = this.#manager.getPreFocusMemory(this);
81
+ if (preFocusedElement && document.contains(preFocusedElement)) {
82
+ // ensure the element is still focusable and in the document
83
+ try {
84
+ preFocusedElement.focus();
85
+ }
86
+ catch {
87
+ // fallback if focus fails
88
+ document.body.focus();
89
+ }
82
90
  }
83
91
  }
84
92
  }
@@ -39,5 +39,6 @@ export declare class RovingFocusGroup {
39
39
  handleKeydown(node: HTMLElement | null | undefined, e: KeyboardEvent, both?: boolean): HTMLElement | undefined;
40
40
  getTabIndex(node: HTMLElement | null | undefined): 0 | -1;
41
41
  setCurrentTabStopId(id: string): void;
42
+ focusCurrentTabStop(): void;
42
43
  }
43
44
  export {};
@@ -3,6 +3,7 @@ import { getElemDirection } from "./locale.js";
3
3
  import { getDirectionalKeys } from "./get-directional-keys.js";
4
4
  import { kbd } from "./kbd.js";
5
5
  import { BROWSER } from "esm-env";
6
+ import { isHTMLElement } from "./is.js";
6
7
  export class RovingFocusGroup {
7
8
  #opts;
8
9
  #currentTabStopId = box(null);
@@ -84,4 +85,13 @@ export class RovingFocusGroup {
84
85
  setCurrentTabStopId(id) {
85
86
  this.#currentTabStopId.current = id;
86
87
  }
88
+ focusCurrentTabStop() {
89
+ const currentTabStopId = this.#currentTabStopId.current;
90
+ if (!currentTabStopId)
91
+ return;
92
+ const currentTabStop = this.#opts.rootNode.current?.querySelector(`#${currentTabStopId}`);
93
+ if (!currentTabStop || !isHTMLElement(currentTabStop))
94
+ return;
95
+ currentTabStop.focus();
96
+ }
87
97
  }
@@ -0,0 +1 @@
1
+ export declare function warn(...messages: string[]): void;
@@ -0,0 +1,14 @@
1
+ import { DEV } from "esm-env";
2
+ let set;
3
+ if (DEV) {
4
+ set = new Set();
5
+ }
6
+ export function warn(...messages) {
7
+ if (!DEV)
8
+ return;
9
+ const msg = messages.join(" ");
10
+ if (set.has(msg))
11
+ return;
12
+ set.add(msg);
13
+ console.warn(`[Bits UI]: ${msg}`);
14
+ }
@@ -72,7 +72,7 @@ export type EditableTimeSegmentPart = (typeof EDITABLE_TIME_SEGMENT_PARTS)[numbe
72
72
  export type EditableSegmentPart = (typeof EDITABLE_SEGMENT_PARTS)[number];
73
73
  export type NonEditableSegmentPart = (typeof NON_EDITABLE_SEGMENT_PARTS)[number];
74
74
  export type SegmentPart = EditableSegmentPart | NonEditableSegmentPart;
75
- export type TimeSegmentPart = EditableTimeSegmentPart | "literal";
75
+ export type TimeSegmentPart = EditableTimeSegmentPart | "literal" | "timeZoneName";
76
76
  export type AnyTimeExceptLiteral = Exclude<TimeSegmentPart, "literal">;
77
77
  export type AnyExceptLiteral = Exclude<SegmentPart, "literal">;
78
78
  export type DayPeriod = "AM" | "PM" | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "2.9.5",
3
+ "version": "2.9.7",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",