INIT
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenubarContentStaticProps } from "../types.js";
|
||||
import { MenubarContentState } from "../menubar.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import MenuContentStatic from "../../menu/components/menu-content-static.svelte";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
interactOutsideBehavior = "close",
|
||||
id = createId(uid),
|
||||
onInteractOutside = noop,
|
||||
onCloseAutoFocus = noop,
|
||||
onFocusOutside = noop,
|
||||
onOpenAutoFocus = noop,
|
||||
...restProps
|
||||
}: MenubarContentStaticProps = $props();
|
||||
|
||||
const contentState = MenubarContentState.create({
|
||||
id: boxWith(() => id),
|
||||
interactOutsideBehavior: boxWith(() => interactOutsideBehavior),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
onInteractOutside: boxWith(() => onInteractOutside),
|
||||
onFocusOutside: boxWith(() => onFocusOutside),
|
||||
onCloseAutoFocus: boxWith(() => onCloseAutoFocus),
|
||||
onOpenAutoFocus: boxWith(() => onOpenAutoFocus),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, contentState.props));
|
||||
</script>
|
||||
|
||||
<MenuContentStatic bind:ref {...mergedProps} {...contentState.popperProps} preventScroll={false} />
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { MenubarContentStaticProps } from "../types.js";
|
||||
declare const MenubarContentStatic: import("svelte").Component<MenubarContentStaticProps, {}, "ref">;
|
||||
type MenubarContentStatic = ReturnType<typeof MenubarContentStatic>;
|
||||
export default MenubarContentStatic;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenubarContentProps } from "../types.js";
|
||||
import { MenubarContentState } from "../menubar.svelte.js";
|
||||
import MenuContent from "../../menu/components/menu-content.svelte";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
interactOutsideBehavior = "close",
|
||||
id = createId(uid),
|
||||
onInteractOutside = noop,
|
||||
onFocusOutside = noop,
|
||||
onCloseAutoFocus = noop,
|
||||
onOpenAutoFocus = noop,
|
||||
...restProps
|
||||
}: MenubarContentProps = $props();
|
||||
|
||||
const contentState = MenubarContentState.create({
|
||||
id: boxWith(() => id),
|
||||
interactOutsideBehavior: boxWith(() => interactOutsideBehavior),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
onInteractOutside: boxWith(() => onInteractOutside),
|
||||
onFocusOutside: boxWith(() => onFocusOutside),
|
||||
onCloseAutoFocus: boxWith(() => onCloseAutoFocus),
|
||||
onOpenAutoFocus: boxWith(() => onOpenAutoFocus),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, contentState.props));
|
||||
</script>
|
||||
|
||||
<MenuContent bind:ref {...mergedProps} {...contentState.popperProps} preventScroll={false} />
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { MenubarContentProps } from "../types.js";
|
||||
declare const MenubarContent: import("svelte").Component<MenubarContentProps, {}, "ref">;
|
||||
type MenubarContent = ReturnType<typeof MenubarContent>;
|
||||
export default MenubarContent;
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { boxWith } from "svelte-toolbelt";
|
||||
import type { MenubarMenuProps } from "../types.js";
|
||||
import { MenubarMenuState } from "../menubar.svelte.js";
|
||||
import Menu from "../../menu/components/menu.svelte";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let { value = createId(uid), onOpenChange = noop, ...restProps }: MenubarMenuProps = $props();
|
||||
|
||||
const menuState = MenubarMenuState.create({
|
||||
value: boxWith(() => value),
|
||||
onOpenChange: boxWith(() => onOpenChange),
|
||||
});
|
||||
</script>
|
||||
|
||||
<Menu
|
||||
open={menuState.open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) menuState.root.onMenuClose();
|
||||
}}
|
||||
dir={menuState.root.opts.dir.current}
|
||||
_internal_variant="menubar"
|
||||
{...restProps}
|
||||
/>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare const MenubarMenu: import("svelte").Component<import("../types.js").MenubarMenuPropsWithoutHTML, {}, "">;
|
||||
type MenubarMenu = ReturnType<typeof MenubarMenu>;
|
||||
export default MenubarMenu;
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { attachRef, boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenubarTriggerProps } from "../types.js";
|
||||
import { MenubarTriggerState } from "../menubar.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import FloatingLayerAnchor from "../../utilities/floating-layer/components/floating-layer-anchor.svelte";
|
||||
import { DropdownMenuTriggerState } from "../../menu/menu.svelte.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
disabled = false,
|
||||
children,
|
||||
child,
|
||||
ref = $bindable(null),
|
||||
...restProps
|
||||
}: MenubarTriggerProps = $props();
|
||||
|
||||
const triggerState = MenubarTriggerState.create({
|
||||
id: boxWith(() => id),
|
||||
disabled: boxWith(() => disabled ?? false),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const dropdownTriggerState = DropdownMenuTriggerState.create(triggerState.opts);
|
||||
const triggerAttachment = attachRef(
|
||||
(v: HTMLElement | null) => (dropdownTriggerState.parentMenu.triggerNode = v)
|
||||
);
|
||||
|
||||
const mergedProps = $derived(
|
||||
mergeProps(restProps, triggerState.props, {
|
||||
...triggerAttachment,
|
||||
})
|
||||
);
|
||||
</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 { MenubarTriggerProps } from "../types.js";
|
||||
declare const MenubarTrigger: import("svelte").Component<MenubarTriggerProps, {}, "ref">;
|
||||
type MenubarTrigger = ReturnType<typeof MenubarTrigger>;
|
||||
export default MenubarTrigger;
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { MenubarRootProps } from "../types.js";
|
||||
import { MenubarRootState } from "../menubar.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
children,
|
||||
child,
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
dir = "ltr",
|
||||
loop = true,
|
||||
onValueChange = noop,
|
||||
...restProps
|
||||
}: MenubarRootProps = $props();
|
||||
|
||||
const rootState = MenubarRootState.create({
|
||||
id: boxWith(() => id),
|
||||
value: boxWith(
|
||||
() => value,
|
||||
(v) => {
|
||||
value = v;
|
||||
onValueChange?.(v);
|
||||
}
|
||||
),
|
||||
dir: boxWith(() => dir),
|
||||
loop: boxWith(() => loop),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, rootState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { MenubarRootProps } from "../types.js";
|
||||
declare const Menubar: import("svelte").Component<MenubarRootProps, {}, "value" | "ref">;
|
||||
type Menubar = ReturnType<typeof Menubar>;
|
||||
export default Menubar;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
export { default as Root } from "./components/menubar.svelte";
|
||||
export { default as Menu } from "./components/menubar-menu.svelte";
|
||||
export { default as Content } from "./components/menubar-content.svelte";
|
||||
export { default as ContentStatic } from "./components/menubar-content-static.svelte";
|
||||
export { default as Trigger } from "./components/menubar-trigger.svelte";
|
||||
export { default as Sub } from "../menu/components/menu-sub.svelte";
|
||||
export { default as Item } from "../menu/components/menu-item.svelte";
|
||||
export { default as Group } from "../menu/components/menu-group.svelte";
|
||||
export { default as GroupHeading } from "../menu/components/menu-group-heading.svelte";
|
||||
export { default as Arrow } from "../menu/components/menu-arrow.svelte";
|
||||
export { default as RadioItem } from "../menu/components/menu-radio-item.svelte";
|
||||
export { default as Separator } from "../menu/components/menu-separator.svelte";
|
||||
export { default as SubContent } from "../menu/components/menu-sub-content.svelte";
|
||||
export { default as SubContentStatic } from "../menu/components/menu-sub-content-static.svelte";
|
||||
export { default as SubTrigger } from "../menu/components/menu-sub-trigger.svelte";
|
||||
export { default as RadioGroup } from "../menu/components/menu-radio-group.svelte";
|
||||
export { default as CheckboxItem } from "../menu/components/menu-checkbox-item.svelte";
|
||||
export { default as Portal } from "../utilities/portal/portal.svelte";
|
||||
export { default as CheckboxGroup } from "../menu/components/menu-checkbox-group.svelte";
|
||||
export type { MenubarRootProps as RootProps, MenubarMenuProps as MenuProps, MenubarTriggerProps as TriggerProps, MenubarContentProps as ContentProps, MenubarContentStaticProps as ContentStaticProps, MenubarPortalProps as PortalProps, } from "./types.js";
|
||||
export type { MenuSubPropsWithoutHTML as SubProps, MenuItemProps as ItemProps, MenuGroupProps as GroupProps, MenuGroupHeadingProps as GroupHeadingProps, MenuArrowProps as ArrowProps, MenuRadioItemProps as RadioItemProps, MenuSeparatorProps as SeparatorProps, MenuSubContentProps as SubContentProps, MenuSubTriggerProps as SubTriggerProps, MenuRadioGroupProps as RadioGroupProps, MenuCheckboxItemProps as CheckboxItemProps, MenuSubContentStaticProps as SubContentStaticProps, MenuCheckboxGroupProps as CheckboxGroupProps, } from "../menu/types.js";
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
export { default as Root } from "./components/menubar.svelte";
|
||||
export { default as Menu } from "./components/menubar-menu.svelte";
|
||||
export { default as Content } from "./components/menubar-content.svelte";
|
||||
export { default as ContentStatic } from "./components/menubar-content-static.svelte";
|
||||
export { default as Trigger } from "./components/menubar-trigger.svelte";
|
||||
export { default as Sub } from "../menu/components/menu-sub.svelte";
|
||||
export { default as Item } from "../menu/components/menu-item.svelte";
|
||||
export { default as Group } from "../menu/components/menu-group.svelte";
|
||||
export { default as GroupHeading } from "../menu/components/menu-group-heading.svelte";
|
||||
export { default as Arrow } from "../menu/components/menu-arrow.svelte";
|
||||
export { default as RadioItem } from "../menu/components/menu-radio-item.svelte";
|
||||
export { default as Separator } from "../menu/components/menu-separator.svelte";
|
||||
export { default as SubContent } from "../menu/components/menu-sub-content.svelte";
|
||||
export { default as SubContentStatic } from "../menu/components/menu-sub-content-static.svelte";
|
||||
export { default as SubTrigger } from "../menu/components/menu-sub-trigger.svelte";
|
||||
export { default as RadioGroup } from "../menu/components/menu-radio-group.svelte";
|
||||
export { default as CheckboxItem } from "../menu/components/menu-checkbox-item.svelte";
|
||||
export { default as Portal } from "../utilities/portal/portal.svelte";
|
||||
export { default as CheckboxGroup } from "../menu/components/menu-checkbox-group.svelte";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * as Menubar from "./exports.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * as Menubar from "./exports.js";
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import { type ReadableBox, type ReadableBoxedValues, type WritableBoxedValues } from "svelte-toolbelt";
|
||||
import type { InteractOutsideBehaviorType } from "../utilities/dismissible-layer/types.js";
|
||||
import type { Direction } from "../../shared/index.js";
|
||||
import type { OnChangeFn, RefAttachment, WithRefOpts } from "../../internal/types.js";
|
||||
import type { FocusEventHandler, KeyboardEventHandler, PointerEventHandler } from "svelte/elements";
|
||||
import { RovingFocusGroup } from "../../internal/roving-focus-group.js";
|
||||
interface MenubarRootStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
dir: Direction;
|
||||
loop: boolean;
|
||||
}>, WritableBoxedValues<{
|
||||
value: string;
|
||||
}> {
|
||||
}
|
||||
export declare class MenubarRootState {
|
||||
static create(opts: MenubarRootStateOpts): MenubarRootState;
|
||||
readonly opts: MenubarRootStateOpts;
|
||||
readonly rovingFocusGroup: RovingFocusGroup;
|
||||
readonly attachment: RefAttachment;
|
||||
wasOpenedByKeyboard: boolean;
|
||||
triggerIds: string[];
|
||||
valueToChangeHandler: Map<string, ReadableBox<OnChangeFn<boolean>>>;
|
||||
constructor(opts: MenubarRootStateOpts);
|
||||
/**
|
||||
* @param id - the id of the trigger to register
|
||||
* @returns - a function to de-register the trigger
|
||||
*/
|
||||
registerTrigger: (id: string) => () => void;
|
||||
/**
|
||||
* @param value - the value of the menu to register
|
||||
* @param contentId - the content id to associate with the value
|
||||
* @returns - a function to de-register the menu
|
||||
*/
|
||||
registerMenu: (value: string, onOpenChange: ReadableBox<OnChangeFn<boolean>>) => () => void;
|
||||
updateValue: (value: string) => void;
|
||||
getTriggers: () => HTMLButtonElement[];
|
||||
onMenuOpen: (id: string, triggerId: string) => void;
|
||||
onMenuClose: () => void;
|
||||
onMenuToggle: (id: string) => void;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly role: "menubar";
|
||||
};
|
||||
}
|
||||
interface MenubarMenuStateOpts extends ReadableBoxedValues<{
|
||||
value: string;
|
||||
onOpenChange: OnChangeFn<boolean>;
|
||||
}> {
|
||||
}
|
||||
export declare class MenubarMenuState {
|
||||
static create(opts: MenubarMenuStateOpts): MenubarMenuState;
|
||||
readonly opts: MenubarMenuStateOpts;
|
||||
readonly root: MenubarRootState;
|
||||
open: boolean;
|
||||
wasOpenedByKeyboard: boolean;
|
||||
triggerNode: HTMLElement | null;
|
||||
triggerId: string | undefined;
|
||||
contentId: string | undefined;
|
||||
contentNode: HTMLElement | null;
|
||||
constructor(opts: MenubarMenuStateOpts, root: MenubarRootState);
|
||||
getTriggerNode(): HTMLElement | null;
|
||||
toggleMenu(): void;
|
||||
openMenu(): void;
|
||||
}
|
||||
interface MenubarTriggerStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
disabled: boolean;
|
||||
}> {
|
||||
}
|
||||
export declare class MenubarTriggerState {
|
||||
#private;
|
||||
static create(opts: MenubarTriggerStateOpts): MenubarTriggerState;
|
||||
readonly opts: MenubarTriggerStateOpts;
|
||||
readonly menu: MenubarMenuState;
|
||||
readonly root: MenubarRootState;
|
||||
readonly attachment: RefAttachment;
|
||||
isFocused: boolean;
|
||||
constructor(opts: MenubarTriggerStateOpts, menu: MenubarMenuState);
|
||||
onpointerdown: PointerEventHandler<HTMLElement>;
|
||||
onpointerenter: PointerEventHandler<HTMLElement>;
|
||||
onkeydown: KeyboardEventHandler<HTMLElement>;
|
||||
onfocus: FocusEventHandler<HTMLElement>;
|
||||
onblur: FocusEventHandler<HTMLElement>;
|
||||
readonly props: {
|
||||
readonly type: "button";
|
||||
readonly role: "menuitem";
|
||||
readonly id: string;
|
||||
readonly "aria-haspopup": "menu";
|
||||
readonly "aria-expanded": "true" | "false";
|
||||
readonly "aria-controls": string | undefined;
|
||||
readonly "data-highlighted": "" | undefined;
|
||||
readonly "data-state": "open" | "closed";
|
||||
readonly "data-disabled": "" | undefined;
|
||||
readonly "data-menu-value": string;
|
||||
readonly disabled: true | undefined;
|
||||
readonly tabindex: number;
|
||||
readonly onpointerdown: PointerEventHandler<HTMLElement>;
|
||||
readonly onpointerenter: PointerEventHandler<HTMLElement>;
|
||||
readonly onkeydown: KeyboardEventHandler<HTMLElement>;
|
||||
readonly onfocus: FocusEventHandler<HTMLElement>;
|
||||
readonly onblur: FocusEventHandler<HTMLElement>;
|
||||
};
|
||||
}
|
||||
interface MenubarContentStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
interactOutsideBehavior: InteractOutsideBehaviorType;
|
||||
onOpenAutoFocus: (e: Event) => void;
|
||||
onCloseAutoFocus: (e: Event) => void;
|
||||
onFocusOutside: (e: FocusEvent) => void;
|
||||
onInteractOutside: (e: PointerEvent) => void;
|
||||
}> {
|
||||
}
|
||||
export declare class MenubarContentState {
|
||||
static create(opts: MenubarContentStateOpts): MenubarContentState;
|
||||
readonly opts: MenubarContentStateOpts;
|
||||
readonly menu: MenubarMenuState;
|
||||
readonly root: MenubarRootState;
|
||||
readonly attachment: RefAttachment;
|
||||
constructor(opts: MenubarContentStateOpts, menu: MenubarMenuState);
|
||||
onCloseAutoFocus: (e: Event) => void;
|
||||
onFocusOutside: (e: FocusEvent) => void;
|
||||
onInteractOutside: (e: PointerEvent) => void;
|
||||
onOpenAutoFocus: (e: Event) => void;
|
||||
onkeydown: KeyboardEventHandler<HTMLElement>;
|
||||
props: {
|
||||
readonly id: string;
|
||||
readonly "aria-labelledby": string | undefined;
|
||||
readonly style: {
|
||||
[x: string]: string;
|
||||
};
|
||||
readonly onkeydown: KeyboardEventHandler<HTMLElement>;
|
||||
readonly "data-menu-content": "";
|
||||
};
|
||||
popperProps: {
|
||||
onCloseAutoFocus: (e: Event) => void;
|
||||
onFocusOutside: (e: FocusEvent) => void;
|
||||
onInteractOutside: (e: PointerEvent) => void;
|
||||
onOpenAutoFocus: (e: Event) => void;
|
||||
};
|
||||
}
|
||||
export {};
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
import { afterTick, boxWith, attachRef, } from "svelte-toolbelt";
|
||||
import { Context, watch } from "runed";
|
||||
import { createBitsAttrs, boolToStr, boolToEmptyStrOrUndef, getDataOpenClosed, } from "../../internal/attrs.js";
|
||||
import { kbd } from "../../internal/kbd.js";
|
||||
import { wrapArray } from "../../internal/arrays.js";
|
||||
import { onMount } from "svelte";
|
||||
import { getFloatingContentCSSVars } from "../../internal/floating-svelte/floating-utils.svelte.js";
|
||||
import { RovingFocusGroup } from "../../internal/roving-focus-group.js";
|
||||
const menubarAttrs = createBitsAttrs({
|
||||
component: "menubar",
|
||||
parts: ["root", "trigger", "content"],
|
||||
});
|
||||
const MenubarRootContext = new Context("Menubar.Root");
|
||||
const MenubarMenuContext = new Context("Menubar.Menu");
|
||||
export class MenubarRootState {
|
||||
static create(opts) {
|
||||
return MenubarRootContext.set(new MenubarRootState(opts));
|
||||
}
|
||||
opts;
|
||||
rovingFocusGroup;
|
||||
attachment;
|
||||
wasOpenedByKeyboard = $state(false);
|
||||
triggerIds = $state([]);
|
||||
valueToChangeHandler = new Map();
|
||||
constructor(opts) {
|
||||
this.opts = opts;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
this.rovingFocusGroup = new RovingFocusGroup({
|
||||
rootNode: this.opts.ref,
|
||||
candidateAttr: menubarAttrs.trigger,
|
||||
loop: this.opts.loop,
|
||||
orientation: boxWith(() => "horizontal"),
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param id - the id of the trigger to register
|
||||
* @returns - a function to de-register the trigger
|
||||
*/
|
||||
registerTrigger = (id) => {
|
||||
this.triggerIds.push(id);
|
||||
return () => {
|
||||
this.triggerIds = this.triggerIds.filter((triggerId) => triggerId !== id);
|
||||
};
|
||||
};
|
||||
/**
|
||||
* @param value - the value of the menu to register
|
||||
* @param contentId - the content id to associate with the value
|
||||
* @returns - a function to de-register the menu
|
||||
*/
|
||||
registerMenu = (value, onOpenChange) => {
|
||||
this.valueToChangeHandler.set(value, onOpenChange);
|
||||
return () => {
|
||||
this.valueToChangeHandler.delete(value);
|
||||
};
|
||||
};
|
||||
updateValue = (value) => {
|
||||
const currValue = this.opts.value.current;
|
||||
const currHandler = this.valueToChangeHandler.get(currValue)?.current;
|
||||
const nextHandler = this.valueToChangeHandler.get(value)?.current;
|
||||
this.opts.value.current = value;
|
||||
if (currHandler && currValue !== value) {
|
||||
currHandler(false);
|
||||
}
|
||||
if (nextHandler) {
|
||||
nextHandler(true);
|
||||
}
|
||||
};
|
||||
getTriggers = () => {
|
||||
const node = this.opts.ref.current;
|
||||
if (!node)
|
||||
return [];
|
||||
return Array.from(node.querySelectorAll(menubarAttrs.selector("trigger")));
|
||||
};
|
||||
onMenuOpen = (id, triggerId) => {
|
||||
this.updateValue(id);
|
||||
this.rovingFocusGroup.setCurrentTabStopId(triggerId);
|
||||
};
|
||||
onMenuClose = () => {
|
||||
this.updateValue("");
|
||||
};
|
||||
onMenuToggle = (id) => {
|
||||
this.updateValue(this.opts.value.current ? "" : id);
|
||||
};
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
role: "menubar",
|
||||
[menubarAttrs.root]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class MenubarMenuState {
|
||||
static create(opts) {
|
||||
return MenubarMenuContext.set(new MenubarMenuState(opts, MenubarRootContext.get()));
|
||||
}
|
||||
opts;
|
||||
root;
|
||||
open = $derived.by(() => this.root.opts.value.current === this.opts.value.current);
|
||||
wasOpenedByKeyboard = false;
|
||||
triggerNode = $state(null);
|
||||
triggerId = $derived.by(() => this.triggerNode?.id);
|
||||
contentId = $derived.by(() => this.contentNode?.id);
|
||||
contentNode = $state(null);
|
||||
constructor(opts, root) {
|
||||
this.opts = opts;
|
||||
this.root = root;
|
||||
watch(() => this.open, () => {
|
||||
if (!this.open) {
|
||||
this.wasOpenedByKeyboard = false;
|
||||
}
|
||||
});
|
||||
onMount(() => {
|
||||
return this.root.registerMenu(this.opts.value.current, opts.onOpenChange);
|
||||
});
|
||||
}
|
||||
getTriggerNode() {
|
||||
return this.triggerNode;
|
||||
}
|
||||
toggleMenu() {
|
||||
this.root.onMenuToggle(this.opts.value.current);
|
||||
}
|
||||
openMenu() {
|
||||
this.root.onMenuOpen(this.opts.value.current, this.triggerNode?.id ?? "");
|
||||
}
|
||||
}
|
||||
export class MenubarTriggerState {
|
||||
static create(opts) {
|
||||
return new MenubarTriggerState(opts, MenubarMenuContext.get());
|
||||
}
|
||||
opts;
|
||||
menu;
|
||||
root;
|
||||
attachment;
|
||||
isFocused = $state(false);
|
||||
#tabIndex = $state(0);
|
||||
constructor(opts, menu) {
|
||||
this.opts = opts;
|
||||
this.menu = menu;
|
||||
this.root = menu.root;
|
||||
this.attachment = attachRef(this.opts.ref, (v) => (this.menu.triggerNode = v));
|
||||
onMount(() => {
|
||||
return this.root.registerTrigger(opts.id.current);
|
||||
});
|
||||
$effect(() => {
|
||||
if (this.root.triggerIds.length) {
|
||||
this.#tabIndex = this.root.rovingFocusGroup.getTabIndex(this.menu.getTriggerNode());
|
||||
}
|
||||
});
|
||||
}
|
||||
onpointerdown = (e) => {
|
||||
// only call if the left button but not when the CTRL key is pressed
|
||||
if (!this.opts.disabled.current && e.button === 0 && e.ctrlKey === false) {
|
||||
// prevent trigger from focusing when opening
|
||||
// which allows the content to focus without competition
|
||||
if (!this.menu.open) {
|
||||
e.preventDefault();
|
||||
}
|
||||
this.menu.toggleMenu();
|
||||
}
|
||||
};
|
||||
onpointerenter = () => {
|
||||
const isMenubarOpen = Boolean(this.root.opts.value.current);
|
||||
if (isMenubarOpen && !this.menu.open) {
|
||||
this.menu.openMenu();
|
||||
this.menu.getTriggerNode()?.focus();
|
||||
}
|
||||
};
|
||||
onkeydown = (e) => {
|
||||
if (this.opts.disabled.current)
|
||||
return;
|
||||
if (e.key === kbd.TAB)
|
||||
return;
|
||||
if (e.key === kbd.ENTER || e.key === kbd.SPACE) {
|
||||
this.root.onMenuToggle(this.menu.opts.value.current);
|
||||
}
|
||||
if (e.key === kbd.ARROW_DOWN) {
|
||||
this.menu.openMenu();
|
||||
}
|
||||
// prevent keydown from scrolling window / first focused item
|
||||
// from inadvertently closing the menu
|
||||
if (e.key === kbd.ENTER || e.key === kbd.SPACE || e.key === kbd.ARROW_DOWN) {
|
||||
this.menu.wasOpenedByKeyboard = true;
|
||||
e.preventDefault();
|
||||
}
|
||||
this.root.rovingFocusGroup.handleKeydown(this.menu.getTriggerNode(), e);
|
||||
};
|
||||
onfocus = () => {
|
||||
this.isFocused = true;
|
||||
};
|
||||
onblur = () => {
|
||||
this.isFocused = false;
|
||||
};
|
||||
props = $derived.by(() => ({
|
||||
type: "button",
|
||||
role: "menuitem",
|
||||
id: this.opts.id.current,
|
||||
"aria-haspopup": "menu",
|
||||
"aria-expanded": boolToStr(this.menu.open),
|
||||
"aria-controls": this.menu.open ? this.menu.contentId : undefined,
|
||||
"data-highlighted": this.isFocused ? "" : undefined,
|
||||
"data-state": getDataOpenClosed(this.menu.open),
|
||||
"data-disabled": boolToEmptyStrOrUndef(this.opts.disabled.current),
|
||||
"data-menu-value": this.menu.opts.value.current,
|
||||
disabled: this.opts.disabled.current ? true : undefined,
|
||||
tabindex: this.#tabIndex,
|
||||
[menubarAttrs.trigger]: "",
|
||||
onpointerdown: this.onpointerdown,
|
||||
onpointerenter: this.onpointerenter,
|
||||
onkeydown: this.onkeydown,
|
||||
onfocus: this.onfocus,
|
||||
onblur: this.onblur,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class MenubarContentState {
|
||||
static create(opts) {
|
||||
return new MenubarContentState(opts, MenubarMenuContext.get());
|
||||
}
|
||||
opts;
|
||||
menu;
|
||||
root;
|
||||
attachment;
|
||||
constructor(opts, menu) {
|
||||
this.opts = opts;
|
||||
this.menu = menu;
|
||||
this.root = menu.root;
|
||||
this.attachment = attachRef(this.opts.ref, (v) => (this.menu.contentNode = v));
|
||||
}
|
||||
onCloseAutoFocus = (e) => {
|
||||
this.opts.onCloseAutoFocus.current?.(e);
|
||||
if (e.defaultPrevented)
|
||||
return;
|
||||
};
|
||||
onFocusOutside = (e) => {
|
||||
const target = e.target;
|
||||
const isMenubarTrigger = this.root
|
||||
.getTriggers()
|
||||
.some((trigger) => trigger.contains(target));
|
||||
if (isMenubarTrigger)
|
||||
e.preventDefault();
|
||||
this.opts.onFocusOutside.current(e);
|
||||
};
|
||||
onInteractOutside = (e) => {
|
||||
this.opts.onInteractOutside.current(e);
|
||||
};
|
||||
onOpenAutoFocus = (e) => {
|
||||
this.opts.onOpenAutoFocus.current(e);
|
||||
if (e.defaultPrevented)
|
||||
return;
|
||||
afterTick(() => this.opts.ref.current?.focus());
|
||||
};
|
||||
onkeydown = (e) => {
|
||||
if (e.key !== kbd.ARROW_LEFT && e.key !== kbd.ARROW_RIGHT)
|
||||
return;
|
||||
const target = e.target;
|
||||
const targetIsSubTrigger = target.hasAttribute("data-menu-sub-trigger");
|
||||
const isKeydownInsideSubMenu = target.closest("[data-menu-content]") !== e.currentTarget;
|
||||
const prevMenuKey = this.root.opts.dir.current === "rtl" ? kbd.ARROW_RIGHT : kbd.ARROW_LEFT;
|
||||
const isPrevKey = prevMenuKey === e.key;
|
||||
const isNextKey = !isPrevKey;
|
||||
// prevent navigation when opening a submenu
|
||||
if (isNextKey && targetIsSubTrigger)
|
||||
return;
|
||||
// or if we're inside a submenu and moving back to close it
|
||||
if (isKeydownInsideSubMenu && isPrevKey)
|
||||
return;
|
||||
const items = this.root.getTriggers().filter((trigger) => !trigger.disabled);
|
||||
let candidates = items.map((item) => ({
|
||||
value: item.getAttribute("data-menu-value"),
|
||||
triggerId: item.id ?? "",
|
||||
}));
|
||||
if (isPrevKey)
|
||||
candidates.reverse();
|
||||
const candidateValues = candidates.map(({ value }) => value);
|
||||
const currentIndex = candidateValues.indexOf(this.menu.opts.value.current);
|
||||
candidates = this.root.opts.loop.current
|
||||
? wrapArray(candidates, currentIndex + 1)
|
||||
: candidates.slice(currentIndex + 1);
|
||||
const [nextValue] = candidates;
|
||||
if (nextValue)
|
||||
this.menu.root.onMenuOpen(nextValue.value, nextValue.triggerId);
|
||||
};
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"aria-labelledby": this.menu.triggerId,
|
||||
style: getFloatingContentCSSVars("menubar"),
|
||||
onkeydown: this.onkeydown,
|
||||
"data-menu-content": "",
|
||||
[menubarAttrs.content]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
popperProps = {
|
||||
onCloseAutoFocus: this.onCloseAutoFocus,
|
||||
onFocusOutside: this.onFocusOutside,
|
||||
onInteractOutside: this.onInteractOutside,
|
||||
onOpenAutoFocus: this.onOpenAutoFocus,
|
||||
};
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import type { ArrowPropsWithoutHTML } from "../utilities/arrow/types.js";
|
||||
import type { MenuArrowProps } from "../menu/types.js";
|
||||
import type { OnChangeFn, WithChild, WithChildren, Without } from "../../internal/types.js";
|
||||
import type { BitsPrimitiveButtonAttributes, BitsPrimitiveDivAttributes } from "../../shared/attributes.js";
|
||||
import type { Direction } from "../../shared/index.js";
|
||||
export type MenubarRootPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* The reading direction of the menubar.
|
||||
*/
|
||||
dir?: Direction;
|
||||
/**
|
||||
* When `true`, the roving focus will loop back to the first menu item when the last
|
||||
* menu item is reached and vice verse.
|
||||
*/
|
||||
loop?: boolean;
|
||||
/**
|
||||
* The 'value' assigned to the currently active menu in the menubar.
|
||||
*/
|
||||
value?: string;
|
||||
/**
|
||||
* A callback that is called when the active menu changes.
|
||||
*/
|
||||
onValueChange?: OnChangeFn<string>;
|
||||
}>;
|
||||
export type MenubarRootProps = MenubarRootPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, MenubarRootPropsWithoutHTML>;
|
||||
export type MenubarMenuPropsWithoutHTML = WithChildren<{
|
||||
/**
|
||||
* The `value` assigned to the menu. Used to programmatically control the menu's open state
|
||||
* within the menubar.
|
||||
*/
|
||||
value?: string;
|
||||
/**
|
||||
* A callback that is called when the menu is opened or closed.
|
||||
*/
|
||||
onOpenChange?: OnChangeFn<boolean>;
|
||||
}>;
|
||||
export type MenubarMenuProps = MenubarMenuPropsWithoutHTML;
|
||||
export type MenubarTriggerPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* Whether the trigger for the menubar item is disabled.
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
disabled?: boolean | null | undefined;
|
||||
}>;
|
||||
export type MenubarTriggerProps = MenubarTriggerPropsWithoutHTML & Without<BitsPrimitiveButtonAttributes, MenubarTriggerPropsWithoutHTML>;
|
||||
export type { MenuContentPropsWithoutHTML as MenubarContentPropsWithoutHTML, MenuContentProps as MenubarContentProps, MenuContentStaticPropsWithoutHTML as MenubarContentStaticPropsWithoutHTML, MenuContentStaticProps as MenubarContentStaticProps, MenuItemPropsWithoutHTML as MenubarItemPropsWithoutHTML, MenuItemProps as MenubarItemProps, MenuGroupPropsWithoutHTML as MenubarGroupPropsWithoutHTML, MenuGroupProps as MenubarGroupProps, MenuGroupHeadingPropsWithoutHTML as MenubarGroupHeadingPropsWithoutHTML, MenuGroupHeadingProps as MenubarGroupHeadingProps, MenuCheckboxItemPropsWithoutHTML as MenubarCheckboxItemPropsWithoutHTML, MenuCheckboxItemProps as MenubarCheckboxItemProps, MenuRadioGroupPropsWithoutHTML as MenubarRadioGroupPropsWithoutHTML, MenuRadioGroupProps as MenubarRadioGroupProps, MenuRadioItemPropsWithoutHTML as MenubarRadioItemPropsWithoutHTML, MenuRadioItemProps as MenubarRadioItemProps, MenuSeparatorPropsWithoutHTML as MenubarSeparatorPropsWithoutHTML, MenuSeparatorProps as MenubarSeparatorProps, MenuSubContentPropsWithoutHTML as MenubarSubContentPropsWithoutHTML, MenuSubContentProps as MenubarSubContentProps, MenuSubContentStaticPropsWithoutHTML as MenubarSubContentStaticPropsWithoutHTML, MenuSubContentStaticProps as MenubarSubContentStaticProps, MenuSubTriggerPropsWithoutHTML as MenubarSubTriggerPropsWithoutHTML, MenuSubTriggerProps as MenubarSubTriggerProps, MenuSubPropsWithoutHTML as MenubarSubPropsWithoutHTML, MenuPortalPropsWithoutHTML as MenubarPortalPropsWithoutHTML, MenuPortalProps as MenubarPortalProps, MenuCheckboxGroupPropsWithoutHTML as MenubarCheckboxGroupPropsWithoutHTML, MenuCheckboxGroupProps as MenubarCheckboxGroupProps, } from "../menu/types.js";
|
||||
export type MenubarArrowPropsWithoutHTML = ArrowPropsWithoutHTML;
|
||||
export type MenubarArrowProps = MenuArrowProps;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Reference in New Issue
Block a user