bits-ui 1.0.0-next.31 → 1.0.0-next.32

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.
@@ -10,6 +10,7 @@
10
10
  import { noop } from "../../../internal/noop.js";
11
11
  import ScrollLock from "../../utilities/scroll-lock/scroll-lock.svelte";
12
12
  import { useDialogContent } from "../../dialog/dialog.svelte.js";
13
+ import { shouldTrapFocus } from "../../../internal/should-trap-focus.js";
13
14
 
14
15
  let {
15
16
  id = useId(),
@@ -24,6 +25,7 @@
24
25
  onInteractOutside = noop,
25
26
  preventScroll = true,
26
27
  trapFocus = true,
28
+ restoreScrollDelay = null,
27
29
  ...restProps
28
30
  }: AlertDialogContentProps = $props();
29
31
 
@@ -38,12 +40,16 @@
38
40
  const mergedProps = $derived(mergeProps(restProps, contentState.props));
39
41
  </script>
40
42
 
41
- <PresenceLayer {...mergedProps} present={contentState.root.open.current || forceMount}>
43
+ <PresenceLayer {...mergedProps} {forceMount} present={contentState.root.open.current || forceMount}>
42
44
  {#snippet presence({ present })}
43
- <ScrollLock {preventScroll} />
44
45
  <FocusScope
45
46
  loop
46
- trapFocus={present.current && trapFocus}
47
+ trapFocus={shouldTrapFocus({
48
+ forceMount,
49
+ present: present.current,
50
+ trapFocus,
51
+ open: contentState.root.open.current,
52
+ })}
47
53
  {...mergedProps}
48
54
  onCloseAutoFocus={(e) => {
49
55
  onCloseAutoFocus(e);
@@ -79,18 +85,16 @@
79
85
  >
80
86
  <TextSelectionLayer {...mergedProps} enabled={present.current}>
81
87
  {#if child}
88
+ {#if contentState.root.open.current}
89
+ <ScrollLock {preventScroll} {restoreScrollDelay} />
90
+ {/if}
82
91
  {@render child({
83
92
  props: mergeProps(mergedProps, focusScopeProps),
84
93
  ...contentState.snippetProps,
85
94
  })}
86
95
  {:else}
87
- <div
88
- {...mergeProps(mergedProps, focusScopeProps, {
89
- style: {
90
- pointerEvents: "auto",
91
- },
92
- })}
93
- >
96
+ <ScrollLock {preventScroll} />
97
+ <div {...mergeProps(mergedProps, focusScopeProps)}>
94
98
  {@render children?.()}
95
99
  </div>
96
100
  {/if}
@@ -10,6 +10,7 @@
10
10
  import { useId } from "../../../internal/use-id.js";
11
11
  import { noop } from "../../../internal/noop.js";
12
12
  import ScrollLock from "../../utilities/scroll-lock/scroll-lock.svelte";
13
+ import { shouldTrapFocus } from "../../../internal/should-trap-focus.js";
13
14
 
14
15
  let {
15
16
  id = useId(),
@@ -22,6 +23,7 @@
22
23
  onInteractOutside = noop,
23
24
  trapFocus = true,
24
25
  preventScroll = true,
26
+ restoreScrollDelay = null,
25
27
  ...restProps
26
28
  }: DialogContentProps = $props();
27
29
 
@@ -36,11 +38,16 @@
36
38
  const mergedProps = $derived(mergeProps(restProps, contentState.props));
37
39
  </script>
38
40
 
39
- <PresenceLayer {...mergedProps} present={contentState.root.open.current || forceMount}>
41
+ <PresenceLayer {...mergedProps} {forceMount} present={contentState.root.open.current || forceMount}>
40
42
  {#snippet presence({ present })}
41
43
  <FocusScope
42
44
  loop
43
- trapFocus={present.current && trapFocus}
45
+ trapFocus={shouldTrapFocus({
46
+ forceMount,
47
+ present: present.current,
48
+ trapFocus,
49
+ open: contentState.root.open.current,
50
+ })}
44
51
  {...mergedProps}
45
52
  onCloseAutoFocus={(e) => {
46
53
  onCloseAutoFocus(e);
@@ -68,20 +75,17 @@
68
75
  }}
69
76
  >
70
77
  <TextSelectionLayer {...mergedProps} enabled={present.current}>
71
- <ScrollLock {preventScroll} />
72
78
  {#if child}
79
+ {#if contentState.root.open.current}
80
+ <ScrollLock {preventScroll} {restoreScrollDelay} />
81
+ {/if}
73
82
  {@render child({
74
83
  props: mergeProps(mergedProps, focusScopeProps),
75
84
  ...contentState.snippetProps,
76
85
  })}
77
86
  {:else}
78
- <div
79
- {...mergeProps(mergedProps, focusScopeProps, {
80
- style: {
81
- pointerEvents: "auto",
82
- },
83
- })}
84
- >
87
+ <ScrollLock {preventScroll} />
88
+ <div {...mergeProps(mergedProps, focusScopeProps)}>
85
89
  {@render children?.()}
86
90
  </div>
87
91
  {/if}
@@ -112,6 +112,9 @@ declare class DialogContentState {
112
112
  readonly role: "dialog" | "alertdialog";
113
113
  readonly "aria-describedby": string | undefined;
114
114
  readonly "aria-labelledby": string | undefined;
115
+ readonly style: {
116
+ readonly pointerEvents: "auto";
117
+ };
115
118
  };
116
119
  }
117
120
  type DialogOverlayStateProps = WithRefProps;
@@ -125,6 +128,9 @@ declare class DialogOverlayState {
125
128
  props: {
126
129
  readonly "data-state": "open" | "closed";
127
130
  readonly id: string;
131
+ readonly style: {
132
+ readonly pointerEvents: "auto";
133
+ };
128
134
  };
129
135
  }
130
136
  type AlertDialogCancelStateProps = WithRefProps & ReadableBoxedValues<{
@@ -254,6 +254,9 @@ class DialogContentState {
254
254
  "aria-describedby": this.root.descriptionId,
255
255
  "aria-labelledby": this.root.titleId,
256
256
  [this.root.attrs.content]: "",
257
+ style: {
258
+ pointerEvents: "auto",
259
+ },
257
260
  ...this.root.sharedProps,
258
261
  }));
259
262
  }
@@ -275,6 +278,9 @@ class DialogOverlayState {
275
278
  props = $derived.by(() => ({
276
279
  id: this.#id.current,
277
280
  [this.root.attrs.overlay]: "",
281
+ style: {
282
+ pointerEvents: "auto",
283
+ },
278
284
  ...this.root.sharedProps,
279
285
  }));
280
286
  }
@@ -3,6 +3,7 @@ import type { DismissibleLayerProps } from "../utilities/dismissible-layer/types
3
3
  import type { PresenceLayerProps } from "../utilities/presence-layer/types.js";
4
4
  import type { FocusScopeProps } from "../utilities/focus-scope/types.js";
5
5
  import type { TextSelectionLayerProps } from "../utilities/text-selection-layer/types.js";
6
+ import type { ScrollLockProps } from "../utilities/scroll-lock/index.js";
6
7
  import type { OnChangeFn, WithChild, WithChildNoChildrenSnippetProps, WithChildren, Without } from "../../internal/types.js";
7
8
  import type { BitsPrimitiveButtonAttributes, BitsPrimitiveDivAttributes } from "../../shared/attributes.js";
8
9
  import type { PortalProps } from "../utilities/portal/index.js";
@@ -28,9 +29,7 @@ export type DialogRootProps = DialogRootPropsWithoutHTML;
28
29
  export type DialogContentSnippetProps = {
29
30
  open: boolean;
30
31
  };
31
- export type DialogContentPropsWithoutHTML = WithChildNoChildrenSnippetProps<Omit<EscapeLayerProps & Omit<DismissibleLayerProps, "onInteractOutsideStart"> & PresenceLayerProps & FocusScopeProps & TextSelectionLayerProps & {
32
- preventScroll?: boolean;
33
- }, "loop">, DialogContentSnippetProps>;
32
+ export type DialogContentPropsWithoutHTML = WithChildNoChildrenSnippetProps<Omit<EscapeLayerProps & Omit<DismissibleLayerProps, "onInteractOutsideStart"> & PresenceLayerProps & FocusScopeProps & TextSelectionLayerProps & ScrollLockProps, "loop">, DialogContentSnippetProps>;
34
33
  export type DialogContentProps = DialogContentPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, DialogContentPropsWithoutHTML>;
35
34
  export type DialogOverlaySnippetProps = {
36
35
  open: boolean;
@@ -84,19 +84,19 @@ export function useFocusScope({ id, loop, enabled, onOpenAutoFocus, onCloseAutoF
84
84
  };
85
85
  });
86
86
  $effect(() => {
87
- let container = untrack(() => ref.current);
87
+ let container = ref.current;
88
88
  const previouslyFocusedElement = document.activeElement;
89
89
  untrack(() => {
90
90
  if (!container) {
91
- container = document.getElementById(untrack(() => id.current));
91
+ container = document.getElementById(id.current);
92
92
  }
93
93
  if (!container)
94
94
  return;
95
- untrack(() => focusScopeStack.add(focusScope));
95
+ focusScopeStack.add(focusScope);
96
96
  const hasFocusedCandidate = container.contains(previouslyFocusedElement);
97
97
  if (!hasFocusedCandidate) {
98
98
  const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS);
99
- container.addEventListener(AUTOFOCUS_ON_MOUNT, untrack(() => onOpenAutoFocus.current));
99
+ container.addEventListener(AUTOFOCUS_ON_MOUNT, onOpenAutoFocus.current);
100
100
  container.dispatchEvent(mountEvent);
101
101
  if (!mountEvent.defaultPrevented) {
102
102
  afterTick(() => {
@@ -7,8 +7,8 @@ import type { PresenceLayerImplProps, PresenceLayerProps } from "../presence-lay
7
7
  import type { FocusScopeImplProps, FocusScopeProps } from "../focus-scope/types.js";
8
8
  import type { ScrollLockProps } from "../scroll-lock/index.js";
9
9
  import type { Direction } from "../../../shared/index.js";
10
- export type PopperLayerProps = EscapeLayerProps & Omit<DismissibleLayerProps, "onInteractOutsideStart"> & FloatingLayerContentProps & PresenceLayerProps & TextSelectionLayerProps & FocusScopeProps & ScrollLockProps;
11
- export type PopperLayerStaticProps = EscapeLayerProps & Omit<DismissibleLayerProps, "onInteractOutsideStart"> & PresenceLayerProps & TextSelectionLayerProps & FocusScopeProps & ScrollLockProps & {
10
+ export type PopperLayerProps = EscapeLayerProps & Omit<DismissibleLayerProps, "onInteractOutsideStart"> & FloatingLayerContentProps & PresenceLayerProps & TextSelectionLayerProps & FocusScopeProps & Omit<ScrollLockProps, "restoreScrollDelay">;
11
+ export type PopperLayerStaticProps = EscapeLayerProps & Omit<DismissibleLayerProps, "onInteractOutsideStart"> & PresenceLayerProps & TextSelectionLayerProps & FocusScopeProps & Omit<ScrollLockProps, "restoreScrollDelay"> & {
12
12
  content?: Snippet<[{
13
13
  props: Record<string, unknown>;
14
14
  }]>;
@@ -5,4 +5,14 @@ export type ScrollLockProps = {
5
5
  * @defaultValue true
6
6
  */
7
7
  preventScroll?: boolean;
8
+ /**
9
+ * The delay in milliseconds before the scrollbar is restored after closing the
10
+ * dialog. This is only applicable when using the `child` snippet for custom
11
+ * transitions and `preventScroll` is `true`. You should set this to a value
12
+ * greater than the transition duration to prevent content from shifting during
13
+ * the transition.
14
+ *
15
+ * @defaultValue null
16
+ */
17
+ restoreScrollDelay?: number | null;
8
18
  };
@@ -2,7 +2,7 @@
2
2
  import type { ScrollLockProps } from "./index.js";
3
3
  import { useBodyScrollLock } from "../../../internal/use-body-scroll-lock.svelte.js";
4
4
 
5
- let { preventScroll = true }: ScrollLockProps = $props();
5
+ let { preventScroll = true, restoreScrollDelay = null }: ScrollLockProps = $props();
6
6
 
7
- useBodyScrollLock(preventScroll);
7
+ useBodyScrollLock(preventScroll, () => restoreScrollDelay);
8
8
  </script>
@@ -0,0 +1,6 @@
1
+ export declare function shouldTrapFocus({ forceMount, present, trapFocus, open, }: {
2
+ forceMount: boolean;
3
+ present: boolean;
4
+ trapFocus: boolean;
5
+ open: boolean;
6
+ }): boolean;
@@ -0,0 +1,6 @@
1
+ export function shouldTrapFocus({ forceMount, present, trapFocus, open, }) {
2
+ if (forceMount) {
3
+ return open && trapFocus;
4
+ }
5
+ return present && trapFocus;
6
+ }
@@ -1,5 +1,6 @@
1
+ import { type Getter } from "svelte-toolbelt";
1
2
  export type ScrollBodyOption = {
2
3
  padding?: boolean | number;
3
4
  margin?: boolean | number;
4
5
  };
5
- export declare function useBodyScrollLock(initialState?: boolean | undefined): import("svelte-toolbelt").WritableBox<boolean>;
6
+ export declare function useBodyScrollLock(initialState?: boolean | undefined, restoreScrollDelay?: Getter<number | null>): import("svelte-toolbelt").WritableBox<boolean>;
@@ -1,6 +1,6 @@
1
1
  import { SvelteMap } from "svelte/reactivity";
2
- import { afterTick, box } from "svelte-toolbelt";
3
- import { Previous, watch } from "runed";
2
+ import { afterSleep, afterTick, box } from "svelte-toolbelt";
3
+ import { Previous } from "runed";
4
4
  import { untrack } from "svelte";
5
5
  import { isBrowser, isIOS } from "./is.js";
6
6
  import { addEventListener } from "./events.js";
@@ -80,9 +80,10 @@ const useBodyLockStackCount = createSharedHook(() => {
80
80
  resetBodyStyle,
81
81
  };
82
82
  });
83
- export function useBodyScrollLock(initialState) {
83
+ export function useBodyScrollLock(initialState, restoreScrollDelay = () => null) {
84
84
  const id = useId();
85
85
  const countState = useBodyLockStackCount();
86
+ const _restoreScrollDelay = $derived(restoreScrollDelay());
86
87
  countState.map.set(id, initialState ?? false);
87
88
  const locked = box.with(() => countState.map.get(id) ?? false, (v) => countState.map.set(id, v));
88
89
  $effect(() => {
@@ -90,7 +91,12 @@ export function useBodyScrollLock(initialState) {
90
91
  countState.map.delete(id);
91
92
  const length = Array.from(countState.map.values()).length;
92
93
  if (length === 0) {
93
- countState.resetBodyStyle();
94
+ if (_restoreScrollDelay === null) {
95
+ countState.resetBodyStyle();
96
+ }
97
+ else {
98
+ afterSleep(_restoreScrollDelay, () => countState.resetBodyStyle());
99
+ }
94
100
  }
95
101
  };
96
102
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bits-ui",
3
- "version": "1.0.0-next.31",
3
+ "version": "1.0.0-next.32",
4
4
  "license": "MIT",
5
5
  "repository": "github:huntabyte/bits-ui",
6
6
  "funding": "https://github.com/sponsors/huntabyte",