bits-ui 2.8.0 → 2.8.1
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.
- package/dist/bits/alert-dialog/components/alert-dialog-content.svelte +6 -9
- package/dist/bits/calendar/components/calendar-next-button.svelte +3 -1
- package/dist/bits/calendar/components/calendar-prev-button.svelte +3 -1
- package/dist/bits/context-menu/components/context-menu-content.svelte +1 -1
- package/dist/bits/dialog/components/dialog-content.svelte +5 -7
- package/dist/bits/dropdown-menu/components/dropdown-menu-content.svelte +3 -2
- package/dist/bits/menu/menu.svelte.js +4 -14
- package/dist/bits/menubar/menubar.svelte.d.ts +0 -2
- package/dist/bits/menubar/menubar.svelte.js +0 -3
- package/dist/bits/navigation-menu/components/navigation-menu-link.svelte +2 -1
- package/dist/bits/navigation-menu/components/navigation-menu-trigger.svelte +2 -1
- package/dist/bits/utilities/focus-scope/focus-scope-manager.d.ts +12 -0
- package/dist/bits/utilities/focus-scope/focus-scope-manager.js +40 -0
- package/dist/bits/utilities/focus-scope/focus-scope.svelte +6 -8
- package/dist/bits/utilities/focus-scope/focus-scope.svelte.d.ts +1 -0
- package/dist/bits/utilities/focus-scope/focus-scope.svelte.js +204 -0
- package/dist/bits/utilities/focus-scope/types.d.ts +2 -6
- package/dist/bits/utilities/popper-layer/popper-layer-inner.svelte +2 -2
- package/dist/internal/focus.js +1 -1
- package/dist/internal/should-enable-focus-trap.d.ts +5 -0
- package/dist/internal/should-enable-focus-trap.js +5 -0
- package/package.json +2 -2
- package/dist/bits/utilities/focus-scope/focus-scope-stack.svelte.d.ts +0 -14
- package/dist/bits/utilities/focus-scope/focus-scope-stack.svelte.js +0 -50
- package/dist/bits/utilities/focus-scope/use-focus-scope.svelte.d.ts +0 -49
- package/dist/bits/utilities/focus-scope/use-focus-scope.svelte.js +0 -218
- package/dist/internal/should-trap-focus.d.ts +0 -6
- package/dist/internal/should-trap-focus.js +0 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { afterSleep, box, mergeProps } from "svelte-toolbelt";
|
|
3
3
|
import type { AlertDialogContentProps } from "../types.js";
|
|
4
4
|
import DismissibleLayer from "../../utilities/dismissible-layer/dismissible-layer.svelte";
|
|
5
5
|
import EscapeLayer from "../../utilities/escape-layer/escape-layer.svelte";
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { noop } from "../../../internal/noop.js";
|
|
11
11
|
import ScrollLock from "../../utilities/scroll-lock/scroll-lock.svelte";
|
|
12
12
|
import { DialogContentState } from "../../dialog/dialog.svelte.js";
|
|
13
|
-
import {
|
|
13
|
+
import { shouldEnableFocusTrap } from "../../../internal/should-enable-focus-trap.js";
|
|
14
14
|
|
|
15
15
|
const uid = $props.id();
|
|
16
16
|
|
|
@@ -51,25 +51,22 @@
|
|
|
51
51
|
<FocusScope
|
|
52
52
|
ref={contentState.opts.ref}
|
|
53
53
|
loop
|
|
54
|
-
trapFocus
|
|
54
|
+
{trapFocus}
|
|
55
|
+
enabled={shouldEnableFocusTrap({
|
|
55
56
|
forceMount,
|
|
56
57
|
present: contentState.root.opts.open.current,
|
|
57
|
-
trapFocus,
|
|
58
58
|
open: contentState.root.opts.open.current,
|
|
59
59
|
})}
|
|
60
|
-
{id}
|
|
61
60
|
onCloseAutoFocus={(e) => {
|
|
62
61
|
onCloseAutoFocus(e);
|
|
63
62
|
if (e.defaultPrevented) return;
|
|
64
|
-
contentState.root.triggerNode?.focus();
|
|
63
|
+
afterSleep(0, () => contentState.root.triggerNode?.focus());
|
|
65
64
|
}}
|
|
66
65
|
onOpenAutoFocus={(e) => {
|
|
67
66
|
onOpenAutoFocus(e);
|
|
68
67
|
if (e.defaultPrevented) return;
|
|
69
68
|
e.preventDefault();
|
|
70
|
-
|
|
71
|
-
contentState.opts.ref.current?.focus();
|
|
72
|
-
});
|
|
69
|
+
afterSleep(0, () => contentState.opts.ref.current?.focus());
|
|
73
70
|
}}
|
|
74
71
|
>
|
|
75
72
|
{#snippet focusScope({ props: focusScopeProps })}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
child,
|
|
12
12
|
id = createId(uid),
|
|
13
13
|
ref = $bindable(null),
|
|
14
|
+
// for safari
|
|
15
|
+
tabindex = 0,
|
|
14
16
|
...restProps
|
|
15
17
|
}: CalendarNextButtonProps = $props();
|
|
16
18
|
|
|
@@ -22,7 +24,7 @@
|
|
|
22
24
|
),
|
|
23
25
|
});
|
|
24
26
|
|
|
25
|
-
const mergedProps = $derived(mergeProps(restProps, nextButtonState.props));
|
|
27
|
+
const mergedProps = $derived(mergeProps(restProps, nextButtonState.props, { tabindex }));
|
|
26
28
|
</script>
|
|
27
29
|
|
|
28
30
|
{#if child}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
child,
|
|
12
12
|
id = createId(uid),
|
|
13
13
|
ref = $bindable(null),
|
|
14
|
+
// for safari
|
|
15
|
+
tabindex = 0,
|
|
14
16
|
...restProps
|
|
15
17
|
}: CalendarPrevButtonProps = $props();
|
|
16
18
|
|
|
@@ -22,7 +24,7 @@
|
|
|
22
24
|
),
|
|
23
25
|
});
|
|
24
26
|
|
|
25
|
-
const mergedProps = $derived(mergeProps(restProps, prevButtonState.props));
|
|
27
|
+
const mergedProps = $derived(mergeProps(restProps, prevButtonState.props, { tabindex }));
|
|
26
28
|
</script>
|
|
27
29
|
|
|
28
30
|
{#if child}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { box, mergeProps } from "svelte-toolbelt";
|
|
2
|
+
import { afterSleep, 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";
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
import { createId } from "../../../internal/create-id.js";
|
|
11
11
|
import { noop } from "../../../internal/noop.js";
|
|
12
12
|
import ScrollLock from "../../utilities/scroll-lock/scroll-lock.svelte";
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import { shouldEnableFocusTrap } from "../../../internal/should-enable-focus-trap.js";
|
|
15
14
|
const uid = $props.id();
|
|
16
15
|
|
|
17
16
|
let {
|
|
@@ -51,18 +50,17 @@
|
|
|
51
50
|
<FocusScope
|
|
52
51
|
ref={contentState.opts.ref}
|
|
53
52
|
loop
|
|
54
|
-
trapFocus
|
|
53
|
+
{trapFocus}
|
|
54
|
+
enabled={shouldEnableFocusTrap({
|
|
55
55
|
forceMount,
|
|
56
56
|
present: contentState.root.opts.open.current,
|
|
57
|
-
trapFocus,
|
|
58
57
|
open: contentState.root.opts.open.current,
|
|
59
58
|
})}
|
|
60
59
|
{onOpenAutoFocus}
|
|
61
|
-
{id}
|
|
62
60
|
onCloseAutoFocus={(e) => {
|
|
63
61
|
onCloseAutoFocus(e);
|
|
64
62
|
if (e.defaultPrevented) return;
|
|
65
|
-
contentState.root.triggerNode?.focus();
|
|
63
|
+
afterSleep(1, () => contentState.root.triggerNode?.focus());
|
|
66
64
|
}}
|
|
67
65
|
>
|
|
68
66
|
{#snippet focusScope({ props: focusScopeProps })}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
onEscapeKeydown = noop,
|
|
21
21
|
onCloseAutoFocus = noop,
|
|
22
22
|
forceMount = false,
|
|
23
|
+
trapFocus = false,
|
|
23
24
|
...restProps
|
|
24
25
|
}: DropdownMenuContentProps = $props();
|
|
25
26
|
|
|
@@ -57,7 +58,7 @@
|
|
|
57
58
|
enabled={contentState.parentMenu.opts.open.current}
|
|
58
59
|
onInteractOutside={handleInteractOutside}
|
|
59
60
|
onEscapeKeydown={handleEscapeKeydown}
|
|
60
|
-
trapFocus
|
|
61
|
+
{trapFocus}
|
|
61
62
|
{loop}
|
|
62
63
|
forceMount={true}
|
|
63
64
|
{id}
|
|
@@ -85,7 +86,7 @@
|
|
|
85
86
|
open={contentState.parentMenu.opts.open.current}
|
|
86
87
|
onInteractOutside={handleInteractOutside}
|
|
87
88
|
onEscapeKeydown={handleEscapeKeydown}
|
|
88
|
-
trapFocus
|
|
89
|
+
{trapFocus}
|
|
89
90
|
{loop}
|
|
90
91
|
forceMount={false}
|
|
91
92
|
{id}
|
|
@@ -8,9 +8,7 @@ import { kbd } from "../../internal/kbd.js";
|
|
|
8
8
|
import { createBitsAttrs, getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaOrientation, getDataDisabled, getDataOpenClosed, } from "../../internal/attrs.js";
|
|
9
9
|
import { IsUsingKeyboard } from "../../index.js";
|
|
10
10
|
import { getTabbableFrom } from "../../internal/tabbable.js";
|
|
11
|
-
import { FocusScopeContext } from "../utilities/focus-scope/use-focus-scope.svelte.js";
|
|
12
11
|
import { isTabbable } from "tabbable";
|
|
13
|
-
import { untrack } from "svelte";
|
|
14
12
|
import { DOMTypeahead } from "../../internal/dom-typeahead.svelte.js";
|
|
15
13
|
import { RovingFocusGroup } from "../../internal/roving-focus-group.js";
|
|
16
14
|
import { GraceArea } from "../../internal/grace-area.svelte.js";
|
|
@@ -47,11 +45,6 @@ export const menuAttrs = createBitsAttrs({
|
|
|
47
45
|
export class MenuRootState {
|
|
48
46
|
static create(opts) {
|
|
49
47
|
const root = new MenuRootState(opts);
|
|
50
|
-
FocusScopeContext.set({
|
|
51
|
-
get ignoreCloseAutoFocus() {
|
|
52
|
-
return root.ignoreCloseAutoFocus;
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
48
|
return MenuRootContext.set(root);
|
|
56
49
|
}
|
|
57
50
|
opts;
|
|
@@ -196,9 +189,8 @@ export class MenuContentState {
|
|
|
196
189
|
rootMenu = rootMenu.parentMenu;
|
|
197
190
|
}
|
|
198
191
|
// if for some unforeseen reason the root menu has no trigger, we bail
|
|
199
|
-
if (!rootMenu.triggerNode)
|
|
192
|
+
if (!rootMenu.triggerNode)
|
|
200
193
|
return;
|
|
201
|
-
}
|
|
202
194
|
// cancel default tab behavior
|
|
203
195
|
e.preventDefault();
|
|
204
196
|
// find the next/previous tabbable
|
|
@@ -328,11 +320,9 @@ export class MenuContentState {
|
|
|
328
320
|
pointerEvents: "auto",
|
|
329
321
|
},
|
|
330
322
|
...attachRef(this.opts.ref, (v) => {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
});
|
|
323
|
+
if (this.parentMenu.contentNode !== v) {
|
|
324
|
+
this.parentMenu.contentNode = v;
|
|
325
|
+
}
|
|
336
326
|
}),
|
|
337
327
|
}));
|
|
338
328
|
popperProps = {
|
|
@@ -2,7 +2,6 @@ import { type ReadableBox, type ReadableBoxedValues, type WritableBoxedValues }
|
|
|
2
2
|
import type { InteractOutsideBehaviorType } from "../utilities/dismissible-layer/types.js";
|
|
3
3
|
import type { Direction } from "../../shared/index.js";
|
|
4
4
|
import type { OnChangeFn, WithRefOpts } from "../../internal/types.js";
|
|
5
|
-
import { type FocusScopeContextValue } from "../utilities/focus-scope/use-focus-scope.svelte.js";
|
|
6
5
|
import type { FocusEventHandler, KeyboardEventHandler, PointerEventHandler } from "svelte/elements";
|
|
7
6
|
import { RovingFocusGroup } from "../../internal/roving-focus-group.js";
|
|
8
7
|
interface MenubarRootStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
|
@@ -111,7 +110,6 @@ export declare class MenubarContentState {
|
|
|
111
110
|
readonly opts: MenubarContentStateOpts;
|
|
112
111
|
readonly menu: MenubarMenuState;
|
|
113
112
|
readonly root: MenubarRootState;
|
|
114
|
-
focusScopeContext: FocusScopeContextValue;
|
|
115
113
|
constructor(opts: MenubarContentStateOpts, menu: MenubarMenuState);
|
|
116
114
|
onCloseAutoFocus: (e: Event) => void;
|
|
117
115
|
onFocusOutside: (e: FocusEvent) => void;
|
|
@@ -3,7 +3,6 @@ import { Context, watch } from "runed";
|
|
|
3
3
|
import { createBitsAttrs, getAriaExpanded, getDataDisabled, getDataOpenClosed, } from "../../internal/attrs.js";
|
|
4
4
|
import { kbd } from "../../internal/kbd.js";
|
|
5
5
|
import { wrapArray } from "../../internal/arrays.js";
|
|
6
|
-
import { FocusScopeContext, } from "../utilities/focus-scope/use-focus-scope.svelte.js";
|
|
7
6
|
import { onMount } from "svelte";
|
|
8
7
|
import { getFloatingContentCSSVars } from "../../internal/floating-svelte/floating-utils.svelte";
|
|
9
8
|
import { RovingFocusGroup } from "../../internal/roving-focus-group.js";
|
|
@@ -215,12 +214,10 @@ export class MenubarContentState {
|
|
|
215
214
|
opts;
|
|
216
215
|
menu;
|
|
217
216
|
root;
|
|
218
|
-
focusScopeContext;
|
|
219
217
|
constructor(opts, menu) {
|
|
220
218
|
this.opts = opts;
|
|
221
219
|
this.menu = menu;
|
|
222
220
|
this.root = menu.root;
|
|
223
|
-
this.focusScopeContext = FocusScopeContext.get();
|
|
224
221
|
}
|
|
225
222
|
onCloseAutoFocus = (e) => {
|
|
226
223
|
this.opts.onCloseAutoFocus.current(e);
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
children,
|
|
15
15
|
active = false,
|
|
16
16
|
onSelect = noop,
|
|
17
|
+
tabindex = 0,
|
|
17
18
|
...restProps
|
|
18
19
|
}: NavigationMenuLinkProps = $props();
|
|
19
20
|
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
onSelect: box.with(() => onSelect),
|
|
28
29
|
});
|
|
29
30
|
|
|
30
|
-
const mergedProps = $derived(mergeProps(restProps, linkState.props));
|
|
31
|
+
const mergedProps = $derived(mergeProps(restProps, linkState.props, { tabindex }));
|
|
31
32
|
</script>
|
|
32
33
|
|
|
33
34
|
{#if child}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
children,
|
|
15
15
|
child,
|
|
16
16
|
ref = $bindable(null),
|
|
17
|
+
tabindex = 0,
|
|
17
18
|
...restProps
|
|
18
19
|
}: NavigationMenuTriggerProps = $props();
|
|
19
20
|
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
),
|
|
27
28
|
});
|
|
28
29
|
|
|
29
|
-
const mergedProps = $derived(mergeProps(restProps, triggerState.props));
|
|
30
|
+
const mergedProps = $derived(mergeProps(restProps, triggerState.props, { tabindex }));
|
|
30
31
|
</script>
|
|
31
32
|
|
|
32
33
|
{#if child}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FocusScope } from "./focus-scope.svelte.js";
|
|
2
|
+
export declare class FocusScopeManager {
|
|
3
|
+
#private;
|
|
4
|
+
static instance: FocusScopeManager;
|
|
5
|
+
static getInstance(): FocusScopeManager;
|
|
6
|
+
register(scope: FocusScope): void;
|
|
7
|
+
unregister(scope: FocusScope): void;
|
|
8
|
+
getActive(): FocusScope | undefined;
|
|
9
|
+
setFocusMemory(scope: FocusScope, element: HTMLElement): void;
|
|
10
|
+
getFocusMemory(scope: FocusScope): HTMLElement | undefined;
|
|
11
|
+
isActiveScope(scope: FocusScope): boolean;
|
|
12
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { box } from "svelte-toolbelt";
|
|
2
|
+
import { FocusScope } from "./focus-scope.svelte.js";
|
|
3
|
+
export class FocusScopeManager {
|
|
4
|
+
static instance;
|
|
5
|
+
#scopeStack = box([]);
|
|
6
|
+
#focusHistory = new WeakMap();
|
|
7
|
+
static getInstance() {
|
|
8
|
+
if (!this.instance) {
|
|
9
|
+
this.instance = new FocusScopeManager();
|
|
10
|
+
}
|
|
11
|
+
return this.instance;
|
|
12
|
+
}
|
|
13
|
+
register(scope) {
|
|
14
|
+
const current = this.getActive();
|
|
15
|
+
if (current && current !== scope) {
|
|
16
|
+
current.pause();
|
|
17
|
+
}
|
|
18
|
+
this.#scopeStack.current = this.#scopeStack.current.filter((s) => s !== scope);
|
|
19
|
+
this.#scopeStack.current.unshift(scope);
|
|
20
|
+
}
|
|
21
|
+
unregister(scope) {
|
|
22
|
+
this.#scopeStack.current = this.#scopeStack.current.filter((s) => s !== scope);
|
|
23
|
+
const next = this.getActive();
|
|
24
|
+
if (next) {
|
|
25
|
+
next.resume();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
getActive() {
|
|
29
|
+
return this.#scopeStack.current[0];
|
|
30
|
+
}
|
|
31
|
+
setFocusMemory(scope, element) {
|
|
32
|
+
this.#focusHistory.set(scope, element);
|
|
33
|
+
}
|
|
34
|
+
getFocusMemory(scope) {
|
|
35
|
+
return this.#focusHistory.get(scope);
|
|
36
|
+
}
|
|
37
|
+
isActiveScope(scope) {
|
|
38
|
+
return this.getActive() === scope;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { box } from "svelte-toolbelt";
|
|
3
3
|
import type { FocusScopeImplProps } from "./types.js";
|
|
4
|
-
import { useFocusScope } from "./use-focus-scope.svelte.js";
|
|
5
4
|
import { noop } from "../../../internal/noop.js";
|
|
5
|
+
import { FocusScope } from "./focus-scope.svelte.js";
|
|
6
6
|
|
|
7
7
|
let {
|
|
8
|
-
|
|
8
|
+
enabled = false,
|
|
9
9
|
trapFocus = false,
|
|
10
10
|
loop = false,
|
|
11
11
|
onCloseAutoFocus = noop,
|
|
12
12
|
onOpenAutoFocus = noop,
|
|
13
13
|
focusScope,
|
|
14
|
-
forceMount = false,
|
|
15
14
|
ref,
|
|
16
15
|
}: FocusScopeImplProps = $props();
|
|
17
16
|
|
|
18
|
-
const focusScopeState =
|
|
19
|
-
enabled: box.with(() =>
|
|
20
|
-
|
|
17
|
+
const focusScopeState = FocusScope.use({
|
|
18
|
+
enabled: box.with(() => enabled),
|
|
19
|
+
trap: box.with(() => trapFocus),
|
|
20
|
+
loop: loop,
|
|
21
21
|
onCloseAutoFocus: box.with(() => onCloseAutoFocus),
|
|
22
22
|
onOpenAutoFocus: box.with(() => onOpenAutoFocus),
|
|
23
|
-
id: box.with(() => id),
|
|
24
|
-
forceMount: box.with(() => forceMount),
|
|
25
23
|
ref,
|
|
26
24
|
});
|
|
27
25
|
</script>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FocusScopeImplProps } from "./types.js";
|
|
2
|
+
import { FocusScope } from "./focus-scope.svelte.js";
|
|
2
3
|
declare const FocusScope: import("svelte").Component<FocusScopeImplProps, {}, "">;
|
|
3
4
|
type FocusScope = ReturnType<typeof FocusScope>;
|
|
4
5
|
export default FocusScope;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { onDestroyEffect } from "svelte-toolbelt";
|
|
2
|
+
import { FocusScopeManager } from "./focus-scope-manager.js";
|
|
3
|
+
import { focusable, isFocusable, tabbable } from "tabbable";
|
|
4
|
+
import { on } from "svelte/events";
|
|
5
|
+
import { watch } from "runed";
|
|
6
|
+
export class FocusScope {
|
|
7
|
+
#paused = false;
|
|
8
|
+
#container = null;
|
|
9
|
+
#manager = FocusScopeManager.getInstance();
|
|
10
|
+
#cleanupFns = [];
|
|
11
|
+
#opts;
|
|
12
|
+
constructor(opts) {
|
|
13
|
+
this.#opts = opts;
|
|
14
|
+
}
|
|
15
|
+
get paused() {
|
|
16
|
+
return this.#paused;
|
|
17
|
+
}
|
|
18
|
+
pause() {
|
|
19
|
+
this.#paused = true;
|
|
20
|
+
}
|
|
21
|
+
resume() {
|
|
22
|
+
this.#paused = false;
|
|
23
|
+
}
|
|
24
|
+
#cleanup() {
|
|
25
|
+
for (const fn of this.#cleanupFns) {
|
|
26
|
+
fn();
|
|
27
|
+
}
|
|
28
|
+
this.#cleanupFns = [];
|
|
29
|
+
}
|
|
30
|
+
mount(container) {
|
|
31
|
+
if (this.#container) {
|
|
32
|
+
this.unmount();
|
|
33
|
+
}
|
|
34
|
+
this.#container = container;
|
|
35
|
+
this.#manager.register(this);
|
|
36
|
+
this.#setupEventListeners();
|
|
37
|
+
this.#handleOpenAutoFocus();
|
|
38
|
+
}
|
|
39
|
+
unmount() {
|
|
40
|
+
if (!this.#container)
|
|
41
|
+
return;
|
|
42
|
+
this.#cleanup();
|
|
43
|
+
// handle close auto-focus
|
|
44
|
+
this.#handleCloseAutoFocus();
|
|
45
|
+
this.#manager.unregister(this);
|
|
46
|
+
this.#container = null;
|
|
47
|
+
}
|
|
48
|
+
#handleOpenAutoFocus() {
|
|
49
|
+
if (!this.#container)
|
|
50
|
+
return;
|
|
51
|
+
const event = new CustomEvent("focusScope.onOpenAutoFocus", {
|
|
52
|
+
bubbles: false,
|
|
53
|
+
cancelable: true,
|
|
54
|
+
});
|
|
55
|
+
this.#opts.onOpenAutoFocus.current(event);
|
|
56
|
+
if (!event.defaultPrevented) {
|
|
57
|
+
requestAnimationFrame(() => {
|
|
58
|
+
if (!this.#container)
|
|
59
|
+
return;
|
|
60
|
+
const firstTabbable = this.#getFirstTabbable();
|
|
61
|
+
if (firstTabbable) {
|
|
62
|
+
firstTabbable.focus();
|
|
63
|
+
this.#manager.setFocusMemory(this, firstTabbable);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.#container.focus();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
#handleCloseAutoFocus() {
|
|
72
|
+
const event = new CustomEvent("focusScope.onCloseAutoFocus", {
|
|
73
|
+
bubbles: false,
|
|
74
|
+
cancelable: true,
|
|
75
|
+
});
|
|
76
|
+
this.#opts.onCloseAutoFocus.current(event);
|
|
77
|
+
if (!event.defaultPrevented) {
|
|
78
|
+
// return focus to previously focused element
|
|
79
|
+
const prevFocused = document.activeElement;
|
|
80
|
+
if (prevFocused && prevFocused !== document.body) {
|
|
81
|
+
prevFocused.focus();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
#setupEventListeners() {
|
|
86
|
+
if (!this.#container || !this.#opts.trap.current)
|
|
87
|
+
return;
|
|
88
|
+
const container = this.#container;
|
|
89
|
+
const doc = container.ownerDocument;
|
|
90
|
+
const handleFocus = (e) => {
|
|
91
|
+
if (this.#paused || !this.#manager.isActiveScope(this))
|
|
92
|
+
return;
|
|
93
|
+
const target = e.target;
|
|
94
|
+
if (!target)
|
|
95
|
+
return;
|
|
96
|
+
const isInside = container.contains(target);
|
|
97
|
+
if (isInside) {
|
|
98
|
+
// store last focused element
|
|
99
|
+
this.#manager.setFocusMemory(this, target);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// focus escaped - bring it back
|
|
103
|
+
const lastFocused = this.#manager.getFocusMemory(this);
|
|
104
|
+
if (lastFocused && container.contains(lastFocused) && isFocusable(lastFocused)) {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
lastFocused.focus();
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// fallback to first tabbable or first focusable or container
|
|
110
|
+
const firstTabbable = this.#getFirstTabbable();
|
|
111
|
+
const firstFocusable = this.#getAllFocusables()[0];
|
|
112
|
+
(firstTabbable || firstFocusable || container).focus();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const handleKeydown = (e) => {
|
|
117
|
+
if (!this.#opts.loop || this.#paused || e.key !== "Tab")
|
|
118
|
+
return;
|
|
119
|
+
if (!this.#manager.isActiveScope(this))
|
|
120
|
+
return;
|
|
121
|
+
const tabbables = this.#getTabbables();
|
|
122
|
+
if (tabbables.length < 2)
|
|
123
|
+
return;
|
|
124
|
+
const first = tabbables[0];
|
|
125
|
+
const last = tabbables[tabbables.length - 1];
|
|
126
|
+
if (!e.shiftKey && doc.activeElement === last) {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
first.focus();
|
|
129
|
+
}
|
|
130
|
+
else if (e.shiftKey && doc.activeElement === first) {
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
last.focus();
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
this.#cleanupFns.push(on(doc, "focusin", handleFocus, { capture: true }), on(container, "keydown", handleKeydown));
|
|
136
|
+
const observer = new MutationObserver(() => {
|
|
137
|
+
const lastFocused = this.#manager.getFocusMemory(this);
|
|
138
|
+
if (lastFocused && !container.contains(lastFocused)) {
|
|
139
|
+
// last focused element was removed
|
|
140
|
+
const firstTabbable = this.#getFirstTabbable();
|
|
141
|
+
const firstFocusable = this.#getAllFocusables()[0];
|
|
142
|
+
const elementToFocus = firstTabbable || firstFocusable;
|
|
143
|
+
if (elementToFocus) {
|
|
144
|
+
elementToFocus.focus();
|
|
145
|
+
this.#manager.setFocusMemory(this, elementToFocus);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
// no focusable elements left, focus container
|
|
149
|
+
container.focus();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
observer.observe(container, {
|
|
154
|
+
childList: true,
|
|
155
|
+
subtree: true,
|
|
156
|
+
});
|
|
157
|
+
this.#cleanupFns.push(() => observer.disconnect());
|
|
158
|
+
}
|
|
159
|
+
#getTabbables() {
|
|
160
|
+
if (!this.#container)
|
|
161
|
+
return [];
|
|
162
|
+
return tabbable(this.#container, {
|
|
163
|
+
includeContainer: false,
|
|
164
|
+
getShadowRoot: true,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
#getFirstTabbable() {
|
|
168
|
+
const tabbables = this.#getTabbables();
|
|
169
|
+
return tabbables[0] || null;
|
|
170
|
+
}
|
|
171
|
+
#getAllFocusables() {
|
|
172
|
+
if (!this.#container)
|
|
173
|
+
return [];
|
|
174
|
+
return focusable(this.#container, {
|
|
175
|
+
includeContainer: false,
|
|
176
|
+
getShadowRoot: true,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
static use(opts) {
|
|
180
|
+
let scope = null;
|
|
181
|
+
watch([() => opts.ref.current, () => opts.enabled.current], ([ref, enabled]) => {
|
|
182
|
+
if (ref && enabled) {
|
|
183
|
+
if (!scope) {
|
|
184
|
+
scope = new FocusScope(opts);
|
|
185
|
+
}
|
|
186
|
+
scope.mount(ref);
|
|
187
|
+
}
|
|
188
|
+
else if (scope) {
|
|
189
|
+
scope.unmount();
|
|
190
|
+
scope = null;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
onDestroyEffect(() => {
|
|
194
|
+
scope?.unmount();
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
get props() {
|
|
198
|
+
return {
|
|
199
|
+
tabindex: -1,
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Snippet } from "svelte";
|
|
2
|
-
import type { FocusScopeContainerProps } from "./use-focus-scope.svelte.js";
|
|
3
2
|
import type { EventCallback } from "../../../internal/events.js";
|
|
4
3
|
import type { ReadableBox } from "svelte-toolbelt";
|
|
5
4
|
export type FocusScopeProps = {
|
|
@@ -21,15 +20,11 @@ export type FocusScopeProps = {
|
|
|
21
20
|
trapFocus?: boolean;
|
|
22
21
|
};
|
|
23
22
|
export type FocusScopeImplProps = {
|
|
24
|
-
/**
|
|
25
|
-
* The ID of the focus scope container node.
|
|
26
|
-
*/
|
|
27
|
-
id: string;
|
|
28
23
|
/**
|
|
29
24
|
* The snippet to render the focus scope container with its props.
|
|
30
25
|
*/
|
|
31
26
|
focusScope?: Snippet<[{
|
|
32
|
-
props:
|
|
27
|
+
props: Record<string, unknown>;
|
|
33
28
|
}]>;
|
|
34
29
|
/**
|
|
35
30
|
* When `true` will loop through the tabbable elements in the focus scope.
|
|
@@ -39,5 +34,6 @@ export type FocusScopeImplProps = {
|
|
|
39
34
|
* Whether the content within the focus trap is being force mounted or not.
|
|
40
35
|
*/
|
|
41
36
|
forceMount?: boolean;
|
|
37
|
+
enabled: boolean;
|
|
42
38
|
ref: ReadableBox<HTMLElement | null>;
|
|
43
39
|
} & FocusScopeProps;
|
package/dist/internal/focus.js
CHANGED
|
@@ -34,7 +34,7 @@ export function focusWithoutScroll(element) {
|
|
|
34
34
|
* A utility function that focuses an element.
|
|
35
35
|
*/
|
|
36
36
|
export function focus(element, { select = false } = {}) {
|
|
37
|
-
if (!
|
|
37
|
+
if (!element || !element.focus)
|
|
38
38
|
return;
|
|
39
39
|
const doc = getDocument(element);
|
|
40
40
|
if (doc.activeElement === element)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bits-ui",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": "github:huntabyte/bits-ui",
|
|
6
6
|
"funding": "https://github.com/sponsors/huntabyte",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"css.escape": "^1.5.1",
|
|
47
47
|
"esm-env": "^1.1.2",
|
|
48
48
|
"runed": "^0.28.0",
|
|
49
|
-
"svelte-toolbelt": "^0.9.
|
|
49
|
+
"svelte-toolbelt": "^0.9.2",
|
|
50
50
|
"tabbable": "^6.2.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export interface FocusScopeAPI {
|
|
2
|
-
id: string;
|
|
3
|
-
paused: boolean;
|
|
4
|
-
pause: () => void;
|
|
5
|
-
resume: () => void;
|
|
6
|
-
isHandlingFocus: boolean;
|
|
7
|
-
}
|
|
8
|
-
export declare function createFocusScopeStack(): {
|
|
9
|
-
add(focusScope: FocusScopeAPI): void;
|
|
10
|
-
remove(focusScope: FocusScopeAPI): void;
|
|
11
|
-
readonly current: FocusScopeAPI[];
|
|
12
|
-
};
|
|
13
|
-
export declare function createFocusScopeAPI(): FocusScopeAPI;
|
|
14
|
-
export declare function removeLinks(items: HTMLElement[]): HTMLElement[];
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { box } from "svelte-toolbelt";
|
|
2
|
-
import { useId } from "../../../internal/use-id.js";
|
|
3
|
-
const focusStack = box([]);
|
|
4
|
-
export function createFocusScopeStack() {
|
|
5
|
-
return {
|
|
6
|
-
add(focusScope) {
|
|
7
|
-
const activeFocusScope = focusStack.current[0];
|
|
8
|
-
if (activeFocusScope && focusScope.id !== activeFocusScope.id) {
|
|
9
|
-
activeFocusScope.pause();
|
|
10
|
-
}
|
|
11
|
-
focusStack.current = removeFromFocusScopeArray(focusStack.current, focusScope);
|
|
12
|
-
focusStack.current.unshift(focusScope);
|
|
13
|
-
},
|
|
14
|
-
remove(focusScope) {
|
|
15
|
-
focusStack.current = removeFromFocusScopeArray(focusStack.current, focusScope);
|
|
16
|
-
focusStack.current[0]?.resume();
|
|
17
|
-
},
|
|
18
|
-
get current() {
|
|
19
|
-
return focusStack.current;
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
export function createFocusScopeAPI() {
|
|
24
|
-
let paused = $state(false);
|
|
25
|
-
let isHandlingFocus = $state(false);
|
|
26
|
-
return {
|
|
27
|
-
id: useId(),
|
|
28
|
-
get paused() {
|
|
29
|
-
return paused;
|
|
30
|
-
},
|
|
31
|
-
get isHandlingFocus() {
|
|
32
|
-
return isHandlingFocus;
|
|
33
|
-
},
|
|
34
|
-
set isHandlingFocus(value) {
|
|
35
|
-
isHandlingFocus = value;
|
|
36
|
-
},
|
|
37
|
-
pause() {
|
|
38
|
-
paused = true;
|
|
39
|
-
},
|
|
40
|
-
resume() {
|
|
41
|
-
paused = false;
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
function removeFromFocusScopeArray(arr, item) {
|
|
46
|
-
return [...arr].filter((i) => i.id !== item.id);
|
|
47
|
-
}
|
|
48
|
-
export function removeLinks(items) {
|
|
49
|
-
return items.filter((item) => item.tagName !== "A");
|
|
50
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { type ReadableBoxedValues } from "svelte-toolbelt";
|
|
2
|
-
import { Context } from "runed";
|
|
3
|
-
import { type EventCallback } from "../../../internal/events.js";
|
|
4
|
-
export type FocusScopeContextValue = {
|
|
5
|
-
ignoreCloseAutoFocus: boolean;
|
|
6
|
-
};
|
|
7
|
-
export declare const FocusScopeContext: Context<FocusScopeContextValue>;
|
|
8
|
-
type UseFocusScopeProps = ReadableBoxedValues<{
|
|
9
|
-
/**
|
|
10
|
-
* ID of the focus scope container node.
|
|
11
|
-
*/
|
|
12
|
-
id: string;
|
|
13
|
-
/**
|
|
14
|
-
* When `true` will loop through the tabbable elements in the focus scope.
|
|
15
|
-
*
|
|
16
|
-
* @defaultValue false
|
|
17
|
-
*/
|
|
18
|
-
loop: boolean;
|
|
19
|
-
/**
|
|
20
|
-
* Whether focus is trapped within the focus scope.
|
|
21
|
-
*
|
|
22
|
-
* @defaultValue false
|
|
23
|
-
*/
|
|
24
|
-
enabled: boolean;
|
|
25
|
-
/**
|
|
26
|
-
* Event handler called when auto-focusing onMount.
|
|
27
|
-
* Can be prevented.
|
|
28
|
-
*/
|
|
29
|
-
onOpenAutoFocus: EventCallback;
|
|
30
|
-
/**
|
|
31
|
-
* Event handler called when auto-focusing onDestroy.
|
|
32
|
-
* Can be prevented.
|
|
33
|
-
*/
|
|
34
|
-
onCloseAutoFocus: EventCallback;
|
|
35
|
-
/**
|
|
36
|
-
* Whether force mount is enabled or not
|
|
37
|
-
*/
|
|
38
|
-
forceMount: boolean;
|
|
39
|
-
ref: HTMLElement | null;
|
|
40
|
-
}>;
|
|
41
|
-
export type FocusScopeContainerProps = {
|
|
42
|
-
id: string;
|
|
43
|
-
tabindex: number;
|
|
44
|
-
onkeydown: EventCallback<KeyboardEvent>;
|
|
45
|
-
};
|
|
46
|
-
export declare function useFocusScope({ id, loop, enabled, onOpenAutoFocus, onCloseAutoFocus, forceMount, ref, }: UseFocusScopeProps): {
|
|
47
|
-
readonly props: FocusScopeContainerProps;
|
|
48
|
-
};
|
|
49
|
-
export {};
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { afterSleep, afterTick, DOMContext, executeCallbacks, } from "svelte-toolbelt";
|
|
2
|
-
import { Context, watch } from "runed";
|
|
3
|
-
import { on } from "svelte/events";
|
|
4
|
-
import { createFocusScopeAPI, createFocusScopeStack, removeLinks, } from "./focus-scope-stack.svelte.js";
|
|
5
|
-
import { focus, focusFirst, getTabbableCandidates, getTabbableEdges } from "../../../internal/focus.js";
|
|
6
|
-
import { CustomEventDispatcher } from "../../../internal/events.js";
|
|
7
|
-
import { isHTMLElement } from "../../../internal/is.js";
|
|
8
|
-
import { kbd } from "../../../internal/kbd.js";
|
|
9
|
-
import { isTabbable } from "tabbable";
|
|
10
|
-
const AutoFocusOnMountEvent = new CustomEventDispatcher("focusScope.autoFocusOnMount", {
|
|
11
|
-
bubbles: false,
|
|
12
|
-
cancelable: true,
|
|
13
|
-
});
|
|
14
|
-
const AutoFocusOnDestroyEvent = new CustomEventDispatcher("focusScope.autoFocusOnDestroy", {
|
|
15
|
-
bubbles: false,
|
|
16
|
-
cancelable: true,
|
|
17
|
-
});
|
|
18
|
-
export const FocusScopeContext = new Context("FocusScope");
|
|
19
|
-
export function useFocusScope({ id, loop, enabled, onOpenAutoFocus, onCloseAutoFocus, forceMount, ref, }) {
|
|
20
|
-
const focusScopeStack = createFocusScopeStack();
|
|
21
|
-
const focusScope = createFocusScopeAPI();
|
|
22
|
-
const ctx = FocusScopeContext.getOr({ ignoreCloseAutoFocus: false });
|
|
23
|
-
let lastFocusedElement = null;
|
|
24
|
-
const domContext = new DOMContext(ref);
|
|
25
|
-
function manageFocus(event) {
|
|
26
|
-
if (focusScope.paused || !ref.current || focusScope.isHandlingFocus)
|
|
27
|
-
return;
|
|
28
|
-
focusScope.isHandlingFocus = true;
|
|
29
|
-
try {
|
|
30
|
-
const target = event.target;
|
|
31
|
-
if (!isHTMLElement(target))
|
|
32
|
-
return;
|
|
33
|
-
const isWithinActiveScope = ref.current.contains(target);
|
|
34
|
-
if (event.type === "focusin") {
|
|
35
|
-
if (isWithinActiveScope) {
|
|
36
|
-
lastFocusedElement = target;
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
if (ctx.ignoreCloseAutoFocus)
|
|
40
|
-
return;
|
|
41
|
-
focus(lastFocusedElement, { select: true });
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
else if (event.type === "focusout") {
|
|
45
|
-
if (!isWithinActiveScope && !ctx.ignoreCloseAutoFocus) {
|
|
46
|
-
focus(lastFocusedElement, { select: true });
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
finally {
|
|
51
|
-
focusScope.isHandlingFocus = false;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Handles DOM mutations within the container. Specifically checks if the
|
|
56
|
-
* last known focused element inside the container has been removed. If so,
|
|
57
|
-
* and focus has escaped the container (likely moved to document.body),
|
|
58
|
-
* it refocuses the container itself to maintain the trap.
|
|
59
|
-
*/
|
|
60
|
-
function handleMutations(mutations) {
|
|
61
|
-
// if there's no record of a last focused el, or container isn't mounted, bail
|
|
62
|
-
if (!lastFocusedElement || !ref.current)
|
|
63
|
-
return;
|
|
64
|
-
// track if the last focused element was removed
|
|
65
|
-
let elementWasRemoved = false;
|
|
66
|
-
for (const mutation of mutations) {
|
|
67
|
-
// we only care about mutations where nodes were removed
|
|
68
|
-
if (mutation.type === "childList" && mutation.removedNodes.length > 0) {
|
|
69
|
-
// check if any removed nodes are the last focused element or contain it
|
|
70
|
-
for (const removedNode of mutation.removedNodes) {
|
|
71
|
-
if (removedNode === lastFocusedElement) {
|
|
72
|
-
elementWasRemoved = true;
|
|
73
|
-
// found it directly
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
// contains() only works on elements, so we need to check nodeType
|
|
77
|
-
if (removedNode.nodeType === Node.ELEMENT_NODE &&
|
|
78
|
-
removedNode.contains(lastFocusedElement)) {
|
|
79
|
-
elementWasRemoved = true;
|
|
80
|
-
// descendant found,
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
// if we've confirmed removal in any mutation, bail
|
|
86
|
-
if (elementWasRemoved)
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* If the element was removed and focus is now outside the container,
|
|
91
|
-
* (e.g., browser moved it to body), refocus the container.
|
|
92
|
-
*/
|
|
93
|
-
if (elementWasRemoved &&
|
|
94
|
-
ref.current &&
|
|
95
|
-
!ref.current.contains(domContext.getActiveElement())) {
|
|
96
|
-
focus(ref.current);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
watch([() => ref.current, () => enabled.current], ([container, enabled]) => {
|
|
100
|
-
if (!container || !enabled)
|
|
101
|
-
return;
|
|
102
|
-
const removeEvents = executeCallbacks(on(domContext.getDocument(), "focusin", manageFocus), on(domContext.getDocument(), "focusout", manageFocus));
|
|
103
|
-
const mutationObserver = new MutationObserver(handleMutations);
|
|
104
|
-
mutationObserver.observe(container, {
|
|
105
|
-
childList: true,
|
|
106
|
-
subtree: true,
|
|
107
|
-
attributes: false,
|
|
108
|
-
});
|
|
109
|
-
return () => {
|
|
110
|
-
removeEvents();
|
|
111
|
-
mutationObserver.disconnect();
|
|
112
|
-
};
|
|
113
|
-
});
|
|
114
|
-
watch([() => forceMount.current, () => ref.current], ([forceMount, container]) => {
|
|
115
|
-
if (forceMount)
|
|
116
|
-
return;
|
|
117
|
-
const prevFocusedElement = domContext.getActiveElement();
|
|
118
|
-
handleOpen(container, prevFocusedElement);
|
|
119
|
-
return () => {
|
|
120
|
-
if (!container)
|
|
121
|
-
return;
|
|
122
|
-
handleClose(prevFocusedElement);
|
|
123
|
-
};
|
|
124
|
-
});
|
|
125
|
-
watch([() => forceMount.current, () => ref.current, () => enabled.current], ([forceMount, container]) => {
|
|
126
|
-
if (!forceMount)
|
|
127
|
-
return;
|
|
128
|
-
const prevFocusedElement = domContext.getActiveElement();
|
|
129
|
-
handleOpen(container, prevFocusedElement);
|
|
130
|
-
return () => {
|
|
131
|
-
if (!container)
|
|
132
|
-
return;
|
|
133
|
-
handleClose(prevFocusedElement);
|
|
134
|
-
};
|
|
135
|
-
});
|
|
136
|
-
function handleOpen(container, prevFocusedElement) {
|
|
137
|
-
if (!container)
|
|
138
|
-
container = domContext.getElementById(id.current);
|
|
139
|
-
if (!container || !enabled.current)
|
|
140
|
-
return;
|
|
141
|
-
focusScopeStack.add(focusScope);
|
|
142
|
-
const hasFocusedCandidate = container.contains(prevFocusedElement);
|
|
143
|
-
if (!hasFocusedCandidate) {
|
|
144
|
-
const mountEvent = AutoFocusOnMountEvent.createEvent();
|
|
145
|
-
onOpenAutoFocus.current(mountEvent);
|
|
146
|
-
if (!mountEvent.defaultPrevented) {
|
|
147
|
-
afterTick(() => {
|
|
148
|
-
if (!container)
|
|
149
|
-
return;
|
|
150
|
-
const result = focusFirst(removeLinks(getTabbableCandidates(container)), {
|
|
151
|
-
select: true,
|
|
152
|
-
}, () => domContext.getActiveElement());
|
|
153
|
-
if (!result)
|
|
154
|
-
focus(container);
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
function handleClose(prevFocusedElement) {
|
|
160
|
-
const destroyEvent = AutoFocusOnDestroyEvent.createEvent();
|
|
161
|
-
onCloseAutoFocus.current?.(destroyEvent);
|
|
162
|
-
const shouldIgnore = ctx.ignoreCloseAutoFocus;
|
|
163
|
-
afterSleep(0, () => {
|
|
164
|
-
if (!destroyEvent.defaultPrevented && prevFocusedElement && !shouldIgnore) {
|
|
165
|
-
focus(isTabbable(prevFocusedElement)
|
|
166
|
-
? prevFocusedElement
|
|
167
|
-
: domContext.getDocument().body, {
|
|
168
|
-
select: true,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
focusScopeStack.remove(focusScope);
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
function handleKeydown(e) {
|
|
175
|
-
if (!enabled.current)
|
|
176
|
-
return;
|
|
177
|
-
if (!loop.current && !enabled.current)
|
|
178
|
-
return;
|
|
179
|
-
if (focusScope.paused)
|
|
180
|
-
return;
|
|
181
|
-
const isTabKey = e.key === kbd.TAB && !e.ctrlKey && !e.altKey && !e.metaKey;
|
|
182
|
-
const focusedElement = domContext.getActiveElement();
|
|
183
|
-
if (!(isTabKey && focusedElement))
|
|
184
|
-
return;
|
|
185
|
-
const container = ref.current;
|
|
186
|
-
if (!container)
|
|
187
|
-
return;
|
|
188
|
-
const [first, last] = getTabbableEdges(container);
|
|
189
|
-
const hasTabbableElementsInside = first && last;
|
|
190
|
-
if (!hasTabbableElementsInside) {
|
|
191
|
-
if (focusedElement === container) {
|
|
192
|
-
e.preventDefault();
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
if (!e.shiftKey && focusedElement === last) {
|
|
197
|
-
e.preventDefault();
|
|
198
|
-
if (loop.current)
|
|
199
|
-
focus(first, { select: true });
|
|
200
|
-
}
|
|
201
|
-
else if (e.shiftKey && focusedElement === first) {
|
|
202
|
-
e.preventDefault();
|
|
203
|
-
if (loop.current)
|
|
204
|
-
focus(last, { select: true });
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
const props = $derived.by(() => ({
|
|
209
|
-
id: id.current,
|
|
210
|
-
tabindex: -1,
|
|
211
|
-
onkeydown: handleKeydown,
|
|
212
|
-
}));
|
|
213
|
-
return {
|
|
214
|
-
get props() {
|
|
215
|
-
return props;
|
|
216
|
-
},
|
|
217
|
-
};
|
|
218
|
-
}
|