feat(primitives): add menu, dropdown-menu, context-menu, and menubar primitives

Implements WAI-ARIA APG-compliant headless menu primitive families:

- menu: base primitive with MenuRoot, MenuContent, MenuItem,
  MenuCheckboxItem, MenuRadioGroup/Item, MenuSub, and helpers
- dropdown-menu: DropdownMenuRoot with trigger anchoring
- context-menu: ContextMenuRoot with right-click virtual anchor
- menubar: MenubarRoot with keyboard navigation between menus

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-17 18:48:43 +07:00
parent 333a18cbaf
commit 1d3efa5028
81 changed files with 2554 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
import type { CheckedState } from './types';
import type { ComputedRef, Ref, ShallowRef } from 'vue';
import type { Direction } from '../config-provider';
import { useContextFactory } from '@robonen/vue';
export interface MenuContext {
open: Ref<boolean>;
onOpenChange: (open: boolean) => void;
content: Ref<HTMLElement | null>;
onContentChange: (el: HTMLElement | null) => void;
}
export const { inject: useMenuContext, provide: provideMenuContext }
= useContextFactory<MenuContext>('MenuContext');
export interface MenuRootContext {
onClose: () => void;
dir: Ref<Direction>;
isUsingKeyboardRef: Ref<boolean>;
modal: Ref<boolean>;
}
export const { inject: useMenuRootContext, provide: provideMenuRootContext }
= useContextFactory<MenuRootContext>('MenuRootContext');
export interface MenuContentContext {
onItemEnter: (event: PointerEvent) => boolean;
onItemLeave: (event: PointerEvent) => void;
onTriggerLeave: (event: PointerEvent) => boolean;
searchRef: Ref<string>;
pointerGraceTimerRef: Ref<number>;
onPointerGraceIntentChange: (intent: { area: Array<{ x: number; y: number }>; side: 'left' | 'right' } | null) => void;
}
export const { inject: useMenuContentContext, provide: provideMenuContentContext }
= useContextFactory<MenuContentContext>('MenuContentContext');
export interface MenuSubContext {
contentId: ComputedRef<string>;
triggerId: ComputedRef<string>;
trigger: ShallowRef<HTMLElement | null>;
onTriggerChange: (el: HTMLElement | null) => void;
}
export const { inject: useMenuSubContext, provide: provideMenuSubContext }
= useContextFactory<MenuSubContext>('MenuSubContext');
export interface MenuRadioGroupContext {
modelValue: Ref<string | undefined>;
onValueChange: (value: string) => void;
}
export const { inject: useMenuRadioGroupContext, provide: provideMenuRadioGroupContext }
= useContextFactory<MenuRadioGroupContext>('MenuRadioGroupContext');
export interface MenuItemIndicatorContext {
checkedState: Ref<CheckedState>;
}
export const { inject: useMenuItemIndicatorContext, provide: provideMenuItemIndicatorContext }
= useContextFactory<MenuItemIndicatorContext>('MenuItemIndicatorContext');
export interface MenuGroupContext {
id: string;
}
export const { inject: useMenuGroupContext, provide: provideMenuGroupContext }
= useContextFactory<MenuGroupContext>('MenuGroupContext');