bits-ui 2.9.5 → 2.9.6

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,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 {
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "2.9.5",
3
+ "version": "2.9.6",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",