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

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.
@@ -44,7 +44,7 @@ class CommandRootState {
44
44
  // internal state that we mutate in batches and publish to the `state` at once
45
45
  _commandState = $state(null);
46
46
  #snapshot() {
47
- return this._commandState;
47
+ return $state.snapshot(this._commandState);
48
48
  }
49
49
  #scheduleUpdate() {
50
50
  if (this.#updateScheduled)
@@ -56,7 +56,7 @@ class CommandRootState {
56
56
  const hasStateChanged = !Object.is(this.commandState, currentState);
57
57
  if (hasStateChanged) {
58
58
  this.commandState = currentState;
59
- this.opts.onStateChange?.current?.($state.snapshot(currentState));
59
+ this.opts.onStateChange?.current?.(currentState);
60
60
  }
61
61
  });
62
62
  }
@@ -319,6 +319,7 @@ declare class NavigationMenuViewportState {
319
319
  readonly "--bits-navigation-menu-viewport-width": string | undefined;
320
320
  readonly "--bits-navigation-menu-viewport-height": string | undefined;
321
321
  };
322
+ readonly "data-navigation-menu-viewport": "";
322
323
  readonly onpointerenter: () => void;
323
324
  readonly onpointerleave: () => void;
324
325
  };
@@ -26,6 +26,7 @@ const NAVIGATION_MENU_LIST_ATTR = "data-navigation-menu-list";
26
26
  const NAVIGATION_MENU_TRIGGER_ATTR = "data-navigation-menu-trigger";
27
27
  const NAVIGATION_MENU_CONTENT_ATTR = "data-navigation-menu-content";
28
28
  const NAVIGATION_MENU_LINK_ATTR = "data-navigation-menu-link";
29
+ const NAVIGATION_MENU_VIEWPORT_ATTR = "data-navigation-menu-viewport";
29
30
  class NavigationMenuProviderState {
30
31
  opts;
31
32
  indicatorTrackRef = box(null);
@@ -704,6 +705,7 @@ class NavigationMenuViewportState {
704
705
  "--bits-navigation-menu-viewport-width": this.viewportWidth,
705
706
  "--bits-navigation-menu-viewport-height": this.viewportHeight,
706
707
  },
708
+ [NAVIGATION_MENU_VIEWPORT_ATTR]: "",
707
709
  onpointerenter: this.context.onContentEnter,
708
710
  onpointerleave: this.context.onContentLeave,
709
711
  }));
@@ -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.94",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",