bits-ui 1.0.0-next.92 → 1.0.0-next.93

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.
@@ -207,7 +207,7 @@ class TooltipContentState {
207
207
  useGraceArea({
208
208
  triggerNode: () => this.root.triggerNode,
209
209
  contentNode: () => this.root.contentNode,
210
- enabled: () => this.root.opts.open.current && this.root.disableHoverableContent,
210
+ enabled: () => this.root.opts.open.current && !this.root.disableHoverableContent,
211
211
  onPointerExit: () => {
212
212
  this.root.handleClose();
213
213
  },
@@ -1,12 +1,14 @@
1
- export type FocusScopeAPI = {
1
+ export interface FocusScopeAPI {
2
2
  id: string;
3
3
  paused: boolean;
4
4
  pause: () => void;
5
5
  resume: () => void;
6
- };
6
+ isHandlingFocus: boolean;
7
+ }
7
8
  export declare function createFocusScopeStack(): {
8
9
  add(focusScope: FocusScopeAPI): void;
9
10
  remove(focusScope: FocusScopeAPI): void;
11
+ readonly current: FocusScopeAPI[];
10
12
  };
11
13
  export declare function createFocusScopeAPI(): FocusScopeAPI;
12
14
  export declare function removeLinks(items: HTMLElement[]): HTMLElement[];
@@ -2,31 +2,38 @@ import { box } from "svelte-toolbelt";
2
2
  import { useId } from "../../../internal/use-id.js";
3
3
  const focusStack = box([]);
4
4
  export function createFocusScopeStack() {
5
- const stack = focusStack;
6
5
  return {
7
6
  add(focusScope) {
8
- // pause the currently active focus scope (top of the stack)
9
- const activeFocusScope = stack.current[0];
10
- if (focusScope.id !== activeFocusScope?.id) {
11
- activeFocusScope?.pause();
7
+ const activeFocusScope = focusStack.current[0];
8
+ if (activeFocusScope && focusScope.id !== activeFocusScope.id) {
9
+ activeFocusScope.pause();
12
10
  }
13
- // remove in case it already exists because it'll be added to the top
14
- stack.current = removeFromFocusScopeArray(stack.current, focusScope);
15
- stack.current.unshift(focusScope);
11
+ focusStack.current = removeFromFocusScopeArray(focusStack.current, focusScope);
12
+ focusStack.current.unshift(focusScope);
16
13
  },
17
14
  remove(focusScope) {
18
- stack.current = removeFromFocusScopeArray(stack.current, focusScope);
19
- stack.current[0]?.resume();
15
+ focusStack.current = removeFromFocusScopeArray(focusStack.current, focusScope);
16
+ focusStack.current[0]?.resume();
17
+ },
18
+ get current() {
19
+ return focusStack.current;
20
20
  },
21
21
  };
22
22
  }
23
23
  export function createFocusScopeAPI() {
24
24
  let paused = $state(false);
25
+ let isHandlingFocus = $state(false);
25
26
  return {
26
27
  id: useId(),
27
28
  get paused() {
28
29
  return paused;
29
30
  },
31
+ get isHandlingFocus() {
32
+ return isHandlingFocus;
33
+ },
34
+ set isHandlingFocus(value) {
35
+ isHandlingFocus = value;
36
+ },
30
37
  pause() {
31
38
  paused = true;
32
39
  },
@@ -21,69 +21,57 @@ export function useFocusScope({ id, loop, enabled, onOpenAutoFocus, onCloseAutoF
21
21
  const focusScope = createFocusScopeAPI();
22
22
  const ref = box(null);
23
23
  const ctx = FocusScopeContext.getOr({ ignoreCloseAutoFocus: false });
24
+ let lastFocusedElement = null;
24
25
  useRefById({
25
26
  id,
26
27
  ref,
27
28
  deps: () => enabled.current,
28
29
  });
29
- let lastFocusedElement = null;
30
- watch([() => ref.current, () => enabled.current], ([container, enabled]) => {
31
- if (!container || !enabled)
30
+ function manageFocus(event) {
31
+ if (focusScope.paused || !ref.current || focusScope.isHandlingFocus)
32
32
  return;
33
- const handleFocusIn = (event) => {
34
- if (focusScope.paused || !container)
35
- return;
33
+ focusScope.isHandlingFocus = true;
34
+ try {
36
35
  const target = event.target;
37
36
  if (!isHTMLElement(target))
38
37
  return;
39
- if (container.contains(target)) {
40
- lastFocusedElement = target;
41
- }
42
- else {
43
- if (ctx.ignoreCloseAutoFocus)
44
- return;
45
- focus(lastFocusedElement, { select: true });
46
- }
47
- };
48
- const handleFocusOut = (event) => {
49
- if (focusScope.paused || !container || ctx.ignoreCloseAutoFocus) {
50
- return;
51
- }
52
- const relatedTarget = event.relatedTarget;
53
- if (!isHTMLElement(relatedTarget))
54
- return;
55
- // A `focusout` event with a `null` `relatedTarget` will happen in at least two cases:
56
- //
57
- // 1. When the user switches app/tabs/windows/the browser itself loses focus.
58
- // 2. In Google Chrome, when the focused element is removed from the DOM.
59
- //
60
- // We let the browser do its thing here because:
61
- //
62
- // 1. The browser already keeps a memory of what's focused for when the
63
- // page gets refocused.
64
- // 2. In Google Chrome, if we try to focus the deleted focused element it throws
65
- // the CPU to 100%, so we avoid doing anything for this reason here too.
66
- if (relatedTarget === null)
67
- return;
68
- // If the focus has moved to an actual legitimate element (`relatedTarget !== null`)
69
- // that is outside the container, we move focus to the last valid focused element inside.
70
- if (!container.contains(relatedTarget)) {
71
- focus(lastFocusedElement, { select: true });
38
+ const isWithinActiveScope = ref.current.contains(target);
39
+ if (event.type === "focusin") {
40
+ if (isWithinActiveScope) {
41
+ lastFocusedElement = target;
42
+ }
43
+ else {
44
+ if (ctx.ignoreCloseAutoFocus)
45
+ return;
46
+ focus(lastFocusedElement, { select: true });
47
+ }
72
48
  }
73
- };
74
- // When the focused element gets removed from the DOM, browsers move focus
75
- // back to the document.body. In this case, we move focus to the container
76
- // to keep focus trapped correctly.
77
- // instead of leaning on document.activeElement, we use lastFocusedElement to check
78
- // if the element still exists inside the container,
79
- // if not then we focus to the container
80
- const handleMutations = (_) => {
81
- const lastFocusedElementExists = container?.contains(lastFocusedElement);
82
- if (!lastFocusedElementExists) {
83
- focus(container);
49
+ else if (event.type === "focusout") {
50
+ if (!isWithinActiveScope && !ctx.ignoreCloseAutoFocus) {
51
+ focus(lastFocusedElement, { select: true });
52
+ }
84
53
  }
85
- };
86
- const removeEvents = executeCallbacks(on(document, "focusin", handleFocusIn), on(document, "focusout", handleFocusOut));
54
+ }
55
+ finally {
56
+ focusScope.isHandlingFocus = false;
57
+ }
58
+ }
59
+ // When the focused element gets removed from the DOM, browsers move focus
60
+ // back to the document.body. In this case, we move focus to the container
61
+ // to keep focus trapped correctly.
62
+ // instead of leaning on document.activeElement, we use lastFocusedElement to check
63
+ // if the element still exists inside the container,
64
+ // if not then we focus to the container
65
+ function handleMutations(_) {
66
+ const lastFocusedElementExists = ref.current?.contains(lastFocusedElement);
67
+ if (!lastFocusedElementExists && ref.current) {
68
+ focus(ref.current);
69
+ }
70
+ }
71
+ watch([() => ref.current, () => enabled.current], ([container, enabled]) => {
72
+ if (!container || !enabled)
73
+ return;
74
+ const removeEvents = executeCallbacks(on(document, "focusin", manageFocus), on(document, "focusout", manageFocus));
87
75
  const mutationObserver = new MutationObserver(handleMutations);
88
76
  mutationObserver.observe(container, { childList: true, subtree: true });
89
77
  return () => {
@@ -33,6 +33,8 @@ export function focusWithoutScroll(element) {
33
33
  export function focus(element, { select = false } = {}) {
34
34
  if (!(element && element.focus))
35
35
  return;
36
+ if (document.activeElement === element)
37
+ return;
36
38
  const previouslyFocusedElement = document.activeElement;
37
39
  // prevent scroll on focus
38
40
  element.focus({ preventScroll: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "1.0.0-next.92",
3
+ "version": "1.0.0-next.93",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",