INIT
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { mergeProps } from "svelte-toolbelt";
|
||||
import type { MenuArrowProps } from "../types.js";
|
||||
import { MenuArrowState } from "../menu.svelte.js";
|
||||
import FloatingLayerArrow from "../../utilities/floating-layer/components/floating-layer-arrow.svelte";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: MenuArrowProps = $props();
|
||||
|
||||
const arrowState = MenuArrowState.create();
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, arrowState.props));
|
||||
</script>
|
||||
|
||||
<FloatingLayerArrow bind:ref {...mergedProps} />
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare const MenuArrow: import("svelte").Component<import("../../utilities/arrow/types.js").ArrowProps, {}, "ref">;
|
||||
type MenuArrow = ReturnType<typeof MenuArrow>;
|
||||
export default MenuArrow;
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenuCheckboxGroupProps } from "../types.js";
|
||||
import { MenuCheckboxGroupState } from "../menu.svelte.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
children,
|
||||
child,
|
||||
ref = $bindable(null),
|
||||
value = $bindable([]),
|
||||
onValueChange = noop,
|
||||
...restProps
|
||||
}: MenuCheckboxGroupProps = $props();
|
||||
|
||||
const checkboxGroupState = MenuCheckboxGroupState.create({
|
||||
value: boxWith(
|
||||
() => $state.snapshot(value),
|
||||
(v) => {
|
||||
value = $state.snapshot(v);
|
||||
onValueChange(v);
|
||||
}
|
||||
),
|
||||
onValueChange: boxWith(() => onValueChange),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
id: boxWith(() => id),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, checkboxGroupState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { MenuCheckboxGroupProps } from "../types.js";
|
||||
declare const MenuCheckboxGroup: import("svelte").Component<MenuCheckboxGroupProps, {}, "value" | "ref">;
|
||||
type MenuCheckboxGroup = ReturnType<typeof MenuCheckboxGroup>;
|
||||
export default MenuCheckboxGroup;
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenuCheckboxItemProps } from "../types.js";
|
||||
import { MenuCheckboxGroupContext, MenuCheckboxItemState } from "../menu.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
import { watch } from "runed";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
child,
|
||||
children,
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
id = createId(uid),
|
||||
onCheckedChange = noop,
|
||||
disabled = false,
|
||||
onSelect = noop,
|
||||
closeOnSelect = true,
|
||||
indeterminate = $bindable(false),
|
||||
onIndeterminateChange = noop,
|
||||
value = "",
|
||||
...restProps
|
||||
}: MenuCheckboxItemProps = $props();
|
||||
|
||||
const group = MenuCheckboxGroupContext.getOr(null);
|
||||
|
||||
if (group && value) {
|
||||
if (group.opts.value.current.includes(value)) {
|
||||
checked = true;
|
||||
} else {
|
||||
checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch.pre(
|
||||
() => value,
|
||||
() => {
|
||||
if (group && value) {
|
||||
if (group.opts.value.current.includes(value)) {
|
||||
checked = true;
|
||||
} else {
|
||||
checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const checkboxItemState = MenuCheckboxItemState.create(
|
||||
{
|
||||
checked: boxWith(
|
||||
() => checked,
|
||||
(v) => {
|
||||
if (v !== checked) {
|
||||
checked = v;
|
||||
onCheckedChange(v);
|
||||
}
|
||||
}
|
||||
),
|
||||
id: boxWith(() => id),
|
||||
disabled: boxWith(() => disabled),
|
||||
onSelect: boxWith(() => handleSelect),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
closeOnSelect: boxWith(() => closeOnSelect),
|
||||
indeterminate: boxWith(
|
||||
() => indeterminate,
|
||||
(v) => {
|
||||
if (v !== indeterminate) {
|
||||
indeterminate = v;
|
||||
onIndeterminateChange(v);
|
||||
}
|
||||
}
|
||||
),
|
||||
value: boxWith(() => value),
|
||||
},
|
||||
group
|
||||
);
|
||||
|
||||
function handleSelect(e: Event) {
|
||||
onSelect(e);
|
||||
if (e.defaultPrevented) return;
|
||||
checkboxItemState.toggleChecked();
|
||||
}
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, checkboxItemState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ checked, indeterminate, props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.({ checked, indeterminate })}
|
||||
</div>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { MenuCheckboxItemProps } from "../types.js";
|
||||
declare const MenuCheckboxItem: import("svelte").Component<MenuCheckboxItemProps, {}, "checked" | "indeterminate" | "ref">;
|
||||
type MenuCheckboxItem = ReturnType<typeof MenuCheckboxItem>;
|
||||
export default MenuCheckboxItem;
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenuContentStaticProps } from "../types.js";
|
||||
import { MenuContentState } from "../menu.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
import PopperLayer from "../../utilities/popper-layer/popper-layer.svelte";
|
||||
import { getFloatingContentCSSVars } from "../../../internal/floating-svelte/floating-utils.svelte.js";
|
||||
import PopperLayerForceMount from "../../utilities/popper-layer/popper-layer-force-mount.svelte";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
child,
|
||||
children,
|
||||
ref = $bindable(null),
|
||||
loop = true,
|
||||
onInteractOutside = noop,
|
||||
onEscapeKeydown = noop,
|
||||
onCloseAutoFocus: onCloseAutoFocusProp = noop,
|
||||
forceMount = false,
|
||||
style,
|
||||
...restProps
|
||||
}: MenuContentStaticProps = $props();
|
||||
|
||||
const contentState = MenuContentState.create({
|
||||
id: boxWith(() => id),
|
||||
loop: boxWith(() => loop),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
onCloseAutoFocus: boxWith(() => onCloseAutoFocusProp),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(
|
||||
mergeProps(restProps, contentState.props, {
|
||||
style: { outline: "none" },
|
||||
})
|
||||
);
|
||||
|
||||
function handleInteractOutside(e: PointerEvent) {
|
||||
onInteractOutside(e);
|
||||
if (e.defaultPrevented) return;
|
||||
contentState.parentMenu.onClose();
|
||||
}
|
||||
|
||||
function handleEscapeKeydown(e: KeyboardEvent) {
|
||||
onEscapeKeydown(e);
|
||||
if (e.defaultPrevented) return;
|
||||
contentState.parentMenu.onClose();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if forceMount}
|
||||
<PopperLayerForceMount
|
||||
{...mergedProps}
|
||||
{...contentState.popperProps}
|
||||
ref={contentState.opts.ref}
|
||||
enabled={contentState.parentMenu.opts.open.current}
|
||||
onInteractOutside={handleInteractOutside}
|
||||
onEscapeKeydown={handleEscapeKeydown}
|
||||
trapFocus
|
||||
{loop}
|
||||
forceMount={true}
|
||||
isStatic
|
||||
{id}
|
||||
shouldRender={contentState.shouldRender}
|
||||
>
|
||||
{#snippet popper({ props })}
|
||||
{@const finalProps = mergeProps(
|
||||
props,
|
||||
{ style: { outline: "none", ...getFloatingContentCSSVars("menu") } },
|
||||
{ style }
|
||||
)}
|
||||
{#if child}
|
||||
{@render child({ props: finalProps, ...contentState.snippetProps })}
|
||||
{:else}
|
||||
<div {...finalProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</PopperLayerForceMount>
|
||||
{:else if !forceMount}
|
||||
<PopperLayer
|
||||
{...mergedProps}
|
||||
{...contentState.popperProps}
|
||||
ref={contentState.opts.ref}
|
||||
open={contentState.parentMenu.opts.open.current}
|
||||
onInteractOutside={handleInteractOutside}
|
||||
onEscapeKeydown={handleEscapeKeydown}
|
||||
trapFocus
|
||||
{loop}
|
||||
forceMount={false}
|
||||
isStatic
|
||||
{id}
|
||||
shouldRender={contentState.shouldRender}
|
||||
>
|
||||
{#snippet popper({ props })}
|
||||
{@const finalProps = mergeProps(
|
||||
props,
|
||||
{ style: { outline: "none", ...getFloatingContentCSSVars("menu") } },
|
||||
{ style }
|
||||
)}
|
||||
{#if child}
|
||||
{@render child({ props: finalProps, ...contentState.snippetProps })}
|
||||
{:else}
|
||||
<div {...finalProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</PopperLayer>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { MenuContentStaticProps } from "../types.js";
|
||||
declare const MenuContentStatic: import("svelte").Component<MenuContentStaticProps, {}, "ref">;
|
||||
type MenuContentStatic = ReturnType<typeof MenuContentStatic>;
|
||||
export default MenuContentStatic;
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenuContentProps } from "../types.js";
|
||||
import { MenuContentState } from "../menu.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
import PopperLayer from "../../utilities/popper-layer/popper-layer.svelte";
|
||||
import { getFloatingContentCSSVars } from "../../../internal/floating-svelte/floating-utils.svelte.js";
|
||||
import PopperLayerForceMount from "../../utilities/popper-layer/popper-layer-force-mount.svelte";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
child,
|
||||
children,
|
||||
ref = $bindable(null),
|
||||
loop = true,
|
||||
onInteractOutside = noop,
|
||||
onEscapeKeydown = noop,
|
||||
onCloseAutoFocus: onCloseAutoFocusProp = noop,
|
||||
forceMount = false,
|
||||
style,
|
||||
...restProps
|
||||
}: MenuContentProps = $props();
|
||||
|
||||
const contentState = MenuContentState.create({
|
||||
id: boxWith(() => id),
|
||||
loop: boxWith(() => loop),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
onCloseAutoFocus: boxWith(() => onCloseAutoFocusProp),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(
|
||||
mergeProps(restProps, contentState.props, {
|
||||
style: { outline: "none" },
|
||||
})
|
||||
);
|
||||
|
||||
function handleInteractOutside(e: PointerEvent) {
|
||||
onInteractOutside(e);
|
||||
if (e.defaultPrevented) return;
|
||||
// don't close if the interaction is with a submenu content or items
|
||||
if (e.target && e.target instanceof Element) {
|
||||
const subContentSelector = `[${contentState.parentMenu.root.getBitsAttr("sub-content")}]`;
|
||||
if (e.target.closest(subContentSelector)) return;
|
||||
}
|
||||
contentState.parentMenu.onClose();
|
||||
}
|
||||
|
||||
function handleEscapeKeydown(e: KeyboardEvent) {
|
||||
onEscapeKeydown(e);
|
||||
if (e.defaultPrevented) return;
|
||||
contentState.parentMenu.onClose();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if forceMount}
|
||||
<PopperLayerForceMount
|
||||
{...mergedProps}
|
||||
{...contentState.popperProps}
|
||||
ref={contentState.opts.ref}
|
||||
enabled={contentState.parentMenu.opts.open.current}
|
||||
onInteractOutside={handleInteractOutside}
|
||||
onEscapeKeydown={handleEscapeKeydown}
|
||||
trapFocus
|
||||
{loop}
|
||||
forceMount={true}
|
||||
{id}
|
||||
shouldRender={contentState.shouldRender}
|
||||
>
|
||||
{#snippet popper({ props, wrapperProps })}
|
||||
{@const finalProps = mergeProps(
|
||||
props,
|
||||
{ style: { outline: "none", ...getFloatingContentCSSVars("menu") } },
|
||||
{ style }
|
||||
)}
|
||||
{#if child}
|
||||
{@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
|
||||
{:else}
|
||||
<div {...wrapperProps}>
|
||||
<div {...finalProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</PopperLayerForceMount>
|
||||
{:else if !forceMount}
|
||||
<PopperLayer
|
||||
{...mergedProps}
|
||||
{...contentState.popperProps}
|
||||
ref={contentState.opts.ref}
|
||||
open={contentState.parentMenu.opts.open.current}
|
||||
onInteractOutside={handleInteractOutside}
|
||||
onEscapeKeydown={handleEscapeKeydown}
|
||||
trapFocus
|
||||
{loop}
|
||||
forceMount={false}
|
||||
{id}
|
||||
shouldRender={contentState.shouldRender}
|
||||
>
|
||||
{#snippet popper({ props, wrapperProps })}
|
||||
{@const finalProps = mergeProps(
|
||||
props,
|
||||
{ style: { outline: "none", ...getFloatingContentCSSVars("menu") } },
|
||||
{ style }
|
||||
)}
|
||||
{#if child}
|
||||
{@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
|
||||
{:else}
|
||||
<div {...wrapperProps}>
|
||||
<div {...finalProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</PopperLayer>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { MenuContentProps } from "../types.js";
|
||||
declare const MenuContent: import("svelte").Component<MenuContentProps, {}, "ref">;
|
||||
type MenuContent = ReturnType<typeof MenuContent>;
|
||||
export default MenuContent;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenuGroupHeadingProps } from "../types.js";
|
||||
import { MenuGroupHeadingState } from "../menu.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
children,
|
||||
child,
|
||||
ref = $bindable(null),
|
||||
id = createId(uid),
|
||||
...restProps
|
||||
}: MenuGroupHeadingProps = $props();
|
||||
|
||||
const groupHeadingState = MenuGroupHeadingState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
const mergedProps = $derived(mergeProps(restProps, groupHeadingState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { MenuGroupHeadingProps } from "../types.js";
|
||||
declare const MenuGroupHeading: import("svelte").Component<MenuGroupHeadingProps, {}, "ref">;
|
||||
type MenuGroupHeading = ReturnType<typeof MenuGroupHeading>;
|
||||
export default MenuGroupHeading;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenuGroupProps } from "../types.js";
|
||||
import { MenuGroupState } from "../menu.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
children,
|
||||
child,
|
||||
ref = $bindable(null),
|
||||
id = createId(uid),
|
||||
...restProps
|
||||
}: MenuGroupProps = $props();
|
||||
|
||||
const groupState = MenuGroupState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
const mergedProps = $derived(mergeProps(restProps, groupState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { MenuGroupProps } from "../types.js";
|
||||
declare const MenuGroup: import("svelte").Component<MenuGroupProps, {}, "ref">;
|
||||
type MenuGroup = ReturnType<typeof MenuGroup>;
|
||||
export default MenuGroup;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenuItemProps } from "../types.js";
|
||||
import { MenuItemState } from "../menu.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
child,
|
||||
children,
|
||||
ref = $bindable(null),
|
||||
id = createId(uid),
|
||||
disabled = false,
|
||||
onSelect = noop,
|
||||
closeOnSelect = true,
|
||||
...restProps
|
||||
}: MenuItemProps = $props();
|
||||
|
||||
const itemState = MenuItemState.create({
|
||||
id: boxWith(() => id),
|
||||
disabled: boxWith(() => disabled),
|
||||
onSelect: boxWith(() => onSelect),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
closeOnSelect: boxWith(() => closeOnSelect),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, itemState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { MenuItemProps } from "../types.js";
|
||||
declare const MenuItem: import("svelte").Component<MenuItemProps, {}, "ref">;
|
||||
type MenuItem = ReturnType<typeof MenuItem>;
|
||||
export default MenuItem;
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenuRadioGroupProps } from "../types.js";
|
||||
import { MenuRadioGroupState } from "../menu.svelte.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
children,
|
||||
child,
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
onValueChange = noop,
|
||||
...restProps
|
||||
}: MenuRadioGroupProps = $props();
|
||||
|
||||
const radioGroupState = MenuRadioGroupState.create({
|
||||
value: boxWith(
|
||||
() => value,
|
||||
(v) => {
|
||||
value = v;
|
||||
onValueChange(v);
|
||||
}
|
||||
),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
id: boxWith(() => id),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, radioGroupState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { MenuRadioGroupProps } from "../types.js";
|
||||
declare const MenuRadioGroup: import("svelte").Component<MenuRadioGroupProps, {}, "value" | "ref">;
|
||||
type MenuRadioGroup = ReturnType<typeof MenuRadioGroup>;
|
||||
export default MenuRadioGroup;
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenuRadioItemProps } from "../types.js";
|
||||
import { MenuRadioItemState } from "../menu.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
children,
|
||||
child,
|
||||
ref = $bindable(null),
|
||||
value,
|
||||
onSelect = noop,
|
||||
id = createId(uid),
|
||||
disabled = false,
|
||||
closeOnSelect = true,
|
||||
...restProps
|
||||
}: MenuRadioItemProps = $props();
|
||||
|
||||
const radioItemState = MenuRadioItemState.create({
|
||||
value: boxWith(() => value),
|
||||
id: boxWith(() => id),
|
||||
disabled: boxWith(() => disabled),
|
||||
onSelect: boxWith(() => handleSelect),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
closeOnSelect: boxWith(() => closeOnSelect),
|
||||
});
|
||||
|
||||
function handleSelect(e: Event) {
|
||||
onSelect(e);
|
||||
if (e.defaultPrevented) return;
|
||||
radioItemState.selectValue();
|
||||
}
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, radioItemState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps, checked: radioItemState.isChecked })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.({ checked: radioItemState.isChecked })}
|
||||
</div>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { MenuRadioItemProps } from "../types.js";
|
||||
declare const MenuRadioItem: import("svelte").Component<MenuRadioItemProps, {}, "ref">;
|
||||
type MenuRadioItem = ReturnType<typeof MenuRadioItem>;
|
||||
export default MenuRadioItem;
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenuSeparatorProps } from "../types.js";
|
||||
import { MenuSeparatorState } from "../menu.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
id = createId(uid),
|
||||
child,
|
||||
children,
|
||||
...restProps
|
||||
}: MenuSeparatorProps = $props();
|
||||
|
||||
const separatorState = MenuSeparatorState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, separatorState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { MenuSeparatorProps } from "../types.js";
|
||||
declare const MenuSeparator: import("svelte").Component<MenuSeparatorProps, {}, "ref">;
|
||||
type MenuSeparator = ReturnType<typeof MenuSeparator>;
|
||||
export default MenuSeparator;
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
<script lang="ts">
|
||||
import { afterTick, boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenuSubContentStaticProps } from "../types.js";
|
||||
import { MenuContentState } from "../menu.svelte.js";
|
||||
import { SUB_CLOSE_KEYS } from "../utils.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import PopperLayer from "../../utilities/popper-layer/popper-layer.svelte";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
import { isHTMLElement } from "../../../internal/is.js";
|
||||
import { getFloatingContentCSSVars } from "../../../internal/floating-svelte/floating-utils.svelte.js";
|
||||
import PopperLayerForceMount from "../../utilities/popper-layer/popper-layer-force-mount.svelte";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
loop = true,
|
||||
onInteractOutside = noop,
|
||||
forceMount = false,
|
||||
onEscapeKeydown = noop,
|
||||
interactOutsideBehavior = "defer-otherwise-close",
|
||||
escapeKeydownBehavior = "defer-otherwise-close",
|
||||
onOpenAutoFocus: onOpenAutoFocusProp = noop,
|
||||
onCloseAutoFocus: onCloseAutoFocusProp = noop,
|
||||
onFocusOutside = noop,
|
||||
trapFocus = false,
|
||||
style,
|
||||
...restProps
|
||||
}: MenuSubContentStaticProps = $props();
|
||||
|
||||
const subContentState = MenuContentState.create({
|
||||
id: boxWith(() => id),
|
||||
loop: boxWith(() => loop),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
onCloseAutoFocus: boxWith(() => handleCloseAutoFocus),
|
||||
isSub: true,
|
||||
});
|
||||
|
||||
function onkeydown(e: KeyboardEvent) {
|
||||
const isKeyDownInside = (e.currentTarget as HTMLElement).contains(e.target as HTMLElement);
|
||||
const isCloseKey = SUB_CLOSE_KEYS[
|
||||
subContentState.parentMenu.root.opts.dir.current
|
||||
].includes(e.key);
|
||||
if (isKeyDownInside && isCloseKey) {
|
||||
subContentState.parentMenu.onClose();
|
||||
const triggerNode = subContentState.parentMenu.triggerNode;
|
||||
triggerNode?.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
const dataAttr = $derived(subContentState.parentMenu.root.getBitsAttr("sub-content"));
|
||||
|
||||
const mergedProps = $derived(
|
||||
mergeProps(restProps, subContentState.props, {
|
||||
onkeydown,
|
||||
[dataAttr]: "",
|
||||
})
|
||||
);
|
||||
|
||||
function handleOpenAutoFocus(e: Event) {
|
||||
onOpenAutoFocusProp(e);
|
||||
if (e.defaultPrevented) return;
|
||||
afterTick(() => {
|
||||
e.preventDefault();
|
||||
if (subContentState.parentMenu.root.isUsingKeyboard) {
|
||||
const subContentEl = subContentState.parentMenu.contentNode;
|
||||
subContentEl?.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleCloseAutoFocus(e: Event) {
|
||||
onCloseAutoFocusProp(e);
|
||||
if (e.defaultPrevented) return;
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function handleInteractOutside(e: PointerEvent) {
|
||||
onInteractOutside(e);
|
||||
if (e.defaultPrevented) return;
|
||||
subContentState.parentMenu.onClose();
|
||||
}
|
||||
|
||||
function handleEscapeKeydown(e: KeyboardEvent) {
|
||||
onEscapeKeydown(e);
|
||||
if (e.defaultPrevented) return;
|
||||
subContentState.parentMenu.onClose();
|
||||
}
|
||||
|
||||
function handleOnFocusOutside(e: FocusEvent) {
|
||||
onFocusOutside(e);
|
||||
if (e.defaultPrevented) return;
|
||||
// We prevent closing when the trigger is focused to avoid triggering a re-open animation
|
||||
// on pointer interaction.
|
||||
if (!isHTMLElement(e.target)) return;
|
||||
if (e.target.id !== subContentState.parentMenu.triggerNode?.id) {
|
||||
subContentState.parentMenu.onClose();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if forceMount}
|
||||
<PopperLayerForceMount
|
||||
{...mergedProps}
|
||||
ref={subContentState.opts.ref}
|
||||
{interactOutsideBehavior}
|
||||
{escapeKeydownBehavior}
|
||||
onOpenAutoFocus={handleOpenAutoFocus}
|
||||
enabled={subContentState.parentMenu.opts.open.current}
|
||||
onInteractOutside={handleInteractOutside}
|
||||
onEscapeKeydown={handleEscapeKeydown}
|
||||
onFocusOutside={handleOnFocusOutside}
|
||||
preventScroll={false}
|
||||
{loop}
|
||||
{trapFocus}
|
||||
isStatic
|
||||
shouldRender={subContentState.shouldRender}
|
||||
>
|
||||
{#snippet popper({ props })}
|
||||
{@const finalProps = mergeProps(
|
||||
props,
|
||||
mergedProps,
|
||||
{ style: getFloatingContentCSSVars("menu") },
|
||||
{ style }
|
||||
)}
|
||||
{#if child}
|
||||
{@render child({ props: finalProps, ...subContentState.snippetProps })}
|
||||
{:else}
|
||||
<div {...finalProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</PopperLayerForceMount>
|
||||
{:else if !forceMount}
|
||||
<PopperLayer
|
||||
{...mergedProps}
|
||||
ref={subContentState.opts.ref}
|
||||
{interactOutsideBehavior}
|
||||
{escapeKeydownBehavior}
|
||||
onCloseAutoFocus={handleCloseAutoFocus}
|
||||
onOpenAutoFocus={handleOpenAutoFocus}
|
||||
open={subContentState.parentMenu.opts.open.current}
|
||||
onInteractOutside={handleInteractOutside}
|
||||
onEscapeKeydown={handleEscapeKeydown}
|
||||
onFocusOutside={handleOnFocusOutside}
|
||||
preventScroll={false}
|
||||
{loop}
|
||||
{trapFocus}
|
||||
isStatic
|
||||
shouldRender={subContentState.shouldRender}
|
||||
>
|
||||
{#snippet popper({ props })}
|
||||
{@const finalProps = mergeProps(
|
||||
props,
|
||||
mergedProps,
|
||||
{ style: getFloatingContentCSSVars("menu") },
|
||||
{ style }
|
||||
)}
|
||||
{#if child}
|
||||
{@render child({ props: finalProps, ...subContentState.snippetProps })}
|
||||
{:else}
|
||||
<div {...finalProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</PopperLayer>
|
||||
{/if}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { MenuSubContentStaticProps } from "../types.js";
|
||||
declare const MenuSubContentStatic: import("svelte").Component<MenuSubContentStaticProps, {}, "ref">;
|
||||
type MenuSubContentStatic = ReturnType<typeof MenuSubContentStatic>;
|
||||
export default MenuSubContentStatic;
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenuSubContentProps } from "../types.js";
|
||||
import { MenuOpenEvent, MenuContentState } from "../menu.svelte.js";
|
||||
import { SUB_CLOSE_KEYS } from "../utils.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import PopperLayer from "../../utilities/popper-layer/popper-layer.svelte";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
import { isHTMLElement } from "../../../internal/is.js";
|
||||
import { getFloatingContentCSSVars } from "../../../internal/floating-svelte/floating-utils.svelte.js";
|
||||
import PopperLayerForceMount from "../../utilities/popper-layer/popper-layer-force-mount.svelte";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
loop = true,
|
||||
onInteractOutside = noop,
|
||||
forceMount = false,
|
||||
onEscapeKeydown = noop,
|
||||
interactOutsideBehavior = "defer-otherwise-close",
|
||||
escapeKeydownBehavior = "defer-otherwise-close",
|
||||
onOpenAutoFocus: onOpenAutoFocusProp = noop,
|
||||
onCloseAutoFocus: onCloseAutoFocusProp = noop,
|
||||
onFocusOutside = noop,
|
||||
side = "right",
|
||||
trapFocus = false,
|
||||
style,
|
||||
...restProps
|
||||
}: MenuSubContentProps = $props();
|
||||
|
||||
const subContentState = MenuContentState.create({
|
||||
id: boxWith(() => id),
|
||||
loop: boxWith(() => loop),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
isSub: true,
|
||||
onCloseAutoFocus: boxWith(() => handleCloseAutoFocus),
|
||||
});
|
||||
|
||||
function onkeydown(e: KeyboardEvent) {
|
||||
const isKeyDownInside = (e.currentTarget as HTMLElement).contains(e.target as HTMLElement);
|
||||
const isCloseKey = SUB_CLOSE_KEYS[
|
||||
subContentState.parentMenu.root.opts.dir.current
|
||||
].includes(e.key);
|
||||
if (isKeyDownInside && isCloseKey) {
|
||||
subContentState.parentMenu.onClose();
|
||||
const triggerNode = subContentState.parentMenu.triggerNode;
|
||||
triggerNode?.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
const dataAttr = $derived(subContentState.parentMenu.root.getBitsAttr("sub-content"));
|
||||
|
||||
const mergedProps = $derived(
|
||||
mergeProps(restProps, subContentState.props, {
|
||||
side,
|
||||
onkeydown,
|
||||
[dataAttr]: "",
|
||||
})
|
||||
);
|
||||
|
||||
function handleOpenAutoFocus(e: Event) {
|
||||
onOpenAutoFocusProp(e);
|
||||
if (e.defaultPrevented) return;
|
||||
e.preventDefault();
|
||||
if (
|
||||
subContentState.parentMenu.root.isUsingKeyboard &&
|
||||
subContentState.parentMenu.contentNode
|
||||
) {
|
||||
MenuOpenEvent.dispatch(subContentState.parentMenu.contentNode);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCloseAutoFocus(e: Event) {
|
||||
onCloseAutoFocusProp(e);
|
||||
if (e.defaultPrevented) return;
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function handleInteractOutside(e: PointerEvent) {
|
||||
onInteractOutside(e);
|
||||
if (e.defaultPrevented) return;
|
||||
subContentState.parentMenu.onClose();
|
||||
}
|
||||
|
||||
function handleEscapeKeydown(e: KeyboardEvent) {
|
||||
onEscapeKeydown(e);
|
||||
if (e.defaultPrevented) return;
|
||||
subContentState.parentMenu.onClose();
|
||||
}
|
||||
|
||||
function handleOnFocusOutside(e: FocusEvent) {
|
||||
onFocusOutside(e);
|
||||
if (e.defaultPrevented) return;
|
||||
// We prevent closing when the trigger is focused to avoid triggering a re-open animation
|
||||
// on pointer interaction.
|
||||
if (!isHTMLElement(e.target)) return;
|
||||
if (e.target.id !== subContentState.parentMenu.triggerNode?.id) {
|
||||
subContentState.parentMenu.onClose();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if forceMount}
|
||||
<PopperLayerForceMount
|
||||
{...mergedProps}
|
||||
ref={subContentState.opts.ref}
|
||||
{interactOutsideBehavior}
|
||||
{escapeKeydownBehavior}
|
||||
onOpenAutoFocus={handleOpenAutoFocus}
|
||||
enabled={subContentState.parentMenu.opts.open.current}
|
||||
onInteractOutside={handleInteractOutside}
|
||||
onEscapeKeydown={handleEscapeKeydown}
|
||||
onFocusOutside={handleOnFocusOutside}
|
||||
preventScroll={false}
|
||||
{loop}
|
||||
{trapFocus}
|
||||
shouldRender={subContentState.shouldRender}
|
||||
>
|
||||
{#snippet popper({ props, wrapperProps })}
|
||||
{@const finalProps = mergeProps(
|
||||
props,
|
||||
mergedProps,
|
||||
{ style: getFloatingContentCSSVars("menu") },
|
||||
{ style }
|
||||
)}
|
||||
{#if child}
|
||||
{@render child({
|
||||
props: finalProps,
|
||||
wrapperProps,
|
||||
...subContentState.snippetProps,
|
||||
})}
|
||||
{:else}
|
||||
<div {...wrapperProps}>
|
||||
<div {...finalProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</PopperLayerForceMount>
|
||||
{:else if !forceMount}
|
||||
<PopperLayer
|
||||
{...mergedProps}
|
||||
ref={subContentState.opts.ref}
|
||||
{interactOutsideBehavior}
|
||||
{escapeKeydownBehavior}
|
||||
onCloseAutoFocus={handleCloseAutoFocus}
|
||||
onOpenAutoFocus={handleOpenAutoFocus}
|
||||
open={subContentState.parentMenu.opts.open.current}
|
||||
onInteractOutside={handleInteractOutside}
|
||||
onEscapeKeydown={handleEscapeKeydown}
|
||||
onFocusOutside={handleOnFocusOutside}
|
||||
preventScroll={false}
|
||||
{loop}
|
||||
{trapFocus}
|
||||
shouldRender={subContentState.shouldRender}
|
||||
>
|
||||
{#snippet popper({ props, wrapperProps })}
|
||||
{@const finalProps = mergeProps(
|
||||
props,
|
||||
mergedProps,
|
||||
{ style: getFloatingContentCSSVars("menu") },
|
||||
{ style }
|
||||
)}
|
||||
{#if child}
|
||||
{@render child({
|
||||
props: finalProps,
|
||||
wrapperProps,
|
||||
...subContentState.snippetProps,
|
||||
})}
|
||||
{:else}
|
||||
<div {...wrapperProps}>
|
||||
<div {...finalProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</PopperLayer>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { MenuSubContentProps } from "../types.js";
|
||||
declare const MenuSubContent: import("svelte").Component<MenuSubContentProps, {}, "ref">;
|
||||
type MenuSubContent = ReturnType<typeof MenuSubContent>;
|
||||
export default MenuSubContent;
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenuSubTriggerProps } from "../types.js";
|
||||
import { MenuSubTriggerState } from "../menu.svelte.js";
|
||||
import FloatingLayerAnchor from "../../utilities/floating-layer/components/floating-layer-anchor.svelte";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
disabled = false,
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
onSelect = noop,
|
||||
openDelay = 100,
|
||||
...restProps
|
||||
}: MenuSubTriggerProps = $props();
|
||||
|
||||
const subTriggerState = MenuSubTriggerState.create({
|
||||
disabled: boxWith(() => disabled),
|
||||
onSelect: boxWith(() => onSelect),
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
openDelay: boxWith(() => openDelay),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, subTriggerState.props));
|
||||
</script>
|
||||
|
||||
<FloatingLayerAnchor {id} ref={subTriggerState.opts.ref}>
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
</FloatingLayerAnchor>
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { MenuSubTriggerProps } from "../types.js";
|
||||
declare const MenuSubTrigger: import("svelte").Component<MenuSubTriggerProps, {}, "ref">;
|
||||
type MenuSubTrigger = ReturnType<typeof MenuSubTrigger>;
|
||||
export default MenuSubTrigger;
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { boxWith } from "svelte-toolbelt";
|
||||
import type { MenuSubProps } from "../types.js";
|
||||
import { MenuSubmenuState } from "../menu.svelte.js";
|
||||
import FloatingLayer from "../../utilities/floating-layer/components/floating-layer.svelte";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
onOpenChange = noop,
|
||||
onOpenChangeComplete = noop,
|
||||
children,
|
||||
}: MenuSubProps = $props();
|
||||
|
||||
MenuSubmenuState.create({
|
||||
open: boxWith(
|
||||
() => open,
|
||||
(v) => {
|
||||
open = v;
|
||||
onOpenChange?.(v);
|
||||
}
|
||||
),
|
||||
onOpenChangeComplete: boxWith(() => onOpenChangeComplete),
|
||||
});
|
||||
</script>
|
||||
|
||||
<FloatingLayer>
|
||||
{@render children?.()}
|
||||
</FloatingLayer>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare const MenuSub: import("svelte").Component<import("../types.js").MenuSubPropsWithoutHTML, {}, "open">;
|
||||
type MenuSub = ReturnType<typeof MenuSub>;
|
||||
export default MenuSub;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenuTriggerProps } from "../types.js";
|
||||
import { DropdownMenuTriggerState } from "../menu.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import FloatingLayerAnchor from "../../utilities/floating-layer/components/floating-layer-anchor.svelte";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
child,
|
||||
children,
|
||||
disabled = false,
|
||||
type = "button",
|
||||
...restProps
|
||||
}: MenuTriggerProps = $props();
|
||||
|
||||
const triggerState = DropdownMenuTriggerState.create({
|
||||
id: boxWith(() => id),
|
||||
disabled: boxWith(() => disabled ?? false),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, triggerState.props, { type }));
|
||||
</script>
|
||||
|
||||
<FloatingLayerAnchor {id} ref={triggerState.opts.ref}>
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<button {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
</FloatingLayerAnchor>
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { MenuTriggerProps } from "../types.js";
|
||||
declare const MenuTrigger: import("svelte").Component<MenuTriggerProps, {}, "ref">;
|
||||
type MenuTrigger = ReturnType<typeof MenuTrigger>;
|
||||
export default MenuTrigger;
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { boxWith } from "svelte-toolbelt";
|
||||
import type { MenuRootProps } from "../types.js";
|
||||
import { MenuMenuState, MenuRootState } from "../menu.svelte.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
import FloatingLayer from "../../utilities/floating-layer/components/floating-layer.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
dir = "ltr",
|
||||
onOpenChange = noop,
|
||||
onOpenChangeComplete = noop,
|
||||
_internal_variant: variant = "dropdown-menu",
|
||||
children,
|
||||
}: MenuRootProps & {
|
||||
_internal_variant?: "context-menu" | "dropdown-menu" | "menubar";
|
||||
} = $props();
|
||||
|
||||
const root = MenuRootState.create({
|
||||
variant: boxWith(() => variant),
|
||||
dir: boxWith(() => dir),
|
||||
onClose: () => {
|
||||
open = false;
|
||||
onOpenChange(false);
|
||||
},
|
||||
});
|
||||
|
||||
MenuMenuState.create(
|
||||
{
|
||||
open: boxWith(
|
||||
() => open,
|
||||
(v) => {
|
||||
open = v;
|
||||
onOpenChange(v);
|
||||
}
|
||||
),
|
||||
onOpenChangeComplete: boxWith(() => onOpenChangeComplete),
|
||||
},
|
||||
root
|
||||
);
|
||||
</script>
|
||||
|
||||
<FloatingLayer>
|
||||
{@render children?.()}
|
||||
</FloatingLayer>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import type { MenuRootProps } from "../types.js";
|
||||
type $$ComponentProps = MenuRootProps & {
|
||||
_internal_variant?: "context-menu" | "dropdown-menu" | "menubar";
|
||||
};
|
||||
declare const Menu: import("svelte").Component<$$ComponentProps, {}, "open">;
|
||||
type Menu = ReturnType<typeof Menu>;
|
||||
export default Menu;
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
export { default as Root } from "./components/menu.svelte";
|
||||
export { default as Arrow } from "./components/menu-arrow.svelte";
|
||||
export { default as CheckboxGroup } from "./components/menu-checkbox-group.svelte";
|
||||
export { default as CheckboxItem } from "./components/menu-checkbox-item.svelte";
|
||||
export { default as Content } from "./components/menu-content.svelte";
|
||||
export { default as ContentStatic } from "./components/menu-content-static.svelte";
|
||||
export { default as Group } from "./components/menu-group.svelte";
|
||||
export { default as Item } from "./components/menu-item.svelte";
|
||||
export { default as GroupHeading } from "./components/menu-group-heading.svelte";
|
||||
export { default as Portal } from "../utilities/portal/portal.svelte";
|
||||
export { default as RadioGroup } from "./components/menu-radio-group.svelte";
|
||||
export { default as RadioItem } from "./components/menu-radio-item.svelte";
|
||||
export { default as Separator } from "./components/menu-separator.svelte";
|
||||
export { default as Sub } from "./components/menu-sub.svelte";
|
||||
export { default as SubContent } from "./components/menu-sub-content.svelte";
|
||||
export { default as SubTrigger } from "./components/menu-sub-trigger.svelte";
|
||||
export { default as Trigger } from "./components/menu-trigger.svelte";
|
||||
export { default as SubContentStatic } from "./components/menu-sub-content-static.svelte";
|
||||
export type { MenuRootPropsWithoutHTML as RootProps, MenuContentProps as ContentProps, MenuContentStaticProps as ContentStaticProps, MenuItemProps as ItemProps, MenuTriggerProps as TriggerProps, MenuSubPropsWithoutHTML as SubProps, MenuSubContentProps as SubContentProps, MenuSubContentStaticProps as SubContentStaticProps, MenuSeparatorProps as SeparatorProps, MenuArrowProps as ArrowProps, MenuCheckboxGroupProps as CheckboxGroupProps, MenuCheckboxItemProps as CheckboxItemProps, MenuGroupHeadingProps as GroupHeadingProps, MenuGroupProps as GroupProps, MenuRadioGroupProps as RadioGroupProps, MenuRadioItemProps as RadioItemProps, MenuSubTriggerProps as SubTriggerProps, MenuPortalProps as PortalProps, } from "./types.js";
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
export { default as Root } from "./components/menu.svelte";
|
||||
export { default as Arrow } from "./components/menu-arrow.svelte";
|
||||
export { default as CheckboxGroup } from "./components/menu-checkbox-group.svelte";
|
||||
export { default as CheckboxItem } from "./components/menu-checkbox-item.svelte";
|
||||
export { default as Content } from "./components/menu-content.svelte";
|
||||
export { default as ContentStatic } from "./components/menu-content-static.svelte";
|
||||
export { default as Group } from "./components/menu-group.svelte";
|
||||
export { default as Item } from "./components/menu-item.svelte";
|
||||
export { default as GroupHeading } from "./components/menu-group-heading.svelte";
|
||||
export { default as Portal } from "../utilities/portal/portal.svelte";
|
||||
export { default as RadioGroup } from "./components/menu-radio-group.svelte";
|
||||
export { default as RadioItem } from "./components/menu-radio-item.svelte";
|
||||
export { default as Separator } from "./components/menu-separator.svelte";
|
||||
export { default as Sub } from "./components/menu-sub.svelte";
|
||||
export { default as SubContent } from "./components/menu-sub-content.svelte";
|
||||
export { default as SubTrigger } from "./components/menu-sub-trigger.svelte";
|
||||
export { default as Trigger } from "./components/menu-trigger.svelte";
|
||||
export { default as SubContentStatic } from "./components/menu-sub-content-static.svelte";
|
||||
+430
@@ -0,0 +1,430 @@
|
||||
import { DOMContext, type ReadableBoxedValues, type WritableBoxedValues, type ReadableBox } from "svelte-toolbelt";
|
||||
import { Context } from "runed";
|
||||
import { CustomEventDispatcher } from "../../internal/events.js";
|
||||
import type { AnyFn, BitsFocusEvent, BitsKeyboardEvent, BitsMouseEvent, BitsPointerEvent, OnChangeFn, RefAttachment, WithRefOpts } from "../../internal/types.js";
|
||||
import type { Direction } from "../../shared/index.js";
|
||||
import { IsUsingKeyboard } from "../utilities/is-using-keyboard/is-using-keyboard.svelte.js";
|
||||
import type { KeyboardEventHandler, PointerEventHandler, MouseEventHandler } from "svelte/elements";
|
||||
import { RovingFocusGroup } from "../../internal/roving-focus-group.js";
|
||||
import { PresenceManager } from "../../internal/presence-manager.svelte.js";
|
||||
export declare const CONTEXT_MENU_TRIGGER_ATTR = "data-context-menu-trigger";
|
||||
export declare const CONTEXT_MENU_CONTENT_ATTR = "data-context-menu-content";
|
||||
export declare const MenuCheckboxGroupContext: Context<MenuCheckboxGroupState>;
|
||||
type MenuVariant = "context-menu" | "dropdown-menu" | "menubar";
|
||||
export interface MenuRootStateOpts extends ReadableBoxedValues<{
|
||||
dir: Direction;
|
||||
variant: MenuVariant;
|
||||
}> {
|
||||
onClose: AnyFn;
|
||||
}
|
||||
export declare const MenuOpenEvent: CustomEventDispatcher<unknown>;
|
||||
export declare const menuAttrs: import("../../internal/attrs.js").CreateBitsAttrsReturn<readonly ["trigger", "content", "sub-trigger", "item", "group", "group-heading", "checkbox-group", "checkbox-item", "radio-group", "radio-item", "separator", "sub-content", "arrow"]>;
|
||||
export declare class MenuRootState {
|
||||
static create(opts: MenuRootStateOpts): MenuRootState;
|
||||
readonly opts: MenuRootStateOpts;
|
||||
readonly isUsingKeyboard: IsUsingKeyboard;
|
||||
ignoreCloseAutoFocus: boolean;
|
||||
isPointerInTransit: boolean;
|
||||
constructor(opts: MenuRootStateOpts);
|
||||
getBitsAttr: typeof menuAttrs.getAttr;
|
||||
}
|
||||
interface MenuMenuStateOpts extends WritableBoxedValues<{
|
||||
open: boolean;
|
||||
}>, ReadableBoxedValues<{
|
||||
onOpenChangeComplete: OnChangeFn<boolean>;
|
||||
}> {
|
||||
}
|
||||
export declare class MenuMenuState {
|
||||
static create(opts: MenuMenuStateOpts, root: MenuRootState): MenuMenuState;
|
||||
readonly opts: MenuMenuStateOpts;
|
||||
readonly root: MenuRootState;
|
||||
readonly parentMenu: MenuMenuState | null;
|
||||
contentId: ReadableBox<string>;
|
||||
contentNode: HTMLElement | null;
|
||||
contentPresence: PresenceManager;
|
||||
triggerNode: HTMLElement | null;
|
||||
constructor(opts: MenuMenuStateOpts, root: MenuRootState, parentMenu: MenuMenuState | null);
|
||||
toggleOpen(): void;
|
||||
onOpen(): void;
|
||||
onClose(): void;
|
||||
}
|
||||
interface MenuContentStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
loop: boolean;
|
||||
onCloseAutoFocus: (event: Event) => void;
|
||||
}> {
|
||||
isSub?: boolean;
|
||||
}
|
||||
export declare class MenuContentState {
|
||||
#private;
|
||||
static create(opts: MenuContentStateOpts): MenuContentState;
|
||||
readonly opts: MenuContentStateOpts;
|
||||
readonly parentMenu: MenuMenuState;
|
||||
readonly rovingFocusGroup: RovingFocusGroup;
|
||||
readonly domContext: DOMContext;
|
||||
readonly attachment: RefAttachment;
|
||||
search: string;
|
||||
mounted: boolean;
|
||||
constructor(opts: MenuContentStateOpts, parentMenu: MenuMenuState);
|
||||
onCloseAutoFocus: (e: Event) => void;
|
||||
handleTabKeyDown(e: BitsKeyboardEvent): void;
|
||||
onkeydown(e: BitsKeyboardEvent): void;
|
||||
onblur(e: BitsFocusEvent): void;
|
||||
onfocus(_: BitsFocusEvent): void;
|
||||
onItemEnter(): boolean;
|
||||
onItemLeave(e: BitsPointerEvent): void;
|
||||
onTriggerLeave(): boolean;
|
||||
handleInteractOutside(e: PointerEvent): void;
|
||||
get shouldRender(): boolean;
|
||||
readonly snippetProps: {
|
||||
open: boolean;
|
||||
};
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly role: "menu";
|
||||
readonly "aria-orientation": "vertical";
|
||||
readonly "data-state": "open" | "closed";
|
||||
readonly onkeydown: (e: BitsKeyboardEvent) => void;
|
||||
readonly onblur: (e: BitsFocusEvent) => void;
|
||||
readonly onfocus: (_: BitsFocusEvent) => void;
|
||||
readonly dir: Direction;
|
||||
readonly style: {
|
||||
readonly pointerEvents: "auto";
|
||||
readonly contain: "layout style";
|
||||
};
|
||||
};
|
||||
readonly popperProps: {
|
||||
onCloseAutoFocus: (e: Event) => void;
|
||||
};
|
||||
}
|
||||
interface MenuItemSharedStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
disabled: boolean;
|
||||
}> {
|
||||
}
|
||||
declare class MenuItemSharedState {
|
||||
#private;
|
||||
readonly opts: MenuItemSharedStateOpts;
|
||||
readonly content: MenuContentState;
|
||||
readonly attachment: RefAttachment;
|
||||
constructor(opts: MenuItemSharedStateOpts, content: MenuContentState);
|
||||
onpointermove(e: BitsPointerEvent): void;
|
||||
onpointerleave(e: BitsPointerEvent): void;
|
||||
onfocus(e: BitsFocusEvent): void;
|
||||
onblur(e: BitsFocusEvent): void;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly tabindex: -1;
|
||||
readonly role: "menuitem";
|
||||
readonly "aria-disabled": "true" | "false";
|
||||
readonly "data-disabled": "" | undefined;
|
||||
readonly "data-highlighted": "" | undefined;
|
||||
readonly onpointermove: (e: BitsPointerEvent) => void;
|
||||
readonly onpointerleave: (e: BitsPointerEvent) => void;
|
||||
readonly onfocus: (e: BitsFocusEvent) => void;
|
||||
readonly onblur: (e: BitsFocusEvent) => void;
|
||||
};
|
||||
}
|
||||
type MenuItemCombinedProps = MenuItemSharedStateOpts & MenuItemStateOpts;
|
||||
interface MenuItemStateOpts extends ReadableBoxedValues<{
|
||||
onSelect: AnyFn;
|
||||
closeOnSelect: boolean;
|
||||
}> {
|
||||
}
|
||||
export declare class MenuItemState {
|
||||
#private;
|
||||
static create(opts: MenuItemCombinedProps): MenuItemState;
|
||||
readonly opts: MenuItemStateOpts;
|
||||
readonly item: MenuItemSharedState;
|
||||
readonly root: MenuRootState;
|
||||
constructor(opts: MenuItemStateOpts, item: MenuItemSharedState);
|
||||
onkeydown(e: BitsKeyboardEvent): void;
|
||||
onclick(_: BitsMouseEvent): void;
|
||||
onpointerup(e: BitsPointerEvent): void;
|
||||
onpointerdown(_: BitsPointerEvent): void;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly tabindex: -1;
|
||||
readonly role: "menuitem";
|
||||
readonly "aria-disabled": "true" | "false";
|
||||
readonly "data-disabled": "" | undefined;
|
||||
readonly "data-highlighted": "" | undefined;
|
||||
readonly onpointermove: (e: BitsPointerEvent) => void;
|
||||
readonly onpointerleave: (e: BitsPointerEvent) => void;
|
||||
readonly onfocus: (e: BitsFocusEvent) => void;
|
||||
readonly onblur: (e: BitsFocusEvent) => void;
|
||||
} & {
|
||||
onclick: (_: BitsMouseEvent) => void;
|
||||
onpointerdown: (_: BitsPointerEvent) => void;
|
||||
onpointerup: (e: BitsPointerEvent) => void;
|
||||
onkeydown: (e: BitsKeyboardEvent) => void;
|
||||
} & {
|
||||
style?: string;
|
||||
};
|
||||
}
|
||||
interface MenuSubTriggerStateOpts extends MenuItemSharedStateOpts, Pick<MenuItemStateOpts, "onSelect"> {
|
||||
openDelay: ReadableBox<number>;
|
||||
}
|
||||
export declare class MenuSubTriggerState {
|
||||
#private;
|
||||
static create(opts: MenuSubTriggerStateOpts): MenuSubTriggerState;
|
||||
readonly opts: MenuSubTriggerStateOpts;
|
||||
readonly item: MenuItemSharedState;
|
||||
readonly content: MenuContentState;
|
||||
readonly submenu: MenuMenuState;
|
||||
readonly attachment: RefAttachment;
|
||||
constructor(opts: MenuSubTriggerStateOpts, item: MenuItemSharedState, content: MenuContentState, submenu: MenuMenuState);
|
||||
onpointermove(e: BitsPointerEvent): void;
|
||||
onpointerleave(e: BitsPointerEvent): void;
|
||||
onkeydown(e: BitsKeyboardEvent): void;
|
||||
onclick(e: BitsMouseEvent): void;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly tabindex: -1;
|
||||
readonly role: "menuitem";
|
||||
readonly "aria-disabled": "true" | "false";
|
||||
readonly "data-disabled": "" | undefined;
|
||||
readonly "data-highlighted": "" | undefined;
|
||||
readonly onpointermove: (e: BitsPointerEvent) => void;
|
||||
readonly onpointerleave: (e: BitsPointerEvent) => void;
|
||||
readonly onfocus: (e: BitsFocusEvent) => void;
|
||||
readonly onblur: (e: BitsFocusEvent) => void;
|
||||
} & {
|
||||
"aria-haspopup": string;
|
||||
"aria-expanded": "true" | "false";
|
||||
"data-state": "open" | "closed";
|
||||
"aria-controls": string | undefined;
|
||||
onclick: (e: BitsMouseEvent) => void;
|
||||
onpointermove: (e: BitsPointerEvent) => void;
|
||||
onpointerleave: (e: BitsPointerEvent) => void;
|
||||
onkeydown: (e: BitsKeyboardEvent) => void;
|
||||
} & {
|
||||
style?: string;
|
||||
};
|
||||
}
|
||||
interface MenuCheckboxItemStateOpts extends WritableBoxedValues<{
|
||||
checked: boolean;
|
||||
indeterminate: boolean;
|
||||
}>, ReadableBoxedValues<{
|
||||
value: string;
|
||||
}> {
|
||||
}
|
||||
export declare class MenuCheckboxItemState {
|
||||
static create(opts: MenuItemCombinedProps & MenuCheckboxItemStateOpts, checkboxGroup: MenuCheckboxGroupState | null): MenuCheckboxItemState;
|
||||
readonly opts: MenuCheckboxItemStateOpts;
|
||||
readonly item: MenuItemState;
|
||||
readonly group: MenuCheckboxGroupState | null;
|
||||
constructor(opts: MenuCheckboxItemStateOpts, item: MenuItemState, group?: MenuCheckboxGroupState | null);
|
||||
toggleChecked(): void;
|
||||
readonly snippetProps: {
|
||||
checked: boolean;
|
||||
indeterminate: boolean;
|
||||
};
|
||||
readonly props: {
|
||||
readonly role: "menuitemcheckbox";
|
||||
readonly "aria-checked": "true" | "false" | "mixed";
|
||||
readonly "data-state": "checked" | "indeterminate" | "unchecked";
|
||||
readonly id: string;
|
||||
readonly tabindex: -1;
|
||||
readonly "aria-disabled": "true" | "false";
|
||||
readonly "data-disabled": "" | undefined;
|
||||
readonly "data-highlighted": "" | undefined;
|
||||
readonly onpointermove: (e: BitsPointerEvent) => void;
|
||||
readonly onpointerleave: (e: BitsPointerEvent) => void;
|
||||
readonly onfocus: (e: BitsFocusEvent) => void;
|
||||
readonly onblur: (e: BitsFocusEvent) => void;
|
||||
readonly onclick: (_: BitsMouseEvent) => void;
|
||||
readonly onpointerdown: (_: BitsPointerEvent) => void;
|
||||
readonly onpointerup: (e: BitsPointerEvent) => void;
|
||||
readonly onkeydown: (e: BitsKeyboardEvent) => void;
|
||||
readonly style?: string;
|
||||
};
|
||||
}
|
||||
interface MenuGroupStateOpts extends WithRefOpts {
|
||||
}
|
||||
export declare class MenuGroupState {
|
||||
static create(opts: MenuGroupStateOpts): MenuGroupState | MenuRadioGroupState;
|
||||
readonly opts: MenuGroupStateOpts;
|
||||
readonly root: MenuRootState;
|
||||
readonly attachment: RefAttachment;
|
||||
groupHeadingId: string | undefined;
|
||||
constructor(opts: MenuGroupStateOpts, root: MenuRootState);
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly role: "group";
|
||||
readonly "aria-labelledby": string | undefined;
|
||||
};
|
||||
}
|
||||
interface MenuGroupHeadingStateOpts extends WithRefOpts {
|
||||
}
|
||||
export declare class MenuGroupHeadingState {
|
||||
static create(opts: MenuGroupHeadingStateOpts): MenuGroupHeadingState;
|
||||
readonly opts: MenuGroupHeadingStateOpts;
|
||||
readonly group: MenuGroupState | MenuRadioGroupState | MenuCheckboxGroupState;
|
||||
readonly attachment: RefAttachment;
|
||||
constructor(opts: MenuGroupHeadingStateOpts, group: MenuGroupState | MenuRadioGroupState | MenuCheckboxGroupState);
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly role: "group";
|
||||
};
|
||||
}
|
||||
interface MenuSeparatorStateOpts extends WithRefOpts {
|
||||
}
|
||||
export declare class MenuSeparatorState {
|
||||
static create(opts: MenuSeparatorStateOpts): MenuSeparatorState;
|
||||
readonly opts: MenuSeparatorStateOpts;
|
||||
readonly root: MenuRootState;
|
||||
readonly attachment: RefAttachment;
|
||||
constructor(opts: MenuSeparatorStateOpts, root: MenuRootState);
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly role: "group";
|
||||
};
|
||||
}
|
||||
export declare class MenuArrowState {
|
||||
static create(): MenuArrowState;
|
||||
readonly root: MenuRootState;
|
||||
constructor(root: MenuRootState);
|
||||
readonly props: {
|
||||
readonly [x: string]: "";
|
||||
};
|
||||
}
|
||||
interface MenuRadioGroupStateOpts extends WithRefOpts, WritableBoxedValues<{
|
||||
value: string;
|
||||
}> {
|
||||
}
|
||||
export declare class MenuRadioGroupState {
|
||||
static create(opts: MenuRadioGroupStateOpts): MenuGroupState | MenuRadioGroupState;
|
||||
readonly opts: MenuRadioGroupStateOpts;
|
||||
readonly content: MenuContentState;
|
||||
readonly attachment: RefAttachment;
|
||||
groupHeadingId: string | null;
|
||||
root: MenuRootState;
|
||||
constructor(opts: MenuRadioGroupStateOpts, content: MenuContentState);
|
||||
setValue(v: string): void;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly role: "group";
|
||||
readonly "aria-labelledby": string | null;
|
||||
};
|
||||
}
|
||||
interface MenuRadioItemStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
value: string;
|
||||
closeOnSelect: boolean;
|
||||
}> {
|
||||
}
|
||||
export declare class MenuRadioItemState {
|
||||
static create(opts: MenuRadioItemStateOpts & MenuItemCombinedProps): MenuRadioItemState;
|
||||
readonly opts: MenuRadioItemStateOpts;
|
||||
readonly item: MenuItemState;
|
||||
readonly group: MenuRadioGroupState;
|
||||
readonly attachment: RefAttachment;
|
||||
readonly isChecked: boolean;
|
||||
constructor(opts: MenuRadioItemStateOpts, item: MenuItemState, group: MenuRadioGroupState);
|
||||
selectValue(): void;
|
||||
readonly props: {
|
||||
readonly role: "menuitemradio";
|
||||
readonly "aria-checked": "true" | "false" | "mixed";
|
||||
readonly "data-state": "checked" | "indeterminate" | "unchecked";
|
||||
readonly id: string;
|
||||
readonly tabindex: -1;
|
||||
readonly "aria-disabled": "true" | "false";
|
||||
readonly "data-disabled": "" | undefined;
|
||||
readonly "data-highlighted": "" | undefined;
|
||||
readonly onpointermove: (e: BitsPointerEvent) => void;
|
||||
readonly onpointerleave: (e: BitsPointerEvent) => void;
|
||||
readonly onfocus: (e: BitsFocusEvent) => void;
|
||||
readonly onblur: (e: BitsFocusEvent) => void;
|
||||
readonly onclick: (_: BitsMouseEvent) => void;
|
||||
readonly onpointerdown: (_: BitsPointerEvent) => void;
|
||||
readonly onpointerup: (e: BitsPointerEvent) => void;
|
||||
readonly onkeydown: (e: BitsKeyboardEvent) => void;
|
||||
readonly style?: string;
|
||||
};
|
||||
}
|
||||
interface DropdownMenuTriggerStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
disabled: boolean;
|
||||
}> {
|
||||
}
|
||||
export declare class DropdownMenuTriggerState {
|
||||
#private;
|
||||
static create(opts: DropdownMenuTriggerStateOpts): DropdownMenuTriggerState;
|
||||
readonly opts: DropdownMenuTriggerStateOpts;
|
||||
readonly parentMenu: MenuMenuState;
|
||||
readonly attachment: RefAttachment;
|
||||
constructor(opts: DropdownMenuTriggerStateOpts, parentMenu: MenuMenuState);
|
||||
onclick: MouseEventHandler<HTMLElement>;
|
||||
onpointerdown: PointerEventHandler<HTMLElement>;
|
||||
onpointerup: PointerEventHandler<HTMLElement>;
|
||||
onkeydown: KeyboardEventHandler<HTMLElement>;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly disabled: boolean;
|
||||
readonly "aria-haspopup": "menu";
|
||||
readonly "aria-expanded": "true" | "false";
|
||||
readonly "aria-controls": string | undefined;
|
||||
readonly "data-disabled": "" | undefined;
|
||||
readonly "data-state": "open" | "closed";
|
||||
readonly onclick: MouseEventHandler<HTMLElement>;
|
||||
readonly onpointerdown: PointerEventHandler<HTMLElement>;
|
||||
readonly onpointerup: PointerEventHandler<HTMLElement>;
|
||||
readonly onkeydown: KeyboardEventHandler<HTMLElement>;
|
||||
};
|
||||
}
|
||||
interface ContextMenuTriggerStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
disabled: boolean;
|
||||
}> {
|
||||
}
|
||||
export declare class ContextMenuTriggerState {
|
||||
#private;
|
||||
static create(opts: ContextMenuTriggerStateOpts): ContextMenuTriggerState;
|
||||
readonly opts: ContextMenuTriggerStateOpts;
|
||||
readonly parentMenu: MenuMenuState;
|
||||
readonly attachment: RefAttachment;
|
||||
virtualElement: import("svelte-toolbelt").WritableBox<{
|
||||
getBoundingClientRect: () => DOMRect;
|
||||
}>;
|
||||
constructor(opts: ContextMenuTriggerStateOpts, parentMenu: MenuMenuState);
|
||||
oncontextmenu(e: BitsMouseEvent): void;
|
||||
onpointerdown(e: BitsPointerEvent): void;
|
||||
onpointermove(e: BitsPointerEvent): void;
|
||||
onpointercancel(e: BitsPointerEvent): void;
|
||||
onpointerup(e: BitsPointerEvent): void;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly disabled: boolean;
|
||||
readonly "data-disabled": "" | undefined;
|
||||
readonly "data-state": "open" | "closed";
|
||||
readonly "data-context-menu-trigger": "";
|
||||
readonly tabindex: -1;
|
||||
readonly onpointerdown: (e: BitsPointerEvent) => void;
|
||||
readonly onpointermove: (e: BitsPointerEvent) => void;
|
||||
readonly onpointercancel: (e: BitsPointerEvent) => void;
|
||||
readonly onpointerup: (e: BitsPointerEvent) => void;
|
||||
readonly oncontextmenu: (e: BitsMouseEvent) => void;
|
||||
};
|
||||
}
|
||||
interface MenuCheckboxGroupStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
onValueChange: (value: string[]) => void;
|
||||
}>, WritableBoxedValues<{
|
||||
value: string[];
|
||||
}> {
|
||||
}
|
||||
export declare class MenuCheckboxGroupState {
|
||||
static create(opts: MenuCheckboxGroupStateOpts): MenuCheckboxGroupState;
|
||||
readonly opts: MenuCheckboxGroupStateOpts;
|
||||
readonly content: MenuContentState;
|
||||
readonly root: MenuRootState;
|
||||
readonly attachment: RefAttachment;
|
||||
groupHeadingId: string | null;
|
||||
constructor(opts: MenuCheckboxGroupStateOpts, content: MenuContentState);
|
||||
addValue(checkboxValue: string | undefined): void;
|
||||
removeValue(checkboxValue: string | undefined): void;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly role: "group";
|
||||
readonly "aria-labelledby": string | null;
|
||||
};
|
||||
}
|
||||
export declare class MenuSubmenuState {
|
||||
static create(opts: MenuMenuStateOpts): MenuMenuState;
|
||||
}
|
||||
export {};
|
||||
+978
@@ -0,0 +1,978 @@
|
||||
import { afterTick, mergeProps, onDestroyEffect, attachRef, DOMContext, getWindow, simpleBox, boxWith, } from "svelte-toolbelt";
|
||||
import { Context, watch } from "runed";
|
||||
import { FIRST_LAST_KEYS, LAST_KEYS, SELECTION_KEYS, SUB_OPEN_KEYS, getCheckedState, isMouseEvent, } from "./utils.js";
|
||||
import { focusFirst } from "../../internal/focus.js";
|
||||
import { CustomEventDispatcher } from "../../internal/events.js";
|
||||
import { isElement, isElementOrSVGElement, isHTMLElement } from "../../internal/is.js";
|
||||
import { kbd } from "../../internal/kbd.js";
|
||||
import { createBitsAttrs, getAriaChecked, boolToStr, getDataOpenClosed, boolToEmptyStrOrUndef, } from "../../internal/attrs.js";
|
||||
import { IsUsingKeyboard } from "../utilities/is-using-keyboard/is-using-keyboard.svelte.js";
|
||||
import { getTabbableFrom } from "../../internal/tabbable.js";
|
||||
import { isTabbable } from "tabbable";
|
||||
import { DOMTypeahead } from "../../internal/dom-typeahead.svelte.js";
|
||||
import { RovingFocusGroup } from "../../internal/roving-focus-group.js";
|
||||
import { GraceArea } from "../../internal/grace-area.svelte.js";
|
||||
import { PresenceManager } from "../../internal/presence-manager.svelte.js";
|
||||
import { arraysAreEqual } from "../../internal/arrays.js";
|
||||
export const CONTEXT_MENU_TRIGGER_ATTR = "data-context-menu-trigger";
|
||||
export const CONTEXT_MENU_CONTENT_ATTR = "data-context-menu-content";
|
||||
const MenuRootContext = new Context("Menu.Root");
|
||||
const MenuMenuContext = new Context("Menu.Root | Menu.Sub");
|
||||
const MenuContentContext = new Context("Menu.Content");
|
||||
const MenuGroupContext = new Context("Menu.Group | Menu.RadioGroup");
|
||||
const MenuRadioGroupContext = new Context("Menu.RadioGroup");
|
||||
export const MenuCheckboxGroupContext = new Context("Menu.CheckboxGroup");
|
||||
export const MenuOpenEvent = new CustomEventDispatcher("bitsmenuopen", {
|
||||
bubbles: false,
|
||||
cancelable: true,
|
||||
});
|
||||
export const menuAttrs = createBitsAttrs({
|
||||
component: "menu",
|
||||
parts: [
|
||||
"trigger",
|
||||
"content",
|
||||
"sub-trigger",
|
||||
"item",
|
||||
"group",
|
||||
"group-heading",
|
||||
"checkbox-group",
|
||||
"checkbox-item",
|
||||
"radio-group",
|
||||
"radio-item",
|
||||
"separator",
|
||||
"sub-content",
|
||||
"arrow",
|
||||
],
|
||||
});
|
||||
export class MenuRootState {
|
||||
static create(opts) {
|
||||
const root = new MenuRootState(opts);
|
||||
return MenuRootContext.set(root);
|
||||
}
|
||||
opts;
|
||||
isUsingKeyboard = new IsUsingKeyboard();
|
||||
ignoreCloseAutoFocus = $state(false);
|
||||
isPointerInTransit = $state(false);
|
||||
constructor(opts) {
|
||||
this.opts = opts;
|
||||
}
|
||||
getBitsAttr = (part) => {
|
||||
return menuAttrs.getAttr(part, this.opts.variant.current);
|
||||
};
|
||||
}
|
||||
export class MenuMenuState {
|
||||
static create(opts, root) {
|
||||
return MenuMenuContext.set(new MenuMenuState(opts, root, null));
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
parentMenu;
|
||||
contentId = boxWith(() => "");
|
||||
contentNode = $state(null);
|
||||
contentPresence;
|
||||
triggerNode = $state(null);
|
||||
constructor(opts, root, parentMenu) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.parentMenu = parentMenu;
|
||||
this.contentPresence = new PresenceManager({
|
||||
ref: boxWith(() => this.contentNode),
|
||||
open: this.opts.open,
|
||||
onComplete: () => {
|
||||
this.opts.onOpenChangeComplete.current(this.opts.open.current);
|
||||
},
|
||||
});
|
||||
if (parentMenu) {
|
||||
watch(() => parentMenu.opts.open.current, () => {
|
||||
if (parentMenu.opts.open.current)
|
||||
return;
|
||||
this.opts.open.current = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
toggleOpen() {
|
||||
this.opts.open.current = !this.opts.open.current;
|
||||
}
|
||||
onOpen() {
|
||||
this.opts.open.current = true;
|
||||
}
|
||||
onClose() {
|
||||
this.opts.open.current = false;
|
||||
}
|
||||
}
|
||||
export class MenuContentState {
|
||||
static create(opts) {
|
||||
return MenuContentContext.set(new MenuContentState(opts, MenuMenuContext.get()));
|
||||
}
|
||||
opts;
|
||||
parentMenu;
|
||||
rovingFocusGroup;
|
||||
domContext;
|
||||
attachment;
|
||||
search = $state("");
|
||||
#timer = 0;
|
||||
#handleTypeaheadSearch;
|
||||
mounted = $state(false);
|
||||
#isSub;
|
||||
constructor(opts, parentMenu) {
|
||||
this.opts = opts;
|
||||
this.parentMenu = parentMenu;
|
||||
this.domContext = new DOMContext(opts.ref);
|
||||
this.attachment = attachRef(this.opts.ref, (v) => {
|
||||
if (this.parentMenu.contentNode !== v) {
|
||||
this.parentMenu.contentNode = v;
|
||||
}
|
||||
});
|
||||
parentMenu.contentId = opts.id;
|
||||
this.#isSub = opts.isSub ?? false;
|
||||
this.onkeydown = this.onkeydown.bind(this);
|
||||
this.onblur = this.onblur.bind(this);
|
||||
this.onfocus = this.onfocus.bind(this);
|
||||
this.handleInteractOutside = this.handleInteractOutside.bind(this);
|
||||
new GraceArea({
|
||||
contentNode: () => this.parentMenu.contentNode,
|
||||
triggerNode: () => this.parentMenu.triggerNode,
|
||||
enabled: () => this.parentMenu.opts.open.current &&
|
||||
Boolean(this.parentMenu.triggerNode?.hasAttribute(this.parentMenu.root.getBitsAttr("sub-trigger"))),
|
||||
onPointerExit: () => {
|
||||
this.parentMenu.opts.open.current = false;
|
||||
},
|
||||
setIsPointerInTransit: (value) => {
|
||||
this.parentMenu.root.isPointerInTransit = value;
|
||||
},
|
||||
});
|
||||
this.#handleTypeaheadSearch = new DOMTypeahead({
|
||||
getActiveElement: () => this.domContext.getActiveElement(),
|
||||
getWindow: () => this.domContext.getWindow(),
|
||||
}).handleTypeaheadSearch;
|
||||
this.rovingFocusGroup = new RovingFocusGroup({
|
||||
rootNode: boxWith(() => this.parentMenu.contentNode),
|
||||
candidateAttr: this.parentMenu.root.getBitsAttr("item"),
|
||||
loop: this.opts.loop,
|
||||
orientation: boxWith(() => "vertical"),
|
||||
});
|
||||
watch(() => this.parentMenu.contentNode, (contentNode) => {
|
||||
if (!contentNode)
|
||||
return;
|
||||
const handler = () => {
|
||||
afterTick(() => {
|
||||
if (!this.parentMenu.root.isUsingKeyboard.current)
|
||||
return;
|
||||
this.rovingFocusGroup.focusFirstCandidate();
|
||||
});
|
||||
};
|
||||
return MenuOpenEvent.listen(contentNode, handler);
|
||||
});
|
||||
$effect(() => {
|
||||
if (!this.parentMenu.opts.open.current) {
|
||||
this.domContext.getWindow().clearTimeout(this.#timer);
|
||||
}
|
||||
});
|
||||
}
|
||||
#getCandidateNodes() {
|
||||
const node = this.parentMenu.contentNode;
|
||||
if (!node)
|
||||
return [];
|
||||
const candidates = Array.from(node.querySelectorAll(`[${this.parentMenu.root.getBitsAttr("item")}]:not([data-disabled])`));
|
||||
return candidates;
|
||||
}
|
||||
#isPointerMovingToSubmenu() {
|
||||
return this.parentMenu.root.isPointerInTransit;
|
||||
}
|
||||
onCloseAutoFocus = (e) => {
|
||||
this.opts.onCloseAutoFocus.current?.(e);
|
||||
if (e.defaultPrevented || this.#isSub)
|
||||
return;
|
||||
if (this.parentMenu.triggerNode && isTabbable(this.parentMenu.triggerNode)) {
|
||||
e.preventDefault();
|
||||
this.parentMenu.triggerNode.focus();
|
||||
}
|
||||
};
|
||||
handleTabKeyDown(e) {
|
||||
/**
|
||||
* We locate the root `menu`'s trigger by going up the tree until
|
||||
* we find a menu that has no parent. This will allow us to focus the next
|
||||
* tabbable element before/after the root trigger.
|
||||
*/
|
||||
let rootMenu = this.parentMenu;
|
||||
while (rootMenu.parentMenu !== null) {
|
||||
rootMenu = rootMenu.parentMenu;
|
||||
}
|
||||
// if for some unforeseen reason the root menu has no trigger, we bail
|
||||
if (!rootMenu.triggerNode)
|
||||
return;
|
||||
// cancel default tab behavior
|
||||
e.preventDefault();
|
||||
// find the next/previous tabbable
|
||||
const nodeToFocus = getTabbableFrom(rootMenu.triggerNode, e.shiftKey ? "prev" : "next");
|
||||
if (nodeToFocus) {
|
||||
/**
|
||||
* We set a flag to ignore the `onCloseAutoFocus` event handler
|
||||
* as well as the fallbacks inside the focus scope to prevent
|
||||
* race conditions causing focus to fall back to the body even
|
||||
* though we're trying to focus the next tabbable element.
|
||||
*/
|
||||
this.parentMenu.root.ignoreCloseAutoFocus = true;
|
||||
rootMenu.onClose();
|
||||
afterTick(() => {
|
||||
nodeToFocus.focus();
|
||||
afterTick(() => {
|
||||
this.parentMenu.root.ignoreCloseAutoFocus = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.domContext.getDocument().body.focus();
|
||||
}
|
||||
}
|
||||
onkeydown(e) {
|
||||
if (e.defaultPrevented)
|
||||
return;
|
||||
if (e.key === kbd.TAB) {
|
||||
this.handleTabKeyDown(e);
|
||||
return;
|
||||
}
|
||||
const target = e.target;
|
||||
const currentTarget = e.currentTarget;
|
||||
if (!isHTMLElement(target) || !isHTMLElement(currentTarget))
|
||||
return;
|
||||
const isKeydownInside = target.closest(`[${this.parentMenu.root.getBitsAttr("content")}]`)?.id ===
|
||||
this.parentMenu.contentId.current;
|
||||
const isModifierKey = e.ctrlKey || e.altKey || e.metaKey;
|
||||
const isCharacterKey = e.key.length === 1;
|
||||
const kbdFocusedEl = this.rovingFocusGroup.handleKeydown(target, e);
|
||||
if (kbdFocusedEl)
|
||||
return;
|
||||
// prevent space from being considered with typeahead
|
||||
if (e.code === "Space")
|
||||
return;
|
||||
const candidateNodes = this.#getCandidateNodes();
|
||||
if (isKeydownInside) {
|
||||
if (!isModifierKey && isCharacterKey) {
|
||||
this.#handleTypeaheadSearch(e.key, candidateNodes);
|
||||
}
|
||||
}
|
||||
// focus first/last based on key pressed
|
||||
if (e.target?.id !== this.parentMenu.contentId.current)
|
||||
return;
|
||||
if (!FIRST_LAST_KEYS.includes(e.key))
|
||||
return;
|
||||
e.preventDefault();
|
||||
if (LAST_KEYS.includes(e.key)) {
|
||||
candidateNodes.reverse();
|
||||
}
|
||||
focusFirst(candidateNodes, { select: false }, () => this.domContext.getActiveElement());
|
||||
}
|
||||
onblur(e) {
|
||||
if (!isElement(e.currentTarget))
|
||||
return;
|
||||
if (!isElement(e.target))
|
||||
return;
|
||||
// clear search buffer when leaving the menu
|
||||
if (!e.currentTarget.contains?.(e.target)) {
|
||||
this.domContext.getWindow().clearTimeout(this.#timer);
|
||||
this.search = "";
|
||||
}
|
||||
}
|
||||
onfocus(_) {
|
||||
if (!this.parentMenu.root.isUsingKeyboard.current)
|
||||
return;
|
||||
afterTick(() => this.rovingFocusGroup.focusFirstCandidate());
|
||||
}
|
||||
onItemEnter() {
|
||||
return this.#isPointerMovingToSubmenu();
|
||||
}
|
||||
onItemLeave(e) {
|
||||
if (e.currentTarget.hasAttribute(this.parentMenu.root.getBitsAttr("sub-trigger")))
|
||||
return;
|
||||
if (this.#isPointerMovingToSubmenu() || this.parentMenu.root.isUsingKeyboard.current)
|
||||
return;
|
||||
const contentNode = this.parentMenu.contentNode;
|
||||
contentNode?.focus();
|
||||
this.rovingFocusGroup.setCurrentTabStopId("");
|
||||
}
|
||||
onTriggerLeave() {
|
||||
if (this.#isPointerMovingToSubmenu())
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
handleInteractOutside(e) {
|
||||
if (!isElementOrSVGElement(e.target))
|
||||
return;
|
||||
const triggerId = this.parentMenu.triggerNode?.id;
|
||||
if (e.target.id === triggerId) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (e.target.closest(`#${triggerId}`)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
get shouldRender() {
|
||||
return this.parentMenu.contentPresence.shouldRender;
|
||||
}
|
||||
snippetProps = $derived.by(() => ({ open: this.parentMenu.opts.open.current }));
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
role: "menu",
|
||||
"aria-orientation": "vertical",
|
||||
[this.parentMenu.root.getBitsAttr("content")]: "",
|
||||
"data-state": getDataOpenClosed(this.parentMenu.opts.open.current),
|
||||
onkeydown: this.onkeydown,
|
||||
onblur: this.onblur,
|
||||
onfocus: this.onfocus,
|
||||
dir: this.parentMenu.root.opts.dir.current,
|
||||
style: {
|
||||
pointerEvents: "auto",
|
||||
// CSS containment isolates style/layout/paint calculations from the rest of the page
|
||||
contain: "layout style",
|
||||
},
|
||||
...this.attachment,
|
||||
}));
|
||||
popperProps = {
|
||||
onCloseAutoFocus: (e) => this.onCloseAutoFocus(e),
|
||||
};
|
||||
}
|
||||
class MenuItemSharedState {
|
||||
opts;
|
||||
content;
|
||||
attachment;
|
||||
#isFocused = $state(false);
|
||||
constructor(opts, content) {
|
||||
this.opts = opts;
|
||||
this.content = content;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
this.onpointermove = this.onpointermove.bind(this);
|
||||
this.onpointerleave = this.onpointerleave.bind(this);
|
||||
this.onfocus = this.onfocus.bind(this);
|
||||
this.onblur = this.onblur.bind(this);
|
||||
}
|
||||
onpointermove(e) {
|
||||
if (e.defaultPrevented)
|
||||
return;
|
||||
if (!isMouseEvent(e))
|
||||
return;
|
||||
if (this.opts.disabled.current) {
|
||||
this.content.onItemLeave(e);
|
||||
}
|
||||
else {
|
||||
const defaultPrevented = this.content.onItemEnter();
|
||||
if (defaultPrevented)
|
||||
return;
|
||||
const item = e.currentTarget;
|
||||
if (!isHTMLElement(item))
|
||||
return;
|
||||
item.focus();
|
||||
}
|
||||
}
|
||||
onpointerleave(e) {
|
||||
if (e.defaultPrevented)
|
||||
return;
|
||||
if (!isMouseEvent(e))
|
||||
return;
|
||||
this.content.onItemLeave(e);
|
||||
}
|
||||
onfocus(e) {
|
||||
afterTick(() => {
|
||||
if (e.defaultPrevented || this.opts.disabled.current)
|
||||
return;
|
||||
this.#isFocused = true;
|
||||
});
|
||||
}
|
||||
onblur(e) {
|
||||
afterTick(() => {
|
||||
if (e.defaultPrevented)
|
||||
return;
|
||||
this.#isFocused = false;
|
||||
});
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
tabindex: -1,
|
||||
role: "menuitem",
|
||||
"aria-disabled": boolToStr(this.opts.disabled.current),
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.opts.disabled.current),
|
||||
"data-highlighted": this.#isFocused ? "" : undefined,
|
||||
[this.content.parentMenu.root.getBitsAttr("item")]: "",
|
||||
//
|
||||
onpointermove: this.onpointermove,
|
||||
onpointerleave: this.onpointerleave,
|
||||
onfocus: this.onfocus,
|
||||
onblur: this.onblur,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class MenuItemState {
|
||||
static create(opts) {
|
||||
const item = new MenuItemSharedState(opts, MenuContentContext.get());
|
||||
return new MenuItemState(opts, item);
|
||||
}
|
||||
opts;
|
||||
item;
|
||||
root;
|
||||
#isPointerDown = false;
|
||||
constructor(opts, item) {
|
||||
this.opts = opts;
|
||||
this.item = item;
|
||||
this.root = item.content.parentMenu.root;
|
||||
this.onkeydown = this.onkeydown.bind(this);
|
||||
this.onclick = this.onclick.bind(this);
|
||||
this.onpointerdown = this.onpointerdown.bind(this);
|
||||
this.onpointerup = this.onpointerup.bind(this);
|
||||
}
|
||||
#handleSelect() {
|
||||
if (this.item.opts.disabled.current)
|
||||
return;
|
||||
const selectEvent = new CustomEvent("menuitemselect", { bubbles: true, cancelable: true });
|
||||
this.opts.onSelect.current(selectEvent);
|
||||
if (selectEvent.defaultPrevented) {
|
||||
this.item.content.parentMenu.root.isUsingKeyboard.current = false;
|
||||
return;
|
||||
}
|
||||
if (this.opts.closeOnSelect.current) {
|
||||
this.item.content.parentMenu.root.opts.onClose();
|
||||
}
|
||||
}
|
||||
onkeydown(e) {
|
||||
const isTypingAhead = this.item.content.search !== "";
|
||||
if (this.item.opts.disabled.current || (isTypingAhead && e.key === kbd.SPACE))
|
||||
return;
|
||||
if (SELECTION_KEYS.includes(e.key)) {
|
||||
if (!isHTMLElement(e.currentTarget))
|
||||
return;
|
||||
e.currentTarget.click();
|
||||
/**
|
||||
* We prevent default browser behavior for selection keys as they should trigger
|
||||
* a selection only:
|
||||
* - prevents space from scrolling the page.
|
||||
* - if keydown causes focus to move, prevents keydown from firing on the new target.
|
||||
*/
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
onclick(_) {
|
||||
if (this.item.opts.disabled.current)
|
||||
return;
|
||||
this.#handleSelect();
|
||||
}
|
||||
onpointerup(e) {
|
||||
if (e.defaultPrevented)
|
||||
return;
|
||||
if (!this.#isPointerDown) {
|
||||
if (!isHTMLElement(e.currentTarget))
|
||||
return;
|
||||
e.currentTarget?.click();
|
||||
}
|
||||
}
|
||||
onpointerdown(_) {
|
||||
this.#isPointerDown = true;
|
||||
}
|
||||
props = $derived.by(() => mergeProps(this.item.props, {
|
||||
onclick: this.onclick,
|
||||
onpointerdown: this.onpointerdown,
|
||||
onpointerup: this.onpointerup,
|
||||
onkeydown: this.onkeydown,
|
||||
}));
|
||||
}
|
||||
export class MenuSubTriggerState {
|
||||
static create(opts) {
|
||||
const content = MenuContentContext.get();
|
||||
const item = new MenuItemSharedState(opts, content);
|
||||
const submenu = MenuMenuContext.get();
|
||||
return new MenuSubTriggerState(opts, item, content, submenu);
|
||||
}
|
||||
opts;
|
||||
item;
|
||||
content;
|
||||
submenu;
|
||||
attachment;
|
||||
#openTimer = null;
|
||||
constructor(opts, item, content, submenu) {
|
||||
this.opts = opts;
|
||||
this.item = item;
|
||||
this.content = content;
|
||||
this.submenu = submenu;
|
||||
this.attachment = attachRef(this.opts.ref, (v) => (this.submenu.triggerNode = v));
|
||||
this.onpointerleave = this.onpointerleave.bind(this);
|
||||
this.onpointermove = this.onpointermove.bind(this);
|
||||
this.onkeydown = this.onkeydown.bind(this);
|
||||
this.onclick = this.onclick.bind(this);
|
||||
onDestroyEffect(() => {
|
||||
this.#clearOpenTimer();
|
||||
});
|
||||
}
|
||||
#clearOpenTimer() {
|
||||
if (this.#openTimer === null)
|
||||
return;
|
||||
this.content.domContext.getWindow().clearTimeout(this.#openTimer);
|
||||
this.#openTimer = null;
|
||||
}
|
||||
onpointermove(e) {
|
||||
if (!isMouseEvent(e))
|
||||
return;
|
||||
if (!this.item.opts.disabled.current &&
|
||||
!this.submenu.opts.open.current &&
|
||||
!this.#openTimer &&
|
||||
!this.content.parentMenu.root.isPointerInTransit) {
|
||||
this.#openTimer = this.content.domContext.setTimeout(() => {
|
||||
this.submenu.onOpen();
|
||||
this.#clearOpenTimer();
|
||||
}, this.opts.openDelay.current);
|
||||
}
|
||||
}
|
||||
onpointerleave(e) {
|
||||
if (!isMouseEvent(e))
|
||||
return;
|
||||
this.#clearOpenTimer();
|
||||
}
|
||||
onkeydown(e) {
|
||||
const isTypingAhead = this.content.search !== "";
|
||||
if (this.item.opts.disabled.current || (isTypingAhead && e.key === kbd.SPACE))
|
||||
return;
|
||||
if (SUB_OPEN_KEYS[this.submenu.root.opts.dir.current].includes(e.key)) {
|
||||
e.currentTarget.click();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
onclick(e) {
|
||||
if (this.item.opts.disabled.current)
|
||||
return;
|
||||
/**
|
||||
* We manually focus because iOS Safari doesn't always focus on click (e.g. buttons)
|
||||
* and we rely heavily on `onFocusOutside` for submenus to close when switching
|
||||
* between separate submenus.
|
||||
*/
|
||||
if (!isHTMLElement(e.currentTarget))
|
||||
return;
|
||||
e.currentTarget.focus();
|
||||
const selectEvent = new CustomEvent("menusubtriggerselect", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
this.opts.onSelect.current(selectEvent);
|
||||
if (!this.submenu.opts.open.current) {
|
||||
this.submenu.onOpen();
|
||||
afterTick(() => {
|
||||
const contentNode = this.submenu.contentNode;
|
||||
if (!contentNode)
|
||||
return;
|
||||
MenuOpenEvent.dispatch(contentNode);
|
||||
});
|
||||
}
|
||||
}
|
||||
props = $derived.by(() => mergeProps({
|
||||
"aria-haspopup": "menu",
|
||||
"aria-expanded": boolToStr(this.submenu.opts.open.current),
|
||||
"data-state": getDataOpenClosed(this.submenu.opts.open.current),
|
||||
"aria-controls": this.submenu.opts.open.current
|
||||
? this.submenu.contentId.current
|
||||
: undefined,
|
||||
[this.submenu.root.getBitsAttr("sub-trigger")]: "",
|
||||
onclick: this.onclick,
|
||||
onpointermove: this.onpointermove,
|
||||
onpointerleave: this.onpointerleave,
|
||||
onkeydown: this.onkeydown,
|
||||
...this.attachment,
|
||||
}, this.item.props));
|
||||
}
|
||||
export class MenuCheckboxItemState {
|
||||
static create(opts, checkboxGroup) {
|
||||
const item = new MenuItemState(opts, new MenuItemSharedState(opts, MenuContentContext.get()));
|
||||
return new MenuCheckboxItemState(opts, item, checkboxGroup);
|
||||
}
|
||||
opts;
|
||||
item;
|
||||
group;
|
||||
constructor(opts, item, group = null) {
|
||||
this.opts = opts;
|
||||
this.item = item;
|
||||
this.group = group;
|
||||
// Watch for value changes in the group if we're part of one
|
||||
if (this.group) {
|
||||
watch(() => this.group.opts.value.current, (groupValues) => {
|
||||
this.opts.checked.current = groupValues.includes(this.opts.value.current);
|
||||
});
|
||||
// Watch for checked state changes and sync with group
|
||||
watch(() => this.opts.checked.current, (checked) => {
|
||||
if (checked) {
|
||||
this.group.addValue(this.opts.value.current);
|
||||
}
|
||||
else {
|
||||
this.group.removeValue(this.opts.value.current);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
toggleChecked() {
|
||||
if (this.opts.indeterminate.current) {
|
||||
this.opts.indeterminate.current = false;
|
||||
this.opts.checked.current = true;
|
||||
}
|
||||
else {
|
||||
this.opts.checked.current = !this.opts.checked.current;
|
||||
}
|
||||
}
|
||||
snippetProps = $derived.by(() => ({
|
||||
checked: this.opts.checked.current,
|
||||
indeterminate: this.opts.indeterminate.current,
|
||||
}));
|
||||
props = $derived.by(() => ({
|
||||
...this.item.props,
|
||||
role: "menuitemcheckbox",
|
||||
"aria-checked": getAriaChecked(this.opts.checked.current, this.opts.indeterminate.current),
|
||||
"data-state": getCheckedState(this.opts.checked.current),
|
||||
[this.item.root.getBitsAttr("checkbox-item")]: "",
|
||||
}));
|
||||
}
|
||||
export class MenuGroupState {
|
||||
static create(opts) {
|
||||
return MenuGroupContext.set(new MenuGroupState(opts, MenuRootContext.get()));
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
groupHeadingId = $state(undefined);
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
role: "group",
|
||||
"aria-labelledby": this.groupHeadingId,
|
||||
[this.root.getBitsAttr("group")]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class MenuGroupHeadingState {
|
||||
static create(opts) {
|
||||
// Try to get checkbox group first, then radio group, then regular group
|
||||
const checkboxGroup = MenuCheckboxGroupContext.getOr(null);
|
||||
if (checkboxGroup)
|
||||
return new MenuGroupHeadingState(opts, checkboxGroup);
|
||||
const radioGroup = MenuRadioGroupContext.getOr(null);
|
||||
if (radioGroup)
|
||||
return new MenuGroupHeadingState(opts, radioGroup);
|
||||
return new MenuGroupHeadingState(opts, MenuGroupContext.get());
|
||||
}
|
||||
opts;
|
||||
group;
|
||||
attachment;
|
||||
constructor(opts, group) {
|
||||
this.opts = opts;
|
||||
this.group = group;
|
||||
this.attachment = attachRef(this.opts.ref, (v) => (this.group.groupHeadingId = v?.id));
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
role: "group",
|
||||
[this.group.root.getBitsAttr("group-heading")]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class MenuSeparatorState {
|
||||
static create(opts) {
|
||||
return new MenuSeparatorState(opts, MenuRootContext.get());
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
attachment;
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
role: "group",
|
||||
[this.root.getBitsAttr("separator")]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class MenuArrowState {
|
||||
static create() {
|
||||
return new MenuArrowState(MenuRootContext.get());
|
||||
}
|
||||
root;
|
||||
constructor(root) {
|
||||
this.root = root;
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
[this.root.getBitsAttr("arrow")]: "",
|
||||
}));
|
||||
}
|
||||
export class MenuRadioGroupState {
|
||||
static create(opts) {
|
||||
return MenuGroupContext.set(MenuRadioGroupContext.set(new MenuRadioGroupState(opts, MenuContentContext.get())));
|
||||
}
|
||||
opts;
|
||||
content;
|
||||
attachment;
|
||||
groupHeadingId = $state(null);
|
||||
root;
|
||||
constructor(opts, content) {
|
||||
this.opts = opts;
|
||||
this.content = content;
|
||||
this.root = content.parentMenu.root;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
setValue(v) {
|
||||
this.opts.value.current = v;
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
[this.root.getBitsAttr("radio-group")]: "",
|
||||
role: "group",
|
||||
"aria-labelledby": this.groupHeadingId,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class MenuRadioItemState {
|
||||
static create(opts) {
|
||||
const radioGroup = MenuRadioGroupContext.get();
|
||||
const sharedItem = new MenuItemSharedState(opts, radioGroup.content);
|
||||
const item = new MenuItemState(opts, sharedItem);
|
||||
return new MenuRadioItemState(opts, item, radioGroup);
|
||||
}
|
||||
opts;
|
||||
item;
|
||||
group;
|
||||
attachment;
|
||||
isChecked = $derived.by(() => this.group.opts.value.current === this.opts.value.current);
|
||||
constructor(opts, item, group) {
|
||||
this.opts = opts;
|
||||
this.item = item;
|
||||
this.group = group;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
selectValue() {
|
||||
this.group.setValue(this.opts.value.current);
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
[this.group.root.getBitsAttr("radio-item")]: "",
|
||||
...this.item.props,
|
||||
role: "menuitemradio",
|
||||
"aria-checked": getAriaChecked(this.isChecked, false),
|
||||
"data-state": getCheckedState(this.isChecked),
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class DropdownMenuTriggerState {
|
||||
static create(opts) {
|
||||
return new DropdownMenuTriggerState(opts, MenuMenuContext.get());
|
||||
}
|
||||
opts;
|
||||
parentMenu;
|
||||
attachment;
|
||||
constructor(opts, parentMenu) {
|
||||
this.opts = opts;
|
||||
this.parentMenu = parentMenu;
|
||||
this.attachment = attachRef(this.opts.ref, (v) => (this.parentMenu.triggerNode = v));
|
||||
}
|
||||
onclick = (e) => {
|
||||
/**
|
||||
* MacOS VoiceOver sends a click in Safari/Firefox bypassing the keydown event
|
||||
* when V0+Space is pressed. Since we already handle the keydown event and the
|
||||
* pointerdown events separately, we ignore it if the detail is not 0.
|
||||
*/
|
||||
if (this.opts.disabled.current || e.detail !== 0)
|
||||
return;
|
||||
this.parentMenu.toggleOpen();
|
||||
e.preventDefault();
|
||||
};
|
||||
onpointerdown = (e) => {
|
||||
if (this.opts.disabled.current)
|
||||
return;
|
||||
if (e.pointerType === "touch")
|
||||
return e.preventDefault();
|
||||
if (e.button === 0 && e.ctrlKey === false) {
|
||||
this.parentMenu.toggleOpen();
|
||||
// prevent trigger focusing when opening to allow
|
||||
// the content to be given focus without competition
|
||||
if (!this.parentMenu.opts.open.current)
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
onpointerup = (e) => {
|
||||
if (this.opts.disabled.current)
|
||||
return;
|
||||
if (e.pointerType === "touch") {
|
||||
e.preventDefault();
|
||||
this.parentMenu.toggleOpen();
|
||||
}
|
||||
};
|
||||
onkeydown = (e) => {
|
||||
if (this.opts.disabled.current)
|
||||
return;
|
||||
if (e.key === kbd.SPACE || e.key === kbd.ENTER) {
|
||||
this.parentMenu.toggleOpen();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (e.key === kbd.ARROW_DOWN) {
|
||||
this.parentMenu.onOpen();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
#ariaControls = $derived.by(() => {
|
||||
if (this.parentMenu.opts.open.current && this.parentMenu.contentId.current)
|
||||
return this.parentMenu.contentId.current;
|
||||
return undefined;
|
||||
});
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
disabled: this.opts.disabled.current,
|
||||
"aria-haspopup": "menu",
|
||||
"aria-expanded": boolToStr(this.parentMenu.opts.open.current),
|
||||
"aria-controls": this.#ariaControls,
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.opts.disabled.current),
|
||||
"data-state": getDataOpenClosed(this.parentMenu.opts.open.current),
|
||||
[this.parentMenu.root.getBitsAttr("trigger")]: "",
|
||||
//
|
||||
onclick: this.onclick,
|
||||
onpointerdown: this.onpointerdown,
|
||||
onpointerup: this.onpointerup,
|
||||
onkeydown: this.onkeydown,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class ContextMenuTriggerState {
|
||||
static create(opts) {
|
||||
return new ContextMenuTriggerState(opts, MenuMenuContext.get());
|
||||
}
|
||||
opts;
|
||||
parentMenu;
|
||||
attachment;
|
||||
#point = $state({ x: 0, y: 0 });
|
||||
virtualElement = simpleBox({
|
||||
getBoundingClientRect: () => DOMRect.fromRect({ width: 0, height: 0, ...this.#point }),
|
||||
});
|
||||
#longPressTimer = null;
|
||||
constructor(opts, parentMenu) {
|
||||
this.opts = opts;
|
||||
this.parentMenu = parentMenu;
|
||||
this.attachment = attachRef(this.opts.ref, (v) => (this.parentMenu.triggerNode = v));
|
||||
this.oncontextmenu = this.oncontextmenu.bind(this);
|
||||
this.onpointerdown = this.onpointerdown.bind(this);
|
||||
this.onpointermove = this.onpointermove.bind(this);
|
||||
this.onpointercancel = this.onpointercancel.bind(this);
|
||||
this.onpointerup = this.onpointerup.bind(this);
|
||||
watch(() => this.#point, (point) => {
|
||||
this.virtualElement.current = {
|
||||
getBoundingClientRect: () => DOMRect.fromRect({ width: 0, height: 0, ...point }),
|
||||
};
|
||||
});
|
||||
watch(() => this.opts.disabled.current, (isDisabled) => {
|
||||
if (isDisabled) {
|
||||
this.#clearLongPressTimer();
|
||||
}
|
||||
});
|
||||
onDestroyEffect(() => this.#clearLongPressTimer());
|
||||
}
|
||||
#clearLongPressTimer() {
|
||||
if (this.#longPressTimer === null)
|
||||
return;
|
||||
getWindow(this.opts.ref.current).clearTimeout(this.#longPressTimer);
|
||||
}
|
||||
#handleOpen(e) {
|
||||
this.#point = { x: e.clientX, y: e.clientY };
|
||||
this.parentMenu.onOpen();
|
||||
}
|
||||
oncontextmenu(e) {
|
||||
if (e.defaultPrevented || this.opts.disabled.current)
|
||||
return;
|
||||
this.#clearLongPressTimer();
|
||||
this.#handleOpen(e);
|
||||
e.preventDefault();
|
||||
this.parentMenu.contentNode?.focus();
|
||||
}
|
||||
onpointerdown(e) {
|
||||
if (this.opts.disabled.current || isMouseEvent(e))
|
||||
return;
|
||||
this.#clearLongPressTimer();
|
||||
this.#longPressTimer = getWindow(this.opts.ref.current).setTimeout(() => this.#handleOpen(e), 700);
|
||||
}
|
||||
onpointermove(e) {
|
||||
if (this.opts.disabled.current || isMouseEvent(e))
|
||||
return;
|
||||
this.#clearLongPressTimer();
|
||||
}
|
||||
onpointercancel(e) {
|
||||
if (this.opts.disabled.current || isMouseEvent(e))
|
||||
return;
|
||||
this.#clearLongPressTimer();
|
||||
}
|
||||
onpointerup(e) {
|
||||
if (this.opts.disabled.current || isMouseEvent(e))
|
||||
return;
|
||||
this.#clearLongPressTimer();
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
disabled: this.opts.disabled.current,
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.opts.disabled.current),
|
||||
"data-state": getDataOpenClosed(this.parentMenu.opts.open.current),
|
||||
[CONTEXT_MENU_TRIGGER_ATTR]: "",
|
||||
tabindex: -1,
|
||||
//
|
||||
onpointerdown: this.onpointerdown,
|
||||
onpointermove: this.onpointermove,
|
||||
onpointercancel: this.onpointercancel,
|
||||
onpointerup: this.onpointerup,
|
||||
oncontextmenu: this.oncontextmenu,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class MenuCheckboxGroupState {
|
||||
static create(opts) {
|
||||
return MenuCheckboxGroupContext.set(new MenuCheckboxGroupState(opts, MenuContentContext.get()));
|
||||
}
|
||||
opts;
|
||||
content;
|
||||
root;
|
||||
attachment;
|
||||
groupHeadingId = $state(null);
|
||||
constructor(opts, content) {
|
||||
this.opts = opts;
|
||||
this.content = content;
|
||||
this.root = content.parentMenu.root;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
addValue(checkboxValue) {
|
||||
if (!checkboxValue)
|
||||
return;
|
||||
if (!this.opts.value.current.includes(checkboxValue)) {
|
||||
const newValue = [...$state.snapshot(this.opts.value.current), checkboxValue];
|
||||
this.opts.value.current = newValue;
|
||||
if (arraysAreEqual(this.opts.value.current, newValue))
|
||||
return;
|
||||
this.opts.onValueChange.current(newValue);
|
||||
}
|
||||
}
|
||||
removeValue(checkboxValue) {
|
||||
if (!checkboxValue)
|
||||
return;
|
||||
const index = this.opts.value.current.indexOf(checkboxValue);
|
||||
if (index === -1)
|
||||
return;
|
||||
const newValue = this.opts.value.current.filter((v) => v !== checkboxValue);
|
||||
this.opts.value.current = newValue;
|
||||
if (arraysAreEqual(this.opts.value.current, newValue))
|
||||
return;
|
||||
this.opts.onValueChange.current(newValue);
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
[this.root.getBitsAttr("checkbox-group")]: "",
|
||||
role: "group",
|
||||
"aria-labelledby": this.groupHeadingId,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class MenuSubmenuState {
|
||||
static create(opts) {
|
||||
const menu = MenuMenuContext.get();
|
||||
return MenuMenuContext.set(new MenuMenuState(opts, menu.root, menu));
|
||||
}
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
import type { Expand } from "svelte-toolbelt";
|
||||
import type { PopperLayerProps, PopperLayerStaticProps } from "../utilities/popper-layer/types.js";
|
||||
import type { ArrowProps, ArrowPropsWithoutHTML } from "../utilities/arrow/types.js";
|
||||
import type { OnChangeFn, WithChild, WithChildNoChildrenSnippetProps, WithChildren, Without } from "../../internal/types.js";
|
||||
import type { BitsPrimitiveButtonAttributes, BitsPrimitiveDivAttributes } from "../../shared/attributes.js";
|
||||
import type { Direction } from "../../shared/index.js";
|
||||
import type { PortalProps } from "../utilities/portal/types.js";
|
||||
import type { FloatingContentSnippetProps, StaticContentSnippetProps } from "../../shared/types.js";
|
||||
export type MenuRootPropsWithoutHTML = WithChildren<{
|
||||
/**
|
||||
* The open state of the menu.
|
||||
*/
|
||||
open?: boolean;
|
||||
/**
|
||||
* A callback that is called when the menu is opened or closed.
|
||||
*/
|
||||
onOpenChange?: OnChangeFn<boolean>;
|
||||
/**
|
||||
* A callback that is called when the menu is opened or closed.
|
||||
*/
|
||||
onOpenChangeComplete?: OnChangeFn<boolean>;
|
||||
/**
|
||||
* The direction of the site.
|
||||
*
|
||||
* @defaultValue "ltr"
|
||||
*/
|
||||
dir?: Direction;
|
||||
}>;
|
||||
export type MenuRootProps = MenuRootPropsWithoutHTML;
|
||||
export type _SharedMenuContentProps = {
|
||||
/**
|
||||
* When `true`, the menu will loop through items when navigating with the keyboard.
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
loop?: boolean;
|
||||
};
|
||||
export type MenuContentPropsWithoutHTML = Expand<WithChildNoChildrenSnippetProps<Omit<PopperLayerProps, "content"> & _SharedMenuContentProps, FloatingContentSnippetProps>>;
|
||||
export type MenuContentProps = MenuContentPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, MenuContentPropsWithoutHTML>;
|
||||
export type MenuContentStaticPropsWithoutHTML = Expand<WithChildNoChildrenSnippetProps<Omit<PopperLayerStaticProps, "content"> & _SharedMenuContentProps, StaticContentSnippetProps>>;
|
||||
export type MenuContentStaticProps = MenuContentStaticPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, MenuContentStaticPropsWithoutHTML>;
|
||||
export type MenuItemPropsWithoutHTML<U extends Record<PropertyKey, unknown> = {
|
||||
_default: never;
|
||||
}> = WithChild<{
|
||||
/**
|
||||
* When `true`, the user will not be able to interact with the menu item.
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Optional text to use for typeahead filtering. By default, typeahead will use
|
||||
* the `.textContent` of the menu item. When the content is more complex, you
|
||||
* can provide a string here instead.
|
||||
*
|
||||
* @defaultValue undefined
|
||||
*/
|
||||
textValue?: string;
|
||||
/**
|
||||
* A callback fired when the menu item is selected.
|
||||
*
|
||||
* Prevent default behavior of selection with `event.preventDefault()`.
|
||||
*/
|
||||
onSelect?: (event: Event) => void;
|
||||
/**
|
||||
* Whether or not the menu item should close when selected.
|
||||
* @defaultValue true
|
||||
*/
|
||||
closeOnSelect?: boolean;
|
||||
}, U>;
|
||||
export type MenuItemProps = MenuItemPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, MenuItemPropsWithoutHTML>;
|
||||
export type MenuCheckboxItemSnippetProps = {
|
||||
checked: boolean;
|
||||
indeterminate: boolean;
|
||||
};
|
||||
export type MenuCheckboxItemPropsWithoutHTML = MenuItemPropsWithoutHTML<MenuCheckboxItemSnippetProps> & {
|
||||
/**
|
||||
* The checked state of the checkbox. It can be one of:
|
||||
* - `true` for checked
|
||||
* - `false` for unchecked
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
checked?: boolean;
|
||||
/**
|
||||
* A callback that is fired when the checked state changes.
|
||||
*/
|
||||
onCheckedChange?: OnChangeFn<boolean>;
|
||||
/**
|
||||
* Whether the checkbox is in an indeterminate state or not.
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
indeterminate?: boolean;
|
||||
/**
|
||||
* A callback function called when the indeterminate state changes.
|
||||
*/
|
||||
onIndeterminateChange?: OnChangeFn<boolean>;
|
||||
/**
|
||||
* Whether or not the menu item should close when selected.
|
||||
*
|
||||
* @defaultValue true
|
||||
*/
|
||||
closeOnSelect?: boolean;
|
||||
/**
|
||||
* The value of the checkbox item when used in a checkbox group.
|
||||
*/
|
||||
value?: string;
|
||||
};
|
||||
export type MenuCheckboxItemProps = MenuCheckboxItemPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, MenuCheckboxItemPropsWithoutHTML>;
|
||||
export type MenuCheckboxGroupPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* The values of the selected checkbox items.
|
||||
*
|
||||
* Supports two-way binding with `bind:value`.
|
||||
*/
|
||||
value?: string[];
|
||||
/**
|
||||
* A callback that is fired when the selected checkbox items change.
|
||||
*/
|
||||
onValueChange?: OnChangeFn<string[]>;
|
||||
}>;
|
||||
export type MenuCheckboxGroupProps = MenuCheckboxGroupPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, MenuCheckboxGroupPropsWithoutHTML>;
|
||||
export type MenuTriggerPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* Whether the trigger is disabled.
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
disabled?: boolean | null | undefined;
|
||||
}>;
|
||||
export type MenuTriggerProps = MenuTriggerPropsWithoutHTML & Without<BitsPrimitiveButtonAttributes, MenuTriggerPropsWithoutHTML>;
|
||||
export type MenuSubPropsWithoutHTML = WithChildren<{
|
||||
/**
|
||||
* The open state of the menu.
|
||||
*/
|
||||
open?: boolean;
|
||||
/**
|
||||
* A callback that is called when the menu is opened or closed.
|
||||
*/
|
||||
onOpenChange?: OnChangeFn<boolean>;
|
||||
/**
|
||||
* A callback that is called when the menu finishes opening/closing animations.
|
||||
*/
|
||||
onOpenChangeComplete?: OnChangeFn<boolean>;
|
||||
}>;
|
||||
export type MenuSubProps = MenuSubPropsWithoutHTML;
|
||||
export type MenuSubContentPropsWithoutHTML = Expand<WithChildNoChildrenSnippetProps<Omit<PopperLayerProps, "content" | "preventScroll"> & _SharedMenuContentProps, FloatingContentSnippetProps>>;
|
||||
export type MenuSubContentProps = MenuSubContentPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, MenuSubContentPropsWithoutHTML>;
|
||||
export type MenuSubContentStaticPropsWithoutHTML = Expand<WithChildNoChildrenSnippetProps<Omit<PopperLayerStaticProps, "content" | "preventScroll"> & _SharedMenuContentProps, StaticContentSnippetProps>>;
|
||||
export type MenuSubContentStaticProps = MenuSubContentStaticPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, MenuSubContentStaticPropsWithoutHTML>;
|
||||
export type MenuSubTriggerPropsWithoutHTML = Omit<MenuItemPropsWithoutHTML, "closeOnSelect"> & {
|
||||
/**
|
||||
* The amount of time in ms from when the mouse enters the subtrigger until
|
||||
* the submenu opens. This is useful for preventing the submenu from opening
|
||||
* as a user is moving their mouse through the menu without a true intention to open that
|
||||
* submenu.
|
||||
*
|
||||
* To disable the behavior, set it to `0`.
|
||||
*
|
||||
* @default 100
|
||||
*/
|
||||
openDelay?: number;
|
||||
};
|
||||
export type MenuSubTriggerProps = MenuSubTriggerPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, MenuSubTriggerPropsWithoutHTML>;
|
||||
export type MenuSeparatorPropsWithoutHTML = WithChild;
|
||||
export type MenuSeparatorProps = MenuSeparatorPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, MenuSeparatorPropsWithoutHTML>;
|
||||
export type MenuArrowPropsWithoutHTML = ArrowPropsWithoutHTML;
|
||||
export type MenuArrowProps = ArrowProps;
|
||||
export type MenuGroupPropsWithoutHTML = WithChild;
|
||||
export type MenuGroupProps = MenuGroupPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, MenuGroupPropsWithoutHTML>;
|
||||
export type MenuGroupHeadingPropsWithoutHTML = WithChild;
|
||||
export type MenuGroupHeadingProps = MenuGroupHeadingPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, MenuGroupHeadingPropsWithoutHTML>;
|
||||
export type MenuRadioGroupPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* The value of the selected radio item.
|
||||
*
|
||||
* Supports two-way binding with `bind:value`.
|
||||
*/
|
||||
value?: string;
|
||||
/**
|
||||
* A callback that is fired when the selected radio item changes.
|
||||
*/
|
||||
onValueChange?: OnChangeFn<string>;
|
||||
}>;
|
||||
export type MenuRadioGroupProps = MenuRadioGroupPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, MenuRadioGroupPropsWithoutHTML>;
|
||||
export type MenuRadioItemSnippetProps = {
|
||||
checked: boolean;
|
||||
};
|
||||
export type MenuRadioItemPropsWithoutHTML = MenuItemPropsWithoutHTML<MenuRadioItemSnippetProps> & {
|
||||
/**
|
||||
* The value of the radio item.
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* Whether or not the menu item should close when selected.
|
||||
* @defaultValue true
|
||||
*/
|
||||
closeOnSelect?: boolean;
|
||||
};
|
||||
export type MenuRadioItemProps = MenuRadioItemPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, MenuRadioItemPropsWithoutHTML>;
|
||||
export type MenuPortalPropsWithoutHTML = PortalProps;
|
||||
export type MenuPortalProps = MenuPortalPropsWithoutHTML;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import type { Direction } from "../../shared/index.js";
|
||||
export type CheckedState = boolean | "indeterminate";
|
||||
export declare const SELECTION_KEYS: string[];
|
||||
export declare const FIRST_KEYS: string[];
|
||||
export declare const LAST_KEYS: string[];
|
||||
export declare const FIRST_LAST_KEYS: string[];
|
||||
export declare const SUB_OPEN_KEYS: Record<Direction, string[]>;
|
||||
export declare const SUB_CLOSE_KEYS: Record<Direction, string[]>;
|
||||
export declare function isIndeterminate(checked?: CheckedState): checked is "indeterminate";
|
||||
export declare function getCheckedState(checked: CheckedState): "checked" | "unchecked" | "indeterminate";
|
||||
export declare function isMouseEvent(event: PointerEvent): boolean;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { kbd } from "../../internal/kbd.js";
|
||||
export const SELECTION_KEYS = [kbd.ENTER, kbd.SPACE];
|
||||
export const FIRST_KEYS = [kbd.ARROW_DOWN, kbd.PAGE_UP, kbd.HOME];
|
||||
export const LAST_KEYS = [kbd.ARROW_UP, kbd.PAGE_DOWN, kbd.END];
|
||||
export const FIRST_LAST_KEYS = [...FIRST_KEYS, ...LAST_KEYS];
|
||||
export const SUB_OPEN_KEYS = {
|
||||
ltr: [...SELECTION_KEYS, kbd.ARROW_RIGHT],
|
||||
rtl: [...SELECTION_KEYS, kbd.ARROW_LEFT],
|
||||
};
|
||||
export const SUB_CLOSE_KEYS = {
|
||||
ltr: [kbd.ARROW_LEFT],
|
||||
rtl: [kbd.ARROW_RIGHT],
|
||||
};
|
||||
export function isIndeterminate(checked) {
|
||||
return checked === "indeterminate";
|
||||
}
|
||||
export function getCheckedState(checked) {
|
||||
return isIndeterminate(checked) ? "indeterminate" : checked ? "checked" : "unchecked";
|
||||
}
|
||||
export function isMouseEvent(event) {
|
||||
return event.pointerType === "mouse";
|
||||
}
|
||||
Reference in New Issue
Block a user