bits-ui 1.7.0 → 1.8.0
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/navigation-menu/components/navigation-menu-item.svelte +2 -0
- package/dist/bits/navigation-menu/components/navigation-menu-viewport.svelte +2 -0
- package/dist/bits/navigation-menu/navigation-menu.svelte.d.ts +29 -17
- package/dist/bits/navigation-menu/navigation-menu.svelte.js +94 -24
- package/dist/bits/navigation-menu/types.d.ts +14 -7
- package/dist/bits/slider/components/slider.svelte +2 -0
- package/dist/bits/slider/slider.svelte.d.ts +2 -1
- package/dist/bits/slider/slider.svelte.js +4 -0
- package/dist/bits/slider/types.d.ts +7 -1
- package/dist/shared/index.d.ts +7 -0
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
ref = $bindable(null),
|
|
11
11
|
child,
|
|
12
12
|
children,
|
|
13
|
+
openOnHover = true,
|
|
13
14
|
...restProps
|
|
14
15
|
}: NavigationMenuItemProps = $props();
|
|
15
16
|
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
(v) => (ref = v)
|
|
21
22
|
),
|
|
22
23
|
value: box.with(() => value),
|
|
24
|
+
openOnHover: box.with(() => openOnHover),
|
|
23
25
|
});
|
|
24
26
|
|
|
25
27
|
const mergedProps = $derived(mergeProps(restProps, itemState.props));
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { useId } from "../../../internal/use-id.js";
|
|
5
5
|
import PresenceLayer from "../../utilities/presence-layer/presence-layer.svelte";
|
|
6
6
|
import { box, mergeProps } from "svelte-toolbelt";
|
|
7
|
+
import { Mounted } from "../../utilities/index.js";
|
|
7
8
|
|
|
8
9
|
let {
|
|
9
10
|
id = useId(),
|
|
@@ -34,5 +35,6 @@
|
|
|
34
35
|
{@render children?.()}
|
|
35
36
|
</div>
|
|
36
37
|
{/if}
|
|
38
|
+
<Mounted bind:mounted={viewportState.mounted} />
|
|
37
39
|
{/snippet}
|
|
38
40
|
</PresenceLayer>
|
|
@@ -9,6 +9,7 @@ import { SvelteMap } from "svelte/reactivity";
|
|
|
9
9
|
import { type Direction, type Orientation } from "../../shared/index.js";
|
|
10
10
|
import type { BitsFocusEvent, BitsKeyboardEvent, BitsMouseEvent, BitsPointerEvent } from "../../internal/types.js";
|
|
11
11
|
import { useRovingFocus } from "../../internal/use-roving-focus.svelte.js";
|
|
12
|
+
import type { FocusEventHandler, KeyboardEventHandler, MouseEventHandler, PointerEventHandler } from "svelte/elements";
|
|
12
13
|
type NavigationMenuProviderStateProps = ReadableBoxedValues<{
|
|
13
14
|
dir: Direction;
|
|
14
15
|
orientation: Orientation;
|
|
@@ -18,11 +19,11 @@ type NavigationMenuProviderStateProps = ReadableBoxedValues<{
|
|
|
18
19
|
previousValue: string;
|
|
19
20
|
}> & {
|
|
20
21
|
isRootMenu: boolean;
|
|
21
|
-
onTriggerEnter: (itemValue: string) => void;
|
|
22
|
+
onTriggerEnter: (itemValue: string, itemState: NavigationMenuItemState | null) => void;
|
|
22
23
|
onTriggerLeave?: () => void;
|
|
23
24
|
onContentEnter?: () => void;
|
|
24
25
|
onContentLeave?: () => void;
|
|
25
|
-
onItemSelect: (itemValue: string) => void;
|
|
26
|
+
onItemSelect: (itemValue: string, itemState: NavigationMenuItemState | null) => void;
|
|
26
27
|
onItemDismiss: () => void;
|
|
27
28
|
};
|
|
28
29
|
declare class NavigationMenuProviderState {
|
|
@@ -36,7 +37,10 @@ declare class NavigationMenuProviderState {
|
|
|
36
37
|
onContentLeave: () => void;
|
|
37
38
|
onItemSelect: NavigationMenuProviderStateProps["onItemSelect"];
|
|
38
39
|
onItemDismiss: NavigationMenuProviderStateProps["onItemDismiss"];
|
|
40
|
+
activeItem: NavigationMenuItemState | null;
|
|
41
|
+
prevActiveItem: NavigationMenuItemState | null;
|
|
39
42
|
constructor(opts: NavigationMenuProviderStateProps);
|
|
43
|
+
setActiveItem: (item: NavigationMenuItemState | null) => void;
|
|
40
44
|
}
|
|
41
45
|
type NavigationMenuRootStateProps = WithRefProps<WritableBoxedValues<{
|
|
42
46
|
value: string;
|
|
@@ -53,7 +57,7 @@ declare class NavigationMenuRootState {
|
|
|
53
57
|
previousValue: WritableBox<string>;
|
|
54
58
|
isDelaySkipped: WritableBox<boolean>;
|
|
55
59
|
constructor(opts: NavigationMenuRootStateProps);
|
|
56
|
-
setValue: (newValue: string) => void;
|
|
60
|
+
setValue: (newValue: string, itemState: NavigationMenuItemState | null) => void;
|
|
57
61
|
props: {
|
|
58
62
|
readonly id: string;
|
|
59
63
|
readonly "data-orientation": "horizontal" | "vertical";
|
|
@@ -71,8 +75,9 @@ declare class NavigationMenuSubState {
|
|
|
71
75
|
readonly opts: NavigationMenuSubStateProps;
|
|
72
76
|
readonly context: NavigationMenuProviderState;
|
|
73
77
|
previousValue: WritableBox<string>;
|
|
78
|
+
subProvider: NavigationMenuProviderState;
|
|
74
79
|
constructor(opts: NavigationMenuSubStateProps, context: NavigationMenuProviderState);
|
|
75
|
-
setValue: (newValue: string) => void;
|
|
80
|
+
setValue: (newValue: string, itemState: NavigationMenuItemState | null) => void;
|
|
76
81
|
props: {
|
|
77
82
|
readonly id: string;
|
|
78
83
|
readonly "data-orientation": "horizontal" | "vertical";
|
|
@@ -102,6 +107,7 @@ declare class NavigationMenuListState {
|
|
|
102
107
|
}
|
|
103
108
|
type NavigationMenuItemStateProps = WithRefProps<ReadableBoxedValues<{
|
|
104
109
|
value: string;
|
|
110
|
+
openOnHover: boolean;
|
|
105
111
|
}>>;
|
|
106
112
|
export declare class NavigationMenuItemState {
|
|
107
113
|
#private;
|
|
@@ -147,13 +153,14 @@ declare class NavigationMenuTriggerState {
|
|
|
147
153
|
provider: NavigationMenuProviderState;
|
|
148
154
|
item: NavigationMenuItemState;
|
|
149
155
|
list: NavigationMenuListState;
|
|
156
|
+
sub: NavigationMenuSubState | null;
|
|
150
157
|
});
|
|
151
158
|
onpointerenter: (_: BitsPointerEvent<HTMLButtonElement>) => void;
|
|
152
|
-
onpointermove:
|
|
153
|
-
onpointerleave:
|
|
154
|
-
onclick:
|
|
155
|
-
onkeydown:
|
|
156
|
-
focusProxyOnFocus:
|
|
159
|
+
onpointermove: PointerEventHandler<HTMLElement>;
|
|
160
|
+
onpointerleave: PointerEventHandler<HTMLElement>;
|
|
161
|
+
onclick: MouseEventHandler<HTMLButtonElement>;
|
|
162
|
+
onkeydown: KeyboardEventHandler<HTMLButtonElement>;
|
|
163
|
+
focusProxyOnFocus: FocusEventHandler<HTMLElement>;
|
|
157
164
|
props: {
|
|
158
165
|
readonly id: string;
|
|
159
166
|
readonly disabled: boolean | null | undefined;
|
|
@@ -163,16 +170,16 @@ declare class NavigationMenuTriggerState {
|
|
|
163
170
|
readonly "aria-expanded": "true" | "false";
|
|
164
171
|
readonly "aria-controls": string | undefined;
|
|
165
172
|
readonly "data-navigation-menu-trigger": "";
|
|
166
|
-
readonly onpointermove:
|
|
167
|
-
readonly onpointerleave:
|
|
173
|
+
readonly onpointermove: PointerEventHandler<HTMLElement>;
|
|
174
|
+
readonly onpointerleave: PointerEventHandler<HTMLElement>;
|
|
168
175
|
readonly onpointerenter: (_: BitsPointerEvent<HTMLButtonElement>) => void;
|
|
169
|
-
readonly onclick:
|
|
170
|
-
readonly onkeydown:
|
|
176
|
+
readonly onclick: MouseEventHandler<HTMLButtonElement>;
|
|
177
|
+
readonly onkeydown: KeyboardEventHandler<HTMLButtonElement>;
|
|
171
178
|
};
|
|
172
179
|
focusProxyProps: {
|
|
173
180
|
readonly id: string;
|
|
174
181
|
readonly tabindex: 0;
|
|
175
|
-
readonly onfocus:
|
|
182
|
+
readonly onfocus: FocusEventHandler<HTMLElement>;
|
|
176
183
|
};
|
|
177
184
|
restructureSpanProps: {
|
|
178
185
|
readonly "aria-owns": string | undefined;
|
|
@@ -183,6 +190,7 @@ type NavigationMenuLinkStateProps = WithRefProps & ReadableBoxedValues<{
|
|
|
183
190
|
onSelect: (e: Event) => void;
|
|
184
191
|
}>;
|
|
185
192
|
declare class NavigationMenuLinkState {
|
|
193
|
+
#private;
|
|
186
194
|
readonly opts: NavigationMenuLinkStateProps;
|
|
187
195
|
readonly context: {
|
|
188
196
|
provider: NavigationMenuProviderState;
|
|
@@ -197,6 +205,8 @@ declare class NavigationMenuLinkState {
|
|
|
197
205
|
onkeydown: (e: BitsKeyboardEvent) => void;
|
|
198
206
|
onfocus: (_: BitsFocusEvent) => void;
|
|
199
207
|
onblur: (_: BitsFocusEvent) => void;
|
|
208
|
+
onpointerenter: PointerEventHandler<HTMLAnchorElement>;
|
|
209
|
+
onpointermove: PointerEventHandler<HTMLElement>;
|
|
200
210
|
props: {
|
|
201
211
|
readonly id: string;
|
|
202
212
|
readonly "data-active": "" | undefined;
|
|
@@ -206,6 +216,8 @@ declare class NavigationMenuLinkState {
|
|
|
206
216
|
readonly onkeydown: (e: BitsKeyboardEvent) => void;
|
|
207
217
|
readonly onfocus: (_: BitsFocusEvent) => void;
|
|
208
218
|
readonly onblur: (_: BitsFocusEvent) => void;
|
|
219
|
+
readonly onpointerenter: PointerEventHandler<HTMLAnchorElement>;
|
|
220
|
+
readonly onpointermove: PointerEventHandler<HTMLElement>;
|
|
209
221
|
readonly "data-navigation-menu-link": "";
|
|
210
222
|
};
|
|
211
223
|
}
|
|
@@ -266,11 +278,11 @@ declare class NavigationMenuContentState {
|
|
|
266
278
|
list: NavigationMenuListState;
|
|
267
279
|
});
|
|
268
280
|
onpointerenter: (_: BitsPointerEvent) => void;
|
|
269
|
-
onpointerleave:
|
|
281
|
+
onpointerleave: PointerEventHandler<HTMLElement>;
|
|
270
282
|
props: {
|
|
271
283
|
readonly id: string;
|
|
272
284
|
readonly onpointerenter: (_: BitsPointerEvent) => void;
|
|
273
|
-
readonly onpointerleave:
|
|
285
|
+
readonly onpointerleave: PointerEventHandler<HTMLElement>;
|
|
274
286
|
};
|
|
275
287
|
}
|
|
276
288
|
type MotionAttribute = "to-start" | "to-end" | "from-start" | "from-end";
|
|
@@ -309,6 +321,7 @@ declare class NavigationMenuViewportState {
|
|
|
309
321
|
viewportWidth: string | undefined;
|
|
310
322
|
viewportHeight: string | undefined;
|
|
311
323
|
activeContentValue: string;
|
|
324
|
+
mounted: boolean;
|
|
312
325
|
constructor(opts: NavigationMenuViewportImplStateProps, context: NavigationMenuProviderState);
|
|
313
326
|
props: {
|
|
314
327
|
readonly id: string;
|
|
@@ -338,5 +351,4 @@ export declare function useNavigationMenuLink(props: NavigationMenuLinkStateProp
|
|
|
338
351
|
export declare function useNavigationMenuContentImpl(props: NavigationMenuContentImplStateProps, itemState?: NavigationMenuItemState): NavigationMenuContentImplState;
|
|
339
352
|
export declare function useNavigationMenuViewport(props: NavigationMenuViewportImplStateProps): NavigationMenuViewportState;
|
|
340
353
|
export declare function useNavigationMenuIndicator(): NavigationMenuIndicatorState;
|
|
341
|
-
type BitsPointerEventHandler<T extends HTMLElement = HTMLElement> = (e: BitsPointerEvent<T>) => void;
|
|
342
354
|
export {};
|
|
@@ -38,6 +38,8 @@ class NavigationMenuProviderState {
|
|
|
38
38
|
onContentLeave = noop;
|
|
39
39
|
onItemSelect;
|
|
40
40
|
onItemDismiss;
|
|
41
|
+
activeItem = null;
|
|
42
|
+
prevActiveItem = null;
|
|
41
43
|
constructor(opts) {
|
|
42
44
|
this.opts = opts;
|
|
43
45
|
this.onTriggerEnter = opts.onTriggerEnter;
|
|
@@ -47,6 +49,10 @@ class NavigationMenuProviderState {
|
|
|
47
49
|
this.onItemDismiss = opts.onItemDismiss;
|
|
48
50
|
this.onItemSelect = opts.onItemSelect;
|
|
49
51
|
}
|
|
52
|
+
setActiveItem = (item) => {
|
|
53
|
+
this.prevActiveItem = this.activeItem;
|
|
54
|
+
this.activeItem = item;
|
|
55
|
+
};
|
|
50
56
|
}
|
|
51
57
|
class NavigationMenuRootState {
|
|
52
58
|
opts;
|
|
@@ -74,8 +80,8 @@ class NavigationMenuRootState {
|
|
|
74
80
|
orientation: this.opts.orientation,
|
|
75
81
|
rootNavigationMenuRef: this.opts.ref,
|
|
76
82
|
isRootMenu: true,
|
|
77
|
-
onTriggerEnter: (itemValue) => {
|
|
78
|
-
this.#onTriggerEnter(itemValue);
|
|
83
|
+
onTriggerEnter: (itemValue, itemState) => {
|
|
84
|
+
this.#onTriggerEnter(itemValue, itemState);
|
|
79
85
|
},
|
|
80
86
|
onTriggerLeave: this.#onTriggerLeave,
|
|
81
87
|
onContentEnter: this.#onContentEnter,
|
|
@@ -84,34 +90,44 @@ class NavigationMenuRootState {
|
|
|
84
90
|
onItemDismiss: this.#onItemDismiss,
|
|
85
91
|
});
|
|
86
92
|
}
|
|
87
|
-
#debouncedFn = useDebounce((val) => {
|
|
93
|
+
#debouncedFn = useDebounce((val, itemState) => {
|
|
88
94
|
// passing `undefined` meant to reset the debounce timer
|
|
89
95
|
if (typeof val === "string") {
|
|
90
|
-
this.setValue(val);
|
|
96
|
+
this.setValue(val, itemState);
|
|
91
97
|
}
|
|
92
98
|
}, () => this.#derivedDelay);
|
|
93
|
-
#onTriggerEnter = (itemValue) => {
|
|
94
|
-
this.#debouncedFn(itemValue);
|
|
99
|
+
#onTriggerEnter = (itemValue, itemState) => {
|
|
100
|
+
this.#debouncedFn(itemValue, itemState);
|
|
95
101
|
};
|
|
96
102
|
#onTriggerLeave = () => {
|
|
97
103
|
this.isDelaySkipped.current = false;
|
|
98
|
-
this.#debouncedFn("");
|
|
104
|
+
this.#debouncedFn("", null);
|
|
99
105
|
};
|
|
100
106
|
#onContentEnter = () => {
|
|
101
|
-
this.#debouncedFn();
|
|
107
|
+
this.#debouncedFn(undefined, null);
|
|
102
108
|
};
|
|
103
109
|
#onContentLeave = () => {
|
|
104
|
-
this
|
|
110
|
+
if (this.provider.activeItem &&
|
|
111
|
+
this.provider.activeItem.opts.openOnHover.current === false) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.#debouncedFn("", null);
|
|
105
115
|
};
|
|
106
|
-
#onItemSelect = (itemValue) => {
|
|
107
|
-
this.setValue(itemValue);
|
|
116
|
+
#onItemSelect = (itemValue, itemState) => {
|
|
117
|
+
this.setValue(itemValue, itemState);
|
|
108
118
|
};
|
|
109
119
|
#onItemDismiss = () => {
|
|
110
|
-
this.setValue("");
|
|
120
|
+
this.setValue("", null);
|
|
111
121
|
};
|
|
112
|
-
setValue = (newValue) => {
|
|
122
|
+
setValue = (newValue, itemState) => {
|
|
113
123
|
this.previousValue.current = this.opts.value.current;
|
|
114
124
|
this.opts.value.current = newValue;
|
|
125
|
+
this.provider.setActiveItem(itemState);
|
|
126
|
+
// When all menus are closed, we want to reset previousValue to prevent
|
|
127
|
+
// weird transitions from old positions when opening fresh
|
|
128
|
+
if (newValue === "") {
|
|
129
|
+
this.previousValue.current = "";
|
|
130
|
+
}
|
|
115
131
|
};
|
|
116
132
|
props = $derived.by(() => ({
|
|
117
133
|
id: this.opts.id.current,
|
|
@@ -125,11 +141,12 @@ class NavigationMenuSubState {
|
|
|
125
141
|
opts;
|
|
126
142
|
context;
|
|
127
143
|
previousValue = box("");
|
|
144
|
+
subProvider;
|
|
128
145
|
constructor(opts, context) {
|
|
129
146
|
this.opts = opts;
|
|
130
147
|
this.context = context;
|
|
131
148
|
useRefById(opts);
|
|
132
|
-
useNavigationMenuProvider({
|
|
149
|
+
this.subProvider = useNavigationMenuProvider({
|
|
133
150
|
isRootMenu: false,
|
|
134
151
|
value: this.opts.value,
|
|
135
152
|
dir: this.context.opts.dir,
|
|
@@ -137,12 +154,19 @@ class NavigationMenuSubState {
|
|
|
137
154
|
rootNavigationMenuRef: this.opts.ref,
|
|
138
155
|
onTriggerEnter: this.setValue,
|
|
139
156
|
onItemSelect: this.setValue,
|
|
140
|
-
onItemDismiss: () => this.setValue(""),
|
|
157
|
+
onItemDismiss: () => this.setValue("", null),
|
|
141
158
|
previousValue: this.previousValue,
|
|
142
159
|
});
|
|
143
160
|
}
|
|
144
|
-
setValue = (newValue) => {
|
|
161
|
+
setValue = (newValue, itemState) => {
|
|
162
|
+
this.previousValue.current = this.opts.value.current;
|
|
145
163
|
this.opts.value.current = newValue;
|
|
164
|
+
this.subProvider.setActiveItem(itemState);
|
|
165
|
+
// When all menus are closed, we want to reset previousValue to prevent
|
|
166
|
+
// weird transitions from old positions when opening fresh
|
|
167
|
+
if (newValue === "") {
|
|
168
|
+
this.previousValue.current = "";
|
|
169
|
+
}
|
|
146
170
|
};
|
|
147
171
|
props = $derived.by(() => ({
|
|
148
172
|
id: this.opts.id.current,
|
|
@@ -281,28 +305,30 @@ class NavigationMenuTriggerState {
|
|
|
281
305
|
if (this.opts.disabled.current ||
|
|
282
306
|
this.wasClickClose ||
|
|
283
307
|
this.itemContext.wasEscapeClose ||
|
|
284
|
-
this.hasPointerMoveOpened.current
|
|
308
|
+
this.hasPointerMoveOpened.current ||
|
|
309
|
+
!this.itemContext.opts.openOnHover.current) {
|
|
285
310
|
return;
|
|
286
311
|
}
|
|
287
|
-
this.context.onTriggerEnter(this.itemContext.opts.value.current);
|
|
312
|
+
this.context.onTriggerEnter(this.itemContext.opts.value.current, this.itemContext);
|
|
288
313
|
this.hasPointerMoveOpened.current = true;
|
|
289
314
|
});
|
|
290
315
|
onpointerleave = whenMouse(() => {
|
|
291
|
-
if (this.opts.disabled.current)
|
|
316
|
+
if (this.opts.disabled.current || !this.itemContext.opts.openOnHover.current)
|
|
292
317
|
return;
|
|
293
318
|
this.context.onTriggerLeave();
|
|
294
319
|
this.hasPointerMoveOpened.current = false;
|
|
295
320
|
});
|
|
296
|
-
onclick = (
|
|
321
|
+
onclick = () => {
|
|
297
322
|
// if opened via pointer move, we prevent the click event
|
|
298
323
|
if (this.hasPointerMoveOpened.current)
|
|
299
324
|
return;
|
|
300
|
-
const shouldClose = this.open &&
|
|
325
|
+
const shouldClose = this.open &&
|
|
326
|
+
(!this.itemContext.opts.openOnHover.current || this.context.opts.isRootMenu);
|
|
301
327
|
if (shouldClose) {
|
|
302
|
-
this.context.onItemSelect("");
|
|
328
|
+
this.context.onItemSelect("", null);
|
|
303
329
|
}
|
|
304
330
|
else if (!this.open) {
|
|
305
|
-
this.context.onItemSelect(this.itemContext.opts.value.current);
|
|
331
|
+
this.context.onItemSelect(this.itemContext.opts.value.current, this.itemContext);
|
|
306
332
|
}
|
|
307
333
|
this.wasClickClose = shouldClose;
|
|
308
334
|
};
|
|
@@ -386,6 +412,23 @@ class NavigationMenuLinkState {
|
|
|
386
412
|
onblur = (_) => {
|
|
387
413
|
this.isFocused = false;
|
|
388
414
|
};
|
|
415
|
+
#handlePointerDismiss = () => {
|
|
416
|
+
// only close submenu if this link is not inside the currently open submenu content
|
|
417
|
+
const currentlyOpenValue = this.context.provider.opts.value.current;
|
|
418
|
+
const isInsideOpenSubmenu = this.context.item.opts.value.current === currentlyOpenValue;
|
|
419
|
+
const activeItem = this.context.item.listContext.context.activeItem;
|
|
420
|
+
if (activeItem && !activeItem.opts.openOnHover.current)
|
|
421
|
+
return;
|
|
422
|
+
if (currentlyOpenValue && !isInsideOpenSubmenu) {
|
|
423
|
+
this.context.provider.onItemDismiss();
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
onpointerenter = () => {
|
|
427
|
+
this.#handlePointerDismiss();
|
|
428
|
+
};
|
|
429
|
+
onpointermove = whenMouse(() => {
|
|
430
|
+
this.#handlePointerDismiss();
|
|
431
|
+
});
|
|
389
432
|
props = $derived.by(() => ({
|
|
390
433
|
id: this.opts.id.current,
|
|
391
434
|
"data-active": this.opts.active.current ? "" : undefined,
|
|
@@ -395,6 +438,8 @@ class NavigationMenuLinkState {
|
|
|
395
438
|
onkeydown: this.onkeydown,
|
|
396
439
|
onfocus: this.onfocus,
|
|
397
440
|
onblur: this.onblur,
|
|
441
|
+
onpointerenter: this.onpointerenter,
|
|
442
|
+
onpointermove: this.onpointermove,
|
|
398
443
|
[NAVIGATION_MENU_LINK_ATTR]: "",
|
|
399
444
|
}));
|
|
400
445
|
}
|
|
@@ -497,6 +542,8 @@ class NavigationMenuContentState {
|
|
|
497
542
|
this.context.onContentEnter();
|
|
498
543
|
};
|
|
499
544
|
onpointerleave = whenMouse(() => {
|
|
545
|
+
if (!this.itemContext.opts.openOnHover.current)
|
|
546
|
+
return;
|
|
500
547
|
this.context.onContentLeave();
|
|
501
548
|
});
|
|
502
549
|
props = $derived.by(() => ({
|
|
@@ -520,6 +567,11 @@ class NavigationMenuContentImplState {
|
|
|
520
567
|
const prevIndex = values.indexOf(this.context.opts.previousValue.current);
|
|
521
568
|
const isSelected = this.itemContext.opts.value.current === this.context.opts.value.current;
|
|
522
569
|
const wasSelected = prevIndex === values.indexOf(this.itemContext.opts.value.current);
|
|
570
|
+
// When all menus are closed, we want to reset motion state to prevent residual animations
|
|
571
|
+
if (!this.context.opts.value.current && !this.context.opts.previousValue.current) {
|
|
572
|
+
untrack(() => (this.prevMotionAttribute = null));
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
523
575
|
// We only want to update selected and the last selected content
|
|
524
576
|
// this avoids animations being interrupted outside of that range
|
|
525
577
|
if (!isSelected && !wasSelected)
|
|
@@ -585,8 +637,17 @@ class NavigationMenuContentImplState {
|
|
|
585
637
|
const target = e.target;
|
|
586
638
|
const isTrigger = this.listContext.listTriggers.some((trigger) => trigger.contains(target));
|
|
587
639
|
const isRootViewport = this.context.opts.isRootMenu && this.context.viewportRef.current?.contains(target);
|
|
588
|
-
if (
|
|
640
|
+
if (!this.context.opts.isRootMenu && !isTrigger) {
|
|
641
|
+
this.context.onItemDismiss();
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (isTrigger || isRootViewport) {
|
|
589
645
|
e.preventDefault();
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (!this.itemContext.opts.openOnHover.current) {
|
|
649
|
+
this.context.onItemSelect("", null);
|
|
650
|
+
}
|
|
590
651
|
};
|
|
591
652
|
onkeydown = (e) => {
|
|
592
653
|
// prevent parent menus handling sub-menu keydown events
|
|
@@ -661,6 +722,7 @@ class NavigationMenuViewportState {
|
|
|
661
722
|
viewportWidth = $derived.by(() => (this.size ? `${this.size.width}px` : undefined));
|
|
662
723
|
viewportHeight = $derived.by(() => (this.size ? `${this.size.height}px` : undefined));
|
|
663
724
|
activeContentValue = $derived.by(() => this.context.opts.value.current);
|
|
725
|
+
mounted = $state(false);
|
|
664
726
|
constructor(opts, context) {
|
|
665
727
|
this.opts = opts;
|
|
666
728
|
this.context = context;
|
|
@@ -695,6 +757,12 @@ class NavigationMenuViewportState {
|
|
|
695
757
|
};
|
|
696
758
|
}
|
|
697
759
|
});
|
|
760
|
+
// reset size when viewport closes to prevent residual size animations
|
|
761
|
+
watch(() => this.mounted, () => {
|
|
762
|
+
if (!this.mounted && this.size) {
|
|
763
|
+
this.size = null;
|
|
764
|
+
}
|
|
765
|
+
});
|
|
698
766
|
}
|
|
699
767
|
props = $derived.by(() => ({
|
|
700
768
|
id: this.opts.id.current,
|
|
@@ -714,6 +782,7 @@ const NavigationMenuProviderContext = new Context("NavigationMenu.Root");
|
|
|
714
782
|
export const NavigationMenuItemContext = new Context("NavigationMenu.Item");
|
|
715
783
|
const NavigationMenuListContext = new Context("NavigationMenu.List");
|
|
716
784
|
const NavigationMenuContentContext = new Context("NavigationMenu.Content");
|
|
785
|
+
const NavigationMenuSubContext = new Context("NavigationMenu.Sub");
|
|
717
786
|
export function useNavigationMenuRoot(props) {
|
|
718
787
|
return new NavigationMenuRootState(props);
|
|
719
788
|
}
|
|
@@ -740,6 +809,7 @@ export function useNavigationMenuTrigger(props) {
|
|
|
740
809
|
provider: NavigationMenuProviderContext.get(),
|
|
741
810
|
item: NavigationMenuItemContext.get(),
|
|
742
811
|
list: NavigationMenuListContext.get(),
|
|
812
|
+
sub: NavigationMenuSubContext.getOr(null),
|
|
743
813
|
});
|
|
744
814
|
}
|
|
745
815
|
export function useNavigationMenuContent(props) {
|
|
@@ -15,21 +15,22 @@ export type NavigationMenuRootPropsWithoutHTML = WithChild<{
|
|
|
15
15
|
*/
|
|
16
16
|
onValueChange?: OnChangeFn<string>;
|
|
17
17
|
/**
|
|
18
|
-
* The
|
|
18
|
+
* The amount of time in ms from when the mouse enters a trigger until the content opens.
|
|
19
19
|
*
|
|
20
|
-
* @
|
|
20
|
+
* @default 200
|
|
21
21
|
*/
|
|
22
22
|
delayDuration?: number;
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* The amount of time in ms that a user has to enter another trigger without
|
|
25
|
+
* incurring a delay again.
|
|
25
26
|
*
|
|
26
|
-
* @
|
|
27
|
+
* @default 300
|
|
27
28
|
*/
|
|
28
29
|
skipDelayDuration?: number;
|
|
29
30
|
/**
|
|
30
31
|
* The reading direction of the content.
|
|
31
32
|
*
|
|
32
|
-
* @
|
|
33
|
+
* @default "ltr"
|
|
33
34
|
*/
|
|
34
35
|
dir?: Direction;
|
|
35
36
|
/**
|
|
@@ -68,6 +69,13 @@ export type NavigationMenuItemPropsWithoutHTML = WithChild<{
|
|
|
68
69
|
* The value of the menu item.
|
|
69
70
|
*/
|
|
70
71
|
value?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Whether to open the menu associated with the item when the item's trigger
|
|
74
|
+
* is hovered.
|
|
75
|
+
*
|
|
76
|
+
* @default true
|
|
77
|
+
*/
|
|
78
|
+
openOnHover?: boolean;
|
|
71
79
|
}>;
|
|
72
80
|
export type NavigationMenuItemProps = NavigationMenuItemPropsWithoutHTML & Without<BitsPrimitiveLiAttributes, NavigationMenuItemPropsWithoutHTML>;
|
|
73
81
|
export type NavigationMenuTriggerPropsWithoutHTML = WithChild<{
|
|
@@ -82,7 +90,6 @@ export type NavigationMenuContentPropsWithoutHTML = WithChild<{
|
|
|
82
90
|
/**
|
|
83
91
|
* Callback fired when an interaction occurs outside the content.
|
|
84
92
|
* Default behavior can be prevented with `event.preventDefault()`
|
|
85
|
-
*
|
|
86
93
|
*/
|
|
87
94
|
onInteractOutside?: (event: PointerEvent) => void;
|
|
88
95
|
/**
|
|
@@ -108,7 +115,7 @@ export type NavigationMenuContentPropsWithoutHTML = WithChild<{
|
|
|
108
115
|
* This is useful when wanting to use more custom transition and animation
|
|
109
116
|
* libraries.
|
|
110
117
|
*
|
|
111
|
-
* @
|
|
118
|
+
* @default false
|
|
112
119
|
*/
|
|
113
120
|
forceMount?: boolean;
|
|
114
121
|
}>;
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
dir = "ltr",
|
|
23
23
|
autoSort = true,
|
|
24
24
|
orientation = "horizontal",
|
|
25
|
+
thumbPositioning = "contain",
|
|
25
26
|
...restProps
|
|
26
27
|
}: SliderRootProps = $props();
|
|
27
28
|
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
dir: box.with(() => dir),
|
|
64
65
|
autoSort: box.with(() => autoSort),
|
|
65
66
|
orientation: box.with(() => orientation),
|
|
67
|
+
thumbPositioning: box.with(() => thumbPositioning),
|
|
66
68
|
type,
|
|
67
69
|
});
|
|
68
70
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Box, type ReadableBox } from "svelte-toolbelt";
|
|
2
2
|
import type { ReadableBoxedValues, WritableBoxedValues } from "../../internal/box.svelte.js";
|
|
3
3
|
import type { BitsKeyboardEvent, OnChangeFn, WithRefProps } from "../../internal/types.js";
|
|
4
|
-
import type { Direction, Orientation } from "../../shared/index.js";
|
|
4
|
+
import type { Direction, Orientation, SliderThumbPositioning } from "../../shared/index.js";
|
|
5
5
|
type SliderBaseRootStateProps = WithRefProps<ReadableBoxedValues<{
|
|
6
6
|
disabled: boolean;
|
|
7
7
|
orientation: Orientation;
|
|
@@ -10,6 +10,7 @@ type SliderBaseRootStateProps = WithRefProps<ReadableBoxedValues<{
|
|
|
10
10
|
step: number;
|
|
11
11
|
dir: Direction;
|
|
12
12
|
autoSort: boolean;
|
|
13
|
+
thumbPositioning: SliderThumbPositioning;
|
|
13
14
|
}>>;
|
|
14
15
|
declare class SliderBaseRootState {
|
|
15
16
|
#private;
|
|
@@ -46,6 +46,10 @@ class SliderBaseRootState {
|
|
|
46
46
|
return Array.from(node.querySelectorAll(`[${SLIDER_THUMB_ATTR}]`));
|
|
47
47
|
};
|
|
48
48
|
getThumbScale = () => {
|
|
49
|
+
if (this.opts.thumbPositioning.current === "exact") {
|
|
50
|
+
// User opted out of containment
|
|
51
|
+
return [0, 100];
|
|
52
|
+
}
|
|
49
53
|
const isVertical = this.opts.orientation.current === "vertical";
|
|
50
54
|
// this assumes all thumbs are the same width
|
|
51
55
|
const activeThumb = this.getAllThumbs()[0];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OnChangeFn, WithChild, Without } from "../../internal/types.js";
|
|
2
2
|
import type { BitsPrimitiveSpanAttributes } from "../../shared/attributes.js";
|
|
3
|
-
import type { Direction, Orientation } from "../../shared/index.js";
|
|
3
|
+
import type { Direction, Orientation, SliderThumbPositioning } from "../../shared/index.js";
|
|
4
4
|
export type SliderRootSnippetProps = {
|
|
5
5
|
ticks: number[];
|
|
6
6
|
thumbs: number[];
|
|
@@ -53,6 +53,12 @@ export type BaseSliderRootPropsWithoutHTML = {
|
|
|
53
53
|
* @defaultValue false
|
|
54
54
|
*/
|
|
55
55
|
disabled?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* The positioning of the slider thumb.
|
|
58
|
+
*
|
|
59
|
+
* @defaultValue "contain"
|
|
60
|
+
*/
|
|
61
|
+
thumbPositioning?: SliderThumbPositioning;
|
|
56
62
|
};
|
|
57
63
|
export type SliderSingleRootPropsWithoutHTML = BaseSliderRootPropsWithoutHTML & {
|
|
58
64
|
/**
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -12,6 +12,13 @@ export type StyleProperties = CSS.Properties & {
|
|
|
12
12
|
};
|
|
13
13
|
export type Orientation = "horizontal" | "vertical";
|
|
14
14
|
export type Direction = "ltr" | "rtl";
|
|
15
|
+
/**
|
|
16
|
+
* Controls positioning of the slider thumb.
|
|
17
|
+
*
|
|
18
|
+
* - `exact`: The thumb is centered exactly at the value of the slider.
|
|
19
|
+
* - `contain`: The thumb is centered exactly at the value of the slider, but will be contained within the slider track at the ends.
|
|
20
|
+
*/
|
|
21
|
+
export type SliderThumbPositioning = "exact" | "contain";
|
|
15
22
|
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
|
16
23
|
export type WithoutChildren<T> = T extends {
|
|
17
24
|
children?: any;
|