bits-ui 1.0.0-next.88 → 1.0.0-next.89
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/context-menu/components/context-menu-content-static.svelte +2 -0
- package/dist/bits/context-menu/components/context-menu-content.svelte +2 -0
- package/dist/bits/dropdown-menu/components/dropdown-menu-content-static.svelte +2 -0
- package/dist/bits/dropdown-menu/components/dropdown-menu-content.svelte +2 -0
- package/dist/bits/link-preview/link-preview.svelte.d.ts +0 -1
- package/dist/bits/link-preview/link-preview.svelte.js +7 -9
- package/dist/bits/menu/components/menu-content-static.svelte +2 -0
- package/dist/bits/menu/components/menu-content.svelte +2 -0
- package/dist/bits/menu/components/menu-sub-content-static.svelte +2 -1
- package/dist/bits/menu/components/menu-sub-content.svelte +2 -1
- package/dist/bits/menu/menu.svelte.d.ts +9 -10
- package/dist/bits/menu/menu.svelte.js +53 -81
- package/dist/bits/menu/utils.d.ts +0 -11
- package/dist/bits/menu/utils.js +0 -1
- package/dist/bits/tooltip/tooltip.svelte.js +9 -7
- package/dist/bits/utilities/focus-scope/use-focus-scope.svelte.js +4 -1
- package/dist/internal/box-auto-reset.svelte.d.ts +1 -1
- package/dist/internal/box-auto-reset.svelte.js +5 -2
- package/dist/internal/dev/visualize-grace-area.d.ts +5 -0
- package/dist/internal/dev/visualize-grace-area.js +28 -0
- package/dist/internal/dom.d.ts +1 -0
- package/dist/internal/dom.js +8 -0
- package/dist/internal/use-grace-area.svelte.d.ts +9 -2
- package/dist/internal/use-grace-area.svelte.js +35 -22
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
ref = $bindable(null),
|
|
17
17
|
loop = true,
|
|
18
18
|
onInteractOutside = noop,
|
|
19
|
+
onCloseAutoFocus = noop,
|
|
19
20
|
preventScroll = true,
|
|
20
21
|
// we need to explicitly pass this prop to the PopperLayer to override
|
|
21
22
|
// the default menu behavior of handling outside interactions on the trigger
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
() => ref,
|
|
32
33
|
(v) => (ref = v)
|
|
33
34
|
),
|
|
35
|
+
onCloseAutoFocus: box.with(() => onCloseAutoFocus),
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
const mergedProps = $derived(mergeProps(restProps, contentState.props));
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
ref = $bindable(null),
|
|
17
17
|
loop = true,
|
|
18
18
|
onInteractOutside = noop,
|
|
19
|
+
onCloseAutoFocus = noop,
|
|
19
20
|
preventScroll = true,
|
|
20
21
|
// we need to explicitly pass this prop to the PopperLayer to override
|
|
21
22
|
// the default menu behavior of handling outside interactions on the trigger
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
() => ref,
|
|
32
33
|
(v) => (ref = v)
|
|
33
34
|
),
|
|
35
|
+
onCloseAutoFocus: box.with(() => onCloseAutoFocus),
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
const mergedProps = $derived(mergeProps(restProps, contentState.props));
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
loop = true,
|
|
18
18
|
onInteractOutside = noop,
|
|
19
19
|
onEscapeKeydown = noop,
|
|
20
|
+
onCloseAutoFocus = noop,
|
|
20
21
|
forceMount = false,
|
|
21
22
|
...restProps
|
|
22
23
|
}: DropdownMenuContentStaticProps = $props();
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
() => ref,
|
|
29
30
|
(v) => (ref = v)
|
|
30
31
|
),
|
|
32
|
+
onCloseAutoFocus: box.with(() => onCloseAutoFocus),
|
|
31
33
|
});
|
|
32
34
|
|
|
33
35
|
const mergedProps = $derived(mergeProps(restProps, contentState.props));
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
loop = true,
|
|
18
18
|
onInteractOutside = noop,
|
|
19
19
|
onEscapeKeydown = noop,
|
|
20
|
+
onCloseAutoFocus = noop,
|
|
20
21
|
forceMount = false,
|
|
21
22
|
...restProps
|
|
22
23
|
}: DropdownMenuContentProps = $props();
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
() => ref,
|
|
29
30
|
(v) => (ref = v)
|
|
30
31
|
),
|
|
32
|
+
onCloseAutoFocus: box.with(() => onCloseAutoFocus),
|
|
31
33
|
});
|
|
32
34
|
|
|
33
35
|
const mergedProps = $derived(mergeProps(restProps, contentState.props));
|
|
@@ -15,7 +15,6 @@ declare class LinkPreviewRootState {
|
|
|
15
15
|
contentNode: HTMLElement | null;
|
|
16
16
|
contentMounted: boolean;
|
|
17
17
|
triggerNode: HTMLElement | null;
|
|
18
|
-
isPointerInTransit: import("svelte-toolbelt").WritableBox<boolean>;
|
|
19
18
|
isOpening: boolean;
|
|
20
19
|
constructor(opts: LinkPreviewRootStateProps);
|
|
21
20
|
clearTimeout(): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterSleep,
|
|
1
|
+
import { afterSleep, onDestroyEffect, useRefById } from "svelte-toolbelt";
|
|
2
2
|
import { Context, watch } from "runed";
|
|
3
3
|
import { on } from "svelte/events";
|
|
4
4
|
import { getAriaExpanded, getDataOpenClosed } from "../../internal/attrs.js";
|
|
@@ -16,7 +16,6 @@ class LinkPreviewRootState {
|
|
|
16
16
|
contentNode = $state(null);
|
|
17
17
|
contentMounted = $state(false);
|
|
18
18
|
triggerNode = $state(null);
|
|
19
|
-
isPointerInTransit = box(false);
|
|
20
19
|
isOpening = false;
|
|
21
20
|
constructor(opts) {
|
|
22
21
|
this.opts = opts;
|
|
@@ -152,14 +151,13 @@ class LinkPreviewContentState {
|
|
|
152
151
|
},
|
|
153
152
|
deps: () => this.root.opts.open.current,
|
|
154
153
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
onPointerExit(() => {
|
|
154
|
+
useGraceArea({
|
|
155
|
+
triggerNode: () => this.root.triggerNode,
|
|
156
|
+
contentNode: () => this.opts.ref.current,
|
|
157
|
+
enabled: () => this.root.opts.open.current,
|
|
158
|
+
onPointerExit: () => {
|
|
161
159
|
this.root.handleClose();
|
|
162
|
-
}
|
|
160
|
+
},
|
|
163
161
|
});
|
|
164
162
|
onDestroyEffect(() => {
|
|
165
163
|
this.root.clearTimeout();
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
loop = true,
|
|
18
18
|
onInteractOutside = noop,
|
|
19
19
|
onEscapeKeydown = noop,
|
|
20
|
+
onCloseAutoFocus: onCloseAutoFocusProp = noop,
|
|
20
21
|
forceMount = false,
|
|
21
22
|
...restProps
|
|
22
23
|
}: MenuContentStaticProps = $props();
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
() => ref,
|
|
29
30
|
(v) => (ref = v)
|
|
30
31
|
),
|
|
32
|
+
onCloseAutoFocus: box.with(() => onCloseAutoFocusProp),
|
|
31
33
|
});
|
|
32
34
|
|
|
33
35
|
const mergedProps = $derived(
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
loop = true,
|
|
18
18
|
onInteractOutside = noop,
|
|
19
19
|
onEscapeKeydown = noop,
|
|
20
|
+
onCloseAutoFocus: onCloseAutoFocusProp = noop,
|
|
20
21
|
forceMount = false,
|
|
21
22
|
...restProps
|
|
22
23
|
}: MenuContentProps = $props();
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
() => ref,
|
|
29
30
|
(v) => (ref = v)
|
|
30
31
|
),
|
|
32
|
+
onCloseAutoFocus: box.with(() => onCloseAutoFocusProp),
|
|
31
33
|
});
|
|
32
34
|
|
|
33
35
|
const mergedProps = $derived(
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
() => ref,
|
|
36
36
|
(v) => (ref = v)
|
|
37
37
|
),
|
|
38
|
+
onCloseAutoFocus: box.with(() => handleCloseAutoFocus),
|
|
39
|
+
isSub: true,
|
|
38
40
|
});
|
|
39
41
|
|
|
40
42
|
function onkeydown(e: KeyboardEvent) {
|
|
@@ -106,7 +108,6 @@
|
|
|
106
108
|
{...mergedProps}
|
|
107
109
|
{interactOutsideBehavior}
|
|
108
110
|
{escapeKeydownBehavior}
|
|
109
|
-
onCloseAutoFocus={handleCloseAutoFocus}
|
|
110
111
|
onOpenAutoFocus={handleOpenAutoFocus}
|
|
111
112
|
enabled={subContentState.parentMenu.opts.open.current}
|
|
112
113
|
onInteractOutside={handleInteractOutside}
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
() => ref,
|
|
37
37
|
(v) => (ref = v)
|
|
38
38
|
),
|
|
39
|
+
isSub: true,
|
|
40
|
+
onCloseAutoFocus: box.with(() => handleCloseAutoFocus),
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
function onkeydown(e: KeyboardEvent) {
|
|
@@ -108,7 +110,6 @@
|
|
|
108
110
|
{...mergedProps}
|
|
109
111
|
{interactOutsideBehavior}
|
|
110
112
|
{escapeKeydownBehavior}
|
|
111
|
-
onCloseAutoFocus={handleCloseAutoFocus}
|
|
112
113
|
onOpenAutoFocus={handleOpenAutoFocus}
|
|
113
114
|
enabled={subContentState.parentMenu.opts.open.current}
|
|
114
115
|
onInteractOutside={handleInteractOutside}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { IsFocusWithin } from "runed";
|
|
2
|
-
import { type GraceIntent } from "./utils.js";
|
|
3
1
|
import type { ReadableBoxedValues, WritableBoxedValues } from "../../internal/box.svelte.js";
|
|
4
2
|
import { CustomEventDispatcher } from "../../internal/events.js";
|
|
5
3
|
import type { AnyFn, BitsFocusEvent, BitsKeyboardEvent, BitsMouseEvent, BitsPointerEvent, WithRefProps } from "../../internal/types.js";
|
|
@@ -19,6 +17,7 @@ declare class MenuRootState {
|
|
|
19
17
|
readonly opts: MenuRootStateProps;
|
|
20
18
|
isUsingKeyboard: IsUsingKeyboard;
|
|
21
19
|
ignoreCloseAutoFocus: boolean;
|
|
20
|
+
isPointerInTransit: boolean;
|
|
22
21
|
constructor(opts: MenuRootStateProps);
|
|
23
22
|
getAttr(name: string): string;
|
|
24
23
|
}
|
|
@@ -39,26 +38,26 @@ declare class MenuMenuState {
|
|
|
39
38
|
}
|
|
40
39
|
type MenuContentStateProps = WithRefProps & ReadableBoxedValues<{
|
|
41
40
|
loop: boolean;
|
|
42
|
-
|
|
41
|
+
onCloseAutoFocus: (event: Event) => void;
|
|
42
|
+
}> & {
|
|
43
|
+
isSub?: boolean;
|
|
44
|
+
};
|
|
43
45
|
declare class MenuContentState {
|
|
44
46
|
#private;
|
|
45
47
|
readonly opts: MenuContentStateProps;
|
|
46
48
|
readonly parentMenu: MenuMenuState;
|
|
47
49
|
search: string;
|
|
48
|
-
pointerGraceTimer: number;
|
|
49
50
|
rovingFocusGroup: ReturnType<typeof useRovingFocus>;
|
|
50
51
|
mounted: boolean;
|
|
51
|
-
isFocusWithin: IsFocusWithin;
|
|
52
52
|
constructor(opts: MenuContentStateProps, parentMenu: MenuMenuState);
|
|
53
|
-
|
|
53
|
+
onCloseAutoFocus: (e: Event) => void;
|
|
54
54
|
handleTabKeyDown(e: BitsKeyboardEvent): void;
|
|
55
55
|
onkeydown(e: BitsKeyboardEvent): void;
|
|
56
56
|
onblur(e: BitsFocusEvent): void;
|
|
57
57
|
onfocus(_: BitsFocusEvent): void;
|
|
58
|
-
|
|
59
|
-
onItemEnter(e: BitsPointerEvent): boolean;
|
|
58
|
+
onItemEnter(): boolean;
|
|
60
59
|
onItemLeave(e: BitsPointerEvent): void;
|
|
61
|
-
onTriggerLeave(
|
|
60
|
+
onTriggerLeave(): boolean;
|
|
62
61
|
onOpenAutoFocus: (e: Event) => void;
|
|
63
62
|
handleInteractOutside(e: PointerEvent): void;
|
|
64
63
|
snippetProps: {
|
|
@@ -74,8 +73,8 @@ declare class MenuContentState {
|
|
|
74
73
|
readonly "data-state": "open" | "closed";
|
|
75
74
|
readonly onkeydown: (e: BitsKeyboardEvent) => void;
|
|
76
75
|
readonly onblur: (e: BitsFocusEvent) => void;
|
|
77
|
-
readonly onpointermove: (e: BitsPointerEvent) => void;
|
|
78
76
|
readonly onfocus: (_: BitsFocusEvent) => void;
|
|
77
|
+
readonly onCloseAutoFocus: (e: Event) => void;
|
|
79
78
|
readonly dir: Direction;
|
|
80
79
|
readonly style: {
|
|
81
80
|
readonly pointerEvents: "auto";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterTick, box, mergeProps, onDestroyEffect, useRefById } from "svelte-toolbelt";
|
|
2
|
-
import { Context,
|
|
2
|
+
import { Context, watch } from "runed";
|
|
3
3
|
import { FIRST_LAST_KEYS, LAST_KEYS, SELECTION_KEYS, SUB_OPEN_KEYS, getCheckedState, isMouseEvent, } from "./utils.js";
|
|
4
4
|
import { focusFirst } from "../../internal/focus.js";
|
|
5
5
|
import { CustomEventDispatcher } from "../../internal/events.js";
|
|
@@ -8,10 +8,11 @@ import { isElement, isElementOrSVGElement, isHTMLElement } from "../../internal/
|
|
|
8
8
|
import { useRovingFocus } from "../../internal/use-roving-focus.svelte.js";
|
|
9
9
|
import { kbd } from "../../internal/kbd.js";
|
|
10
10
|
import { getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaOrientation, getDataDisabled, getDataOpenClosed, } from "../../internal/attrs.js";
|
|
11
|
-
import { isPointerInGraceArea, makeHullFromElements } from "../../internal/polygon.js";
|
|
12
11
|
import { IsUsingKeyboard } from "../../index.js";
|
|
12
|
+
import { useGraceArea } from "../../internal/use-grace-area.svelte.js";
|
|
13
13
|
import { getTabbableFrom } from "../../internal/tabbable.js";
|
|
14
14
|
import { FocusScopeContext } from "../utilities/focus-scope/use-focus-scope.svelte.js";
|
|
15
|
+
import { isTabbable } from "tabbable";
|
|
15
16
|
export const CONTEXT_MENU_TRIGGER_ATTR = "data-context-menu-trigger";
|
|
16
17
|
const MenuRootContext = new Context("Menu.Root");
|
|
17
18
|
const MenuMenuContext = new Context("Menu.Root | Menu.Sub");
|
|
@@ -26,6 +27,7 @@ class MenuRootState {
|
|
|
26
27
|
opts;
|
|
27
28
|
isUsingKeyboard = new IsUsingKeyboard();
|
|
28
29
|
ignoreCloseAutoFocus = $state(false);
|
|
30
|
+
isPointerInTransit = $state(false);
|
|
29
31
|
constructor(opts) {
|
|
30
32
|
this.opts = opts;
|
|
31
33
|
}
|
|
@@ -45,8 +47,8 @@ class MenuMenuState {
|
|
|
45
47
|
this.root = root;
|
|
46
48
|
this.parentMenu = parentMenu;
|
|
47
49
|
if (parentMenu) {
|
|
48
|
-
watch(() => parentMenu.opts.open, (
|
|
49
|
-
if (
|
|
50
|
+
watch(() => parentMenu.opts.open.current, () => {
|
|
51
|
+
if (parentMenu.opts.open.current)
|
|
50
52
|
return;
|
|
51
53
|
this.opts.open.current = false;
|
|
52
54
|
});
|
|
@@ -66,23 +68,18 @@ class MenuContentState {
|
|
|
66
68
|
opts;
|
|
67
69
|
parentMenu;
|
|
68
70
|
search = $state("");
|
|
69
|
-
#timer =
|
|
70
|
-
pointerGraceTimer = $state(0);
|
|
71
|
-
#pointerGraceIntent = $state(null);
|
|
72
|
-
#pointerDir = $state("right");
|
|
73
|
-
#lastPointerX = $state(0);
|
|
71
|
+
#timer = 0;
|
|
74
72
|
#handleTypeaheadSearch;
|
|
75
73
|
rovingFocusGroup;
|
|
76
74
|
mounted = $state(false);
|
|
77
|
-
|
|
75
|
+
#isSub;
|
|
78
76
|
constructor(opts, parentMenu) {
|
|
79
77
|
this.opts = opts;
|
|
80
78
|
this.parentMenu = parentMenu;
|
|
81
|
-
|
|
82
|
-
this
|
|
79
|
+
parentMenu.contentId = opts.id;
|
|
80
|
+
this.#isSub = opts.isSub ?? false;
|
|
83
81
|
this.onkeydown = this.onkeydown.bind(this);
|
|
84
82
|
this.onblur = this.onblur.bind(this);
|
|
85
|
-
this.onpointermove = this.onpointermove.bind(this);
|
|
86
83
|
this.onfocus = this.onfocus.bind(this);
|
|
87
84
|
this.handleInteractOutside = this.handleInteractOutside.bind(this);
|
|
88
85
|
useRefById({
|
|
@@ -94,8 +91,17 @@ class MenuContentState {
|
|
|
94
91
|
}
|
|
95
92
|
},
|
|
96
93
|
});
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
useGraceArea({
|
|
95
|
+
contentNode: () => this.parentMenu.contentNode,
|
|
96
|
+
triggerNode: () => this.parentMenu.triggerNode,
|
|
97
|
+
enabled: () => this.parentMenu.opts.open.current &&
|
|
98
|
+
Boolean(this.parentMenu.triggerNode?.hasAttribute(this.parentMenu.root.getAttr("sub-trigger"))),
|
|
99
|
+
onPointerExit: () => {
|
|
100
|
+
this.parentMenu.opts.open.current = false;
|
|
101
|
+
},
|
|
102
|
+
setIsPointerInTransit: (value) => {
|
|
103
|
+
this.parentMenu.root.isPointerInTransit = value;
|
|
104
|
+
},
|
|
99
105
|
});
|
|
100
106
|
this.#handleTypeaheadSearch = useDOMTypeahead().handleTypeaheadSearch;
|
|
101
107
|
this.rovingFocusGroup = useRovingFocus({
|
|
@@ -116,6 +122,11 @@ class MenuContentState {
|
|
|
116
122
|
};
|
|
117
123
|
return MenuOpenEvent.listen(contentNode, handler);
|
|
118
124
|
});
|
|
125
|
+
$effect(() => {
|
|
126
|
+
if (!this.parentMenu.opts.open.current) {
|
|
127
|
+
window.clearTimeout(this.#timer);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
119
130
|
}
|
|
120
131
|
#getCandidateNodes() {
|
|
121
132
|
const node = this.parentMenu.contentNode;
|
|
@@ -124,13 +135,17 @@ class MenuContentState {
|
|
|
124
135
|
const candidates = Array.from(node.querySelectorAll(`[${this.parentMenu.root.getAttr("item")}]:not([data-disabled])`));
|
|
125
136
|
return candidates;
|
|
126
137
|
}
|
|
127
|
-
#isPointerMovingToSubmenu(
|
|
128
|
-
|
|
129
|
-
return isMovingTowards && isPointerInGraceArea(e, this.#pointerGraceIntent?.area);
|
|
130
|
-
}
|
|
131
|
-
onPointerGraceIntentChange(intent) {
|
|
132
|
-
this.#pointerGraceIntent = intent;
|
|
138
|
+
#isPointerMovingToSubmenu() {
|
|
139
|
+
return this.parentMenu.root.isPointerInTransit;
|
|
133
140
|
}
|
|
141
|
+
onCloseAutoFocus = (e) => {
|
|
142
|
+
this.opts.onCloseAutoFocus.current(e);
|
|
143
|
+
if (e.defaultPrevented || this.#isSub)
|
|
144
|
+
return;
|
|
145
|
+
if (this.parentMenu.triggerNode && isTabbable(this.parentMenu.triggerNode)) {
|
|
146
|
+
this.parentMenu.triggerNode.focus();
|
|
147
|
+
}
|
|
148
|
+
};
|
|
134
149
|
handleTabKeyDown(e) {
|
|
135
150
|
/**
|
|
136
151
|
* We locate the root `menu`'s trigger by going up the tree until
|
|
@@ -220,38 +235,20 @@ class MenuContentState {
|
|
|
220
235
|
return;
|
|
221
236
|
afterTick(() => this.rovingFocusGroup.focusFirstCandidate());
|
|
222
237
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
return;
|
|
226
|
-
const target = e.target;
|
|
227
|
-
if (!isElement(target))
|
|
228
|
-
return;
|
|
229
|
-
const pointerXHasChanged = this.#lastPointerX !== e.clientX;
|
|
230
|
-
const currentTarget = e.currentTarget;
|
|
231
|
-
if (!isElement(currentTarget))
|
|
232
|
-
return;
|
|
233
|
-
// We don't use `event.movementX` for this check because Safari will
|
|
234
|
-
// always return `0` on a pointer event.
|
|
235
|
-
if (currentTarget.contains(target) && pointerXHasChanged) {
|
|
236
|
-
const newDir = e.clientX > this.#lastPointerX ? "right" : "left";
|
|
237
|
-
this.#pointerDir = newDir;
|
|
238
|
-
this.#lastPointerX = e.clientX;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
onItemEnter(e) {
|
|
242
|
-
if (this.#isPointerMovingToSubmenu(e))
|
|
243
|
-
return true;
|
|
244
|
-
return false;
|
|
238
|
+
onItemEnter() {
|
|
239
|
+
return this.#isPointerMovingToSubmenu();
|
|
245
240
|
}
|
|
246
241
|
onItemLeave(e) {
|
|
247
|
-
if (this
|
|
242
|
+
if (e.currentTarget.hasAttribute(this.parentMenu.root.getAttr("sub-trigger")))
|
|
243
|
+
return;
|
|
244
|
+
if (this.#isPointerMovingToSubmenu() || this.parentMenu.root.isUsingKeyboard.current)
|
|
248
245
|
return;
|
|
249
246
|
const contentNode = this.parentMenu.contentNode;
|
|
250
247
|
contentNode?.focus();
|
|
251
248
|
this.rovingFocusGroup.setCurrentTabStopId("");
|
|
252
249
|
}
|
|
253
|
-
onTriggerLeave(
|
|
254
|
-
if (this.#isPointerMovingToSubmenu(
|
|
250
|
+
onTriggerLeave() {
|
|
251
|
+
if (this.#isPointerMovingToSubmenu())
|
|
255
252
|
return true;
|
|
256
253
|
return false;
|
|
257
254
|
}
|
|
@@ -283,8 +280,8 @@ class MenuContentState {
|
|
|
283
280
|
"data-state": getDataOpenClosed(this.parentMenu.opts.open.current),
|
|
284
281
|
onkeydown: this.onkeydown,
|
|
285
282
|
onblur: this.onblur,
|
|
286
|
-
onpointermove: this.onpointermove,
|
|
287
283
|
onfocus: this.onfocus,
|
|
284
|
+
onCloseAutoFocus: (e) => this.onCloseAutoFocus(e),
|
|
288
285
|
dir: this.parentMenu.root.opts.dir.current,
|
|
289
286
|
style: {
|
|
290
287
|
pointerEvents: "auto",
|
|
@@ -316,7 +313,7 @@ class MenuItemSharedState {
|
|
|
316
313
|
this.content.onItemLeave(e);
|
|
317
314
|
}
|
|
318
315
|
else {
|
|
319
|
-
const defaultPrevented = this.content.onItemEnter(
|
|
316
|
+
const defaultPrevented = this.content.onItemEnter();
|
|
320
317
|
if (defaultPrevented)
|
|
321
318
|
return;
|
|
322
319
|
const item = e.currentTarget;
|
|
@@ -326,13 +323,11 @@ class MenuItemSharedState {
|
|
|
326
323
|
}
|
|
327
324
|
}
|
|
328
325
|
onpointerleave(e) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.content.onItemLeave(e);
|
|
335
|
-
});
|
|
326
|
+
if (e.defaultPrevented)
|
|
327
|
+
return;
|
|
328
|
+
if (!isMouseEvent(e))
|
|
329
|
+
return;
|
|
330
|
+
this.content.onItemLeave(e);
|
|
336
331
|
}
|
|
337
332
|
onfocus(e) {
|
|
338
333
|
afterTick(() => {
|
|
@@ -366,7 +361,7 @@ class MenuItemSharedState {
|
|
|
366
361
|
class MenuItemState {
|
|
367
362
|
opts;
|
|
368
363
|
item;
|
|
369
|
-
#isPointerDown =
|
|
364
|
+
#isPointerDown = false;
|
|
370
365
|
root;
|
|
371
366
|
constructor(opts, item) {
|
|
372
367
|
this.opts = opts;
|
|
@@ -450,7 +445,7 @@ class MenuSubTriggerState {
|
|
|
450
445
|
this.#clearOpenTimer();
|
|
451
446
|
});
|
|
452
447
|
useRefById({
|
|
453
|
-
...
|
|
448
|
+
...item.opts,
|
|
454
449
|
onRefChange: (node) => {
|
|
455
450
|
this.submenu.triggerNode = node;
|
|
456
451
|
},
|
|
@@ -465,13 +460,9 @@ class MenuSubTriggerState {
|
|
|
465
460
|
onpointermove(e) {
|
|
466
461
|
if (!isMouseEvent(e))
|
|
467
462
|
return;
|
|
468
|
-
const defaultPrevented = this.content.onItemEnter(e);
|
|
469
|
-
if (defaultPrevented)
|
|
470
|
-
return;
|
|
471
463
|
if (!this.item.opts.disabled.current &&
|
|
472
464
|
!this.submenu.opts.open.current &&
|
|
473
465
|
!this.#openTimer) {
|
|
474
|
-
this.content.onPointerGraceIntentChange(null);
|
|
475
466
|
this.#openTimer = window.setTimeout(() => {
|
|
476
467
|
this.submenu.onOpen();
|
|
477
468
|
this.#clearOpenTimer();
|
|
@@ -482,25 +473,6 @@ class MenuSubTriggerState {
|
|
|
482
473
|
if (!isMouseEvent(e))
|
|
483
474
|
return;
|
|
484
475
|
this.#clearOpenTimer();
|
|
485
|
-
const contentNode = this.submenu.contentNode;
|
|
486
|
-
const subTriggerNode = this.item.opts.ref.current;
|
|
487
|
-
if (contentNode && subTriggerNode) {
|
|
488
|
-
const polygon = makeHullFromElements([subTriggerNode, contentNode]);
|
|
489
|
-
const side = contentNode?.dataset.side;
|
|
490
|
-
this.content.onPointerGraceIntentChange({
|
|
491
|
-
area: polygon,
|
|
492
|
-
side,
|
|
493
|
-
});
|
|
494
|
-
window.clearTimeout(this.content.pointerGraceTimer);
|
|
495
|
-
this.content.pointerGraceTimer = window.setTimeout(() => this.content.onPointerGraceIntentChange(null), 300);
|
|
496
|
-
}
|
|
497
|
-
else {
|
|
498
|
-
const defaultPrevented = this.content.onTriggerLeave(e);
|
|
499
|
-
if (defaultPrevented)
|
|
500
|
-
return;
|
|
501
|
-
// There's 100ms where the user may leave an item before the submenu was opened.
|
|
502
|
-
this.content.onPointerGraceIntentChange(null);
|
|
503
|
-
}
|
|
504
476
|
}
|
|
505
477
|
onkeydown(e) {
|
|
506
478
|
const isTypingAhead = this.content.search !== "";
|
|
@@ -753,7 +725,7 @@ class ContextMenuTriggerState {
|
|
|
753
725
|
virtualElement = box({
|
|
754
726
|
getBoundingClientRect: () => DOMRect.fromRect({ width: 0, height: 0, ...this.#point }),
|
|
755
727
|
});
|
|
756
|
-
#longPressTimer =
|
|
728
|
+
#longPressTimer = null;
|
|
757
729
|
constructor(opts, parentMenu) {
|
|
758
730
|
this.opts = opts;
|
|
759
731
|
this.parentMenu = parentMenu;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Direction } from "../../shared/index.js";
|
|
2
2
|
export type CheckedState = boolean | "indeterminate";
|
|
3
|
-
export declare const ITEM_NAME = "MenuItem";
|
|
4
3
|
export declare const SELECTION_KEYS: string[];
|
|
5
4
|
export declare const FIRST_KEYS: string[];
|
|
6
5
|
export declare const LAST_KEYS: string[];
|
|
@@ -9,14 +8,4 @@ export declare const SUB_OPEN_KEYS: Record<Direction, string[]>;
|
|
|
9
8
|
export declare const SUB_CLOSE_KEYS: Record<Direction, string[]>;
|
|
10
9
|
export declare function isIndeterminate(checked?: CheckedState): checked is "indeterminate";
|
|
11
10
|
export declare function getCheckedState(checked: CheckedState): "checked" | "unchecked" | "indeterminate";
|
|
12
|
-
export interface Point {
|
|
13
|
-
x: number;
|
|
14
|
-
y: number;
|
|
15
|
-
}
|
|
16
|
-
export type Polygon = Point[];
|
|
17
|
-
export type Side = "left" | "right";
|
|
18
|
-
export interface GraceIntent {
|
|
19
|
-
area: Polygon;
|
|
20
|
-
side: Side;
|
|
21
|
-
}
|
|
22
11
|
export declare function isMouseEvent(event: PointerEvent): boolean;
|
package/dist/bits/menu/utils.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { kbd } from "../../internal/kbd.js";
|
|
2
|
-
export const ITEM_NAME = "MenuItem";
|
|
3
2
|
export const SELECTION_KEYS = [kbd.ENTER, kbd.SPACE];
|
|
4
3
|
export const FIRST_KEYS = [kbd.ARROW_DOWN, kbd.PAGE_UP, kbd.HOME];
|
|
5
4
|
export const LAST_KEYS = [kbd.ARROW_UP, kbd.PAGE_DOWN, kbd.END];
|
|
@@ -204,14 +204,16 @@ class TooltipContentState {
|
|
|
204
204
|
},
|
|
205
205
|
deps: () => this.root.opts.open.current,
|
|
206
206
|
});
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
onPointerExit(() => {
|
|
207
|
+
useGraceArea({
|
|
208
|
+
triggerNode: () => this.root.triggerNode,
|
|
209
|
+
contentNode: () => this.root.contentNode,
|
|
210
|
+
enabled: () => this.root.opts.open.current && this.root.disableHoverableContent,
|
|
211
|
+
onPointerExit: () => {
|
|
213
212
|
this.root.handleClose();
|
|
214
|
-
}
|
|
213
|
+
},
|
|
214
|
+
setIsPointerInTransit: (value) => {
|
|
215
|
+
this.root.provider.isPointerInTransit.current = value;
|
|
216
|
+
},
|
|
215
217
|
});
|
|
216
218
|
onMountEffect(() => executeCallbacks(on(window, "scroll", (e) => {
|
|
217
219
|
const target = e.target;
|
|
@@ -6,6 +6,7 @@ import { focus, focusFirst, getTabbableCandidates, getTabbableEdges } from "../.
|
|
|
6
6
|
import { CustomEventDispatcher } from "../../../internal/events.js";
|
|
7
7
|
import { isHTMLElement } from "../../../internal/is.js";
|
|
8
8
|
import { kbd } from "../../../internal/kbd.js";
|
|
9
|
+
import { isTabbable } from "tabbable";
|
|
9
10
|
const AutoFocusOnMountEvent = new CustomEventDispatcher("focusScope.autoFocusOnMount", {
|
|
10
11
|
bubbles: false,
|
|
11
12
|
cancelable: true,
|
|
@@ -140,7 +141,9 @@ export function useFocusScope({ id, loop, enabled, onOpenAutoFocus, onCloseAutoF
|
|
|
140
141
|
const shouldIgnore = ctx.ignoreCloseAutoFocus;
|
|
141
142
|
afterSleep(0, () => {
|
|
142
143
|
if (!destroyEvent.defaultPrevented && prevFocusedElement && !shouldIgnore) {
|
|
143
|
-
focus(prevFocusedElement
|
|
144
|
+
focus(isTabbable(prevFocusedElement) ? prevFocusedElement : document.body, {
|
|
145
|
+
select: true,
|
|
146
|
+
});
|
|
144
147
|
}
|
|
145
148
|
focusScopeStack.remove(focusScope);
|
|
146
149
|
});
|
|
@@ -5,4 +5,4 @@ import { type WritableBox } from "svelte-toolbelt";
|
|
|
5
5
|
* @param defaultValue The value which will be set.
|
|
6
6
|
* @param afterMs A zero-or-greater delay in milliseconds.
|
|
7
7
|
*/
|
|
8
|
-
export declare function boxAutoReset<T>(defaultValue: T, afterMs?: number): WritableBox<T>;
|
|
8
|
+
export declare function boxAutoReset<T>(defaultValue: T, afterMs?: number, onChange?: (value: T) => void): WritableBox<T>;
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { box } from "svelte-toolbelt";
|
|
2
|
+
import { noop } from "./noop.js";
|
|
2
3
|
/**
|
|
3
4
|
* Creates a box which will be reset to the default value after some time.
|
|
4
5
|
*
|
|
5
6
|
* @param defaultValue The value which will be set.
|
|
6
7
|
* @param afterMs A zero-or-greater delay in milliseconds.
|
|
7
8
|
*/
|
|
8
|
-
export function boxAutoReset(defaultValue, afterMs = 10000) {
|
|
9
|
+
export function boxAutoReset(defaultValue, afterMs = 10000, onChange = noop) {
|
|
9
10
|
let timeout = null;
|
|
10
11
|
let value = $state(defaultValue);
|
|
11
12
|
function resetAfter() {
|
|
12
|
-
return setTimeout(() => {
|
|
13
|
+
return window.setTimeout(() => {
|
|
13
14
|
value = defaultValue;
|
|
15
|
+
onChange(defaultValue);
|
|
14
16
|
}, afterMs);
|
|
15
17
|
}
|
|
16
18
|
$effect(() => {
|
|
@@ -21,6 +23,7 @@ export function boxAutoReset(defaultValue, afterMs = 10000) {
|
|
|
21
23
|
});
|
|
22
24
|
return box.with(() => value, (v) => {
|
|
23
25
|
value = v;
|
|
26
|
+
onChange(v);
|
|
24
27
|
if (timeout)
|
|
25
28
|
clearTimeout(timeout);
|
|
26
29
|
timeout = resetAfter();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
let graceAreaElement = null;
|
|
2
|
+
let svgContainer = null;
|
|
3
|
+
/**
|
|
4
|
+
* Debugging utility to visualize the grace area of a floating layer component.
|
|
5
|
+
*/
|
|
6
|
+
export function visualizeGraceArea(graceArea) {
|
|
7
|
+
if (graceAreaElement) {
|
|
8
|
+
graceAreaElement.remove();
|
|
9
|
+
}
|
|
10
|
+
if (!svgContainer) {
|
|
11
|
+
svgContainer = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
12
|
+
svgContainer.style.position = "absolute";
|
|
13
|
+
svgContainer.style.top = "0";
|
|
14
|
+
svgContainer.style.left = "0";
|
|
15
|
+
svgContainer.style.width = "100%";
|
|
16
|
+
svgContainer.style.height = "100%";
|
|
17
|
+
svgContainer.style.pointerEvents = "none";
|
|
18
|
+
document.body.appendChild(svgContainer);
|
|
19
|
+
}
|
|
20
|
+
graceAreaElement = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
|
|
21
|
+
const pointsString = graceArea.map((p) => `${p.x},${p.y}`).join(" ");
|
|
22
|
+
graceAreaElement.setAttribute("points", pointsString);
|
|
23
|
+
graceAreaElement.setAttribute("fill", "rgba(255, 0, 0, 0.3)");
|
|
24
|
+
graceAreaElement.setAttribute("stroke", "red");
|
|
25
|
+
graceAreaElement.setAttribute("stroke-width", "1");
|
|
26
|
+
graceAreaElement.style.pointerEvents = "none";
|
|
27
|
+
svgContainer.appendChild(graceAreaElement);
|
|
28
|
+
}
|
package/dist/internal/dom.d.ts
CHANGED
|
@@ -7,3 +7,4 @@ export declare function getFirstNonCommentChild(element: HTMLElement | null): Ch
|
|
|
7
7
|
* into the DOM but visually appear inside the content.
|
|
8
8
|
*/
|
|
9
9
|
export declare function isClickTrulyOutside(event: PointerEvent, contentNode: HTMLElement): boolean;
|
|
10
|
+
export declare function getTarget(event: Event): EventTarget | null | undefined;
|
package/dist/internal/dom.js
CHANGED
|
@@ -28,3 +28,11 @@ export function isClickTrulyOutside(event, contentNode) {
|
|
|
28
28
|
const rect = contentNode.getBoundingClientRect();
|
|
29
29
|
return (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom);
|
|
30
30
|
}
|
|
31
|
+
export function getTarget(event) {
|
|
32
|
+
if ("composedPath" in event) {
|
|
33
|
+
return event.composedPath()[0];
|
|
34
|
+
}
|
|
35
|
+
// TS thinks `event` is of type never as it assumes all browsers support
|
|
36
|
+
// `composedPath()`, but browsers without shadow DOM don't.
|
|
37
|
+
return event.target;
|
|
38
|
+
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { type Getter } from "svelte-toolbelt";
|
|
2
|
-
|
|
2
|
+
interface UseGraceAreaOpts {
|
|
3
|
+
enabled: Getter<boolean>;
|
|
4
|
+
triggerNode: Getter<HTMLElement | null>;
|
|
5
|
+
contentNode: Getter<HTMLElement | null>;
|
|
6
|
+
onPointerExit: () => void;
|
|
7
|
+
setIsPointerInTransit?: (value: boolean) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function useGraceArea(opts: UseGraceAreaOpts): {
|
|
3
10
|
isPointerInTransit: import("svelte-toolbelt").WritableBox<boolean>;
|
|
4
|
-
onPointerExit: import("./create-event-hook.svelte.js").EventHookOn<void>;
|
|
5
11
|
};
|
|
12
|
+
export {};
|
|
@@ -2,14 +2,15 @@ import { executeCallbacks } from "svelte-toolbelt";
|
|
|
2
2
|
import { on } from "svelte/events";
|
|
3
3
|
import { watch } from "runed";
|
|
4
4
|
import { boxAutoReset } from "./box-auto-reset.svelte.js";
|
|
5
|
-
import { createEventHook } from "./create-event-hook.svelte.js";
|
|
6
5
|
import { isElement, isHTMLElement } from "./is.js";
|
|
7
|
-
export function useGraceArea(
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
6
|
+
export function useGraceArea(opts) {
|
|
7
|
+
const enabled = $derived(opts.enabled());
|
|
8
|
+
const isPointerInTransit = boxAutoReset(false, 300, (value) => {
|
|
9
|
+
if (enabled) {
|
|
10
|
+
opts.setIsPointerInTransit?.(value);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
11
13
|
let pointerGraceArea = $state(null);
|
|
12
|
-
const pointerExit = createEventHook();
|
|
13
14
|
function handleRemoveGraceArea() {
|
|
14
15
|
pointerGraceArea = null;
|
|
15
16
|
isPointerInTransit.current = false;
|
|
@@ -26,8 +27,8 @@ export function useGraceArea(getTriggerNode, getContentNode) {
|
|
|
26
27
|
pointerGraceArea = graceArea;
|
|
27
28
|
isPointerInTransit.current = true;
|
|
28
29
|
}
|
|
29
|
-
watch([
|
|
30
|
-
if (!triggerNode || !contentNode)
|
|
30
|
+
watch([opts.triggerNode, opts.contentNode, opts.enabled], ([triggerNode, contentNode, enabled]) => {
|
|
31
|
+
if (!triggerNode || !contentNode || !enabled)
|
|
31
32
|
return;
|
|
32
33
|
const handleTriggerLeave = (e) => {
|
|
33
34
|
handleCreateGraceArea(e, contentNode);
|
|
@@ -37,7 +38,7 @@ export function useGraceArea(getTriggerNode, getContentNode) {
|
|
|
37
38
|
};
|
|
38
39
|
return executeCallbacks(on(triggerNode, "pointerleave", handleTriggerLeave), on(contentNode, "pointerleave", handleContentLeave));
|
|
39
40
|
});
|
|
40
|
-
watch(() => pointerGraceArea, (
|
|
41
|
+
watch(() => pointerGraceArea, () => {
|
|
41
42
|
const handleTrackPointerGrace = (e) => {
|
|
42
43
|
if (!pointerGraceArea)
|
|
43
44
|
return;
|
|
@@ -45,21 +46,20 @@ export function useGraceArea(getTriggerNode, getContentNode) {
|
|
|
45
46
|
if (!isElement(target))
|
|
46
47
|
return;
|
|
47
48
|
const pointerPosition = { x: e.clientX, y: e.clientY };
|
|
48
|
-
const hasEnteredTarget = triggerNode?.contains(target) || contentNode?.contains(target);
|
|
49
|
+
const hasEnteredTarget = opts.triggerNode()?.contains(target) || opts.contentNode()?.contains(target);
|
|
49
50
|
const isPointerOutsideGraceArea = !isPointInPolygon(pointerPosition, pointerGraceArea);
|
|
50
51
|
if (hasEnteredTarget) {
|
|
51
52
|
handleRemoveGraceArea();
|
|
52
53
|
}
|
|
53
54
|
else if (isPointerOutsideGraceArea) {
|
|
54
55
|
handleRemoveGraceArea();
|
|
55
|
-
|
|
56
|
+
opts.onPointerExit();
|
|
56
57
|
}
|
|
57
58
|
};
|
|
58
59
|
return on(document, "pointermove", handleTrackPointerGrace);
|
|
59
60
|
});
|
|
60
61
|
return {
|
|
61
62
|
isPointerInTransit,
|
|
62
|
-
onPointerExit: pointerExit.on,
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
function getExitSideFromRect(point, rect) {
|
|
@@ -81,22 +81,35 @@ function getExitSideFromRect(point, rect) {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
function getPaddedExitPoints(exitPoint, exitSide, padding = 5) {
|
|
84
|
-
|
|
84
|
+
// we extend the tip of the exit point to make it easier to navigate without
|
|
85
|
+
// a minor jitter triggering a pointer exit
|
|
86
|
+
const tipPadding = padding * 1.5;
|
|
85
87
|
switch (exitSide) {
|
|
86
88
|
case "top":
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
return [
|
|
90
|
+
{ x: exitPoint.x - padding, y: exitPoint.y + padding },
|
|
91
|
+
{ x: exitPoint.x, y: exitPoint.y - tipPadding },
|
|
92
|
+
{ x: exitPoint.x + padding, y: exitPoint.y + padding },
|
|
93
|
+
];
|
|
89
94
|
case "bottom":
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
return [
|
|
96
|
+
{ x: exitPoint.x - padding, y: exitPoint.y - padding },
|
|
97
|
+
{ x: exitPoint.x, y: exitPoint.y + tipPadding },
|
|
98
|
+
{ x: exitPoint.x + padding, y: exitPoint.y - padding },
|
|
99
|
+
];
|
|
92
100
|
case "left":
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
return [
|
|
102
|
+
{ x: exitPoint.x + padding, y: exitPoint.y - padding },
|
|
103
|
+
{ x: exitPoint.x - tipPadding, y: exitPoint.y },
|
|
104
|
+
{ x: exitPoint.x + padding, y: exitPoint.y + padding },
|
|
105
|
+
];
|
|
95
106
|
case "right":
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
return [
|
|
108
|
+
{ x: exitPoint.x - padding, y: exitPoint.y - padding },
|
|
109
|
+
{ x: exitPoint.x + tipPadding, y: exitPoint.y },
|
|
110
|
+
{ x: exitPoint.x - padding, y: exitPoint.y + padding },
|
|
111
|
+
];
|
|
98
112
|
}
|
|
99
|
-
return paddedExitPoints;
|
|
100
113
|
}
|
|
101
114
|
function getPointsFromRect(rect) {
|
|
102
115
|
const { top, right, bottom, left } = rect;
|