From 1d3efa5028a4d50e740343abfbb37544d3e6da4e Mon Sep 17 00:00:00 2001 From: robonen Date: Sun, 17 May 2026 18:48:43 +0700 Subject: [PATCH] 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> --- .../src/context-menu/ContextMenuArrow.vue | 15 ++ .../context-menu/ContextMenuCheckboxItem.vue | 21 ++ .../src/context-menu/ContextMenuContent.vue | 32 +++ .../src/context-menu/ContextMenuGroup.vue | 15 ++ .../src/context-menu/ContextMenuItem.vue | 17 ++ .../context-menu/ContextMenuItemIndicator.vue | 15 ++ .../src/context-menu/ContextMenuLabel.vue | 15 ++ .../src/context-menu/ContextMenuPortal.vue | 15 ++ .../context-menu/ContextMenuRadioGroup.vue | 17 ++ .../src/context-menu/ContextMenuRadioItem.vue | 17 ++ .../src/context-menu/ContextMenuRoot.vue | 44 +++++ .../src/context-menu/ContextMenuSeparator.vue | 15 ++ .../src/context-menu/ContextMenuSub.vue | 17 ++ .../context-menu/ContextMenuSubContent.vue | 27 +++ .../context-menu/ContextMenuSubTrigger.vue | 15 ++ .../src/context-menu/ContextMenuTrigger.vue | 90 +++++++++ vue/primitives/src/context-menu/context.ts | 14 ++ vue/primitives/src/context-menu/index.ts | 17 ++ .../src/dropdown-menu/DropdownMenuArrow.vue | 15 ++ .../DropdownMenuCheckboxItem.vue | 21 ++ .../src/dropdown-menu/DropdownMenuContent.vue | 33 ++++ .../src/dropdown-menu/DropdownMenuGroup.vue | 15 ++ .../src/dropdown-menu/DropdownMenuItem.vue | 17 ++ .../DropdownMenuItemIndicator.vue | 15 ++ .../src/dropdown-menu/DropdownMenuLabel.vue | 15 ++ .../src/dropdown-menu/DropdownMenuPortal.vue | 15 ++ .../dropdown-menu/DropdownMenuRadioGroup.vue | 17 ++ .../dropdown-menu/DropdownMenuRadioItem.vue | 17 ++ .../src/dropdown-menu/DropdownMenuRoot.vue | 64 ++++++ .../dropdown-menu/DropdownMenuSeparator.vue | 15 ++ .../src/dropdown-menu/DropdownMenuSub.vue | 17 ++ .../dropdown-menu/DropdownMenuSubContent.vue | 27 +++ .../dropdown-menu/DropdownMenuSubTrigger.vue | 15 ++ .../src/dropdown-menu/DropdownMenuTrigger.vue | 67 +++++++ vue/primitives/src/dropdown-menu/context.ts | 15 ++ vue/primitives/src/dropdown-menu/index.ts | 17 ++ vue/primitives/src/index.ts | 39 ++++ vue/primitives/src/menu/MenuAnchor.vue | 17 ++ vue/primitives/src/menu/MenuArrow.vue | 17 ++ vue/primitives/src/menu/MenuCheckboxItem.vue | 57 ++++++ vue/primitives/src/menu/MenuContent.vue | 54 ++++++ vue/primitives/src/menu/MenuContentImpl.vue | 183 ++++++++++++++++++ vue/primitives/src/menu/MenuGroup.vue | 21 ++ vue/primitives/src/menu/MenuItem.vue | 32 +++ vue/primitives/src/menu/MenuItemImpl.vue | 96 +++++++++ vue/primitives/src/menu/MenuItemIndicator.vue | 32 +++ vue/primitives/src/menu/MenuLabel.vue | 17 ++ vue/primitives/src/menu/MenuPortal.vue | 17 ++ vue/primitives/src/menu/MenuRadioGroup.vue | 38 ++++ vue/primitives/src/menu/MenuRadioItem.vue | 45 +++++ vue/primitives/src/menu/MenuRoot.vue | 57 ++++++ .../src/menu/MenuRootContentModal.vue | 41 ++++ .../src/menu/MenuRootContentNonModal.vue | 34 ++++ vue/primitives/src/menu/MenuSeparator.vue | 17 ++ vue/primitives/src/menu/MenuSub.vue | 45 +++++ vue/primitives/src/menu/MenuSubContent.vue | 53 +++++ vue/primitives/src/menu/MenuSubTrigger.vue | 83 ++++++++ vue/primitives/src/menu/context.ts | 62 ++++++ vue/primitives/src/menu/index.ts | 20 ++ vue/primitives/src/menu/types.ts | 1 + vue/primitives/src/menu/useIsUsingKeyboard.ts | 23 +++ vue/primitives/src/menu/utils.ts | 86 ++++++++ vue/primitives/src/menubar/MenubarArrow.vue | 15 ++ .../src/menubar/MenubarCheckboxItem.vue | 21 ++ vue/primitives/src/menubar/MenubarContent.vue | 46 +++++ vue/primitives/src/menubar/MenubarGroup.vue | 15 ++ vue/primitives/src/menubar/MenubarItem.vue | 17 ++ .../src/menubar/MenubarItemIndicator.vue | 15 ++ vue/primitives/src/menubar/MenubarLabel.vue | 15 ++ vue/primitives/src/menubar/MenubarMenu.vue | 50 +++++ vue/primitives/src/menubar/MenubarPortal.vue | 15 ++ .../src/menubar/MenubarRadioGroup.vue | 17 ++ .../src/menubar/MenubarRadioItem.vue | 17 ++ vue/primitives/src/menubar/MenubarRoot.vue | 61 ++++++ .../src/menubar/MenubarSeparator.vue | 15 ++ vue/primitives/src/menubar/MenubarSub.vue | 17 ++ .../src/menubar/MenubarSubContent.vue | 27 +++ .../src/menubar/MenubarSubTrigger.vue | 15 ++ vue/primitives/src/menubar/MenubarTrigger.vue | 72 +++++++ vue/primitives/src/menubar/context.ts | 32 +++ vue/primitives/src/menubar/index.ts | 50 +++++ 81 files changed, 2554 insertions(+) create mode 100644 vue/primitives/src/context-menu/ContextMenuArrow.vue create mode 100644 vue/primitives/src/context-menu/ContextMenuCheckboxItem.vue create mode 100644 vue/primitives/src/context-menu/ContextMenuContent.vue create mode 100644 vue/primitives/src/context-menu/ContextMenuGroup.vue create mode 100644 vue/primitives/src/context-menu/ContextMenuItem.vue create mode 100644 vue/primitives/src/context-menu/ContextMenuItemIndicator.vue create mode 100644 vue/primitives/src/context-menu/ContextMenuLabel.vue create mode 100644 vue/primitives/src/context-menu/ContextMenuPortal.vue create mode 100644 vue/primitives/src/context-menu/ContextMenuRadioGroup.vue create mode 100644 vue/primitives/src/context-menu/ContextMenuRadioItem.vue create mode 100644 vue/primitives/src/context-menu/ContextMenuRoot.vue create mode 100644 vue/primitives/src/context-menu/ContextMenuSeparator.vue create mode 100644 vue/primitives/src/context-menu/ContextMenuSub.vue create mode 100644 vue/primitives/src/context-menu/ContextMenuSubContent.vue create mode 100644 vue/primitives/src/context-menu/ContextMenuSubTrigger.vue create mode 100644 vue/primitives/src/context-menu/ContextMenuTrigger.vue create mode 100644 vue/primitives/src/context-menu/context.ts create mode 100644 vue/primitives/src/context-menu/index.ts create mode 100644 vue/primitives/src/dropdown-menu/DropdownMenuArrow.vue create mode 100644 vue/primitives/src/dropdown-menu/DropdownMenuCheckboxItem.vue create mode 100644 vue/primitives/src/dropdown-menu/DropdownMenuContent.vue create mode 100644 vue/primitives/src/dropdown-menu/DropdownMenuGroup.vue create mode 100644 vue/primitives/src/dropdown-menu/DropdownMenuItem.vue create mode 100644 vue/primitives/src/dropdown-menu/DropdownMenuItemIndicator.vue create mode 100644 vue/primitives/src/dropdown-menu/DropdownMenuLabel.vue create mode 100644 vue/primitives/src/dropdown-menu/DropdownMenuPortal.vue create mode 100644 vue/primitives/src/dropdown-menu/DropdownMenuRadioGroup.vue create mode 100644 vue/primitives/src/dropdown-menu/DropdownMenuRadioItem.vue create mode 100644 vue/primitives/src/dropdown-menu/DropdownMenuRoot.vue create mode 100644 vue/primitives/src/dropdown-menu/DropdownMenuSeparator.vue create mode 100644 vue/primitives/src/dropdown-menu/DropdownMenuSub.vue create mode 100644 vue/primitives/src/dropdown-menu/DropdownMenuSubContent.vue create mode 100644 vue/primitives/src/dropdown-menu/DropdownMenuSubTrigger.vue create mode 100644 vue/primitives/src/dropdown-menu/DropdownMenuTrigger.vue create mode 100644 vue/primitives/src/dropdown-menu/context.ts create mode 100644 vue/primitives/src/dropdown-menu/index.ts create mode 100644 vue/primitives/src/menu/MenuAnchor.vue create mode 100644 vue/primitives/src/menu/MenuArrow.vue create mode 100644 vue/primitives/src/menu/MenuCheckboxItem.vue create mode 100644 vue/primitives/src/menu/MenuContent.vue create mode 100644 vue/primitives/src/menu/MenuContentImpl.vue create mode 100644 vue/primitives/src/menu/MenuGroup.vue create mode 100644 vue/primitives/src/menu/MenuItem.vue create mode 100644 vue/primitives/src/menu/MenuItemImpl.vue create mode 100644 vue/primitives/src/menu/MenuItemIndicator.vue create mode 100644 vue/primitives/src/menu/MenuLabel.vue create mode 100644 vue/primitives/src/menu/MenuPortal.vue create mode 100644 vue/primitives/src/menu/MenuRadioGroup.vue create mode 100644 vue/primitives/src/menu/MenuRadioItem.vue create mode 100644 vue/primitives/src/menu/MenuRoot.vue create mode 100644 vue/primitives/src/menu/MenuRootContentModal.vue create mode 100644 vue/primitives/src/menu/MenuRootContentNonModal.vue create mode 100644 vue/primitives/src/menu/MenuSeparator.vue create mode 100644 vue/primitives/src/menu/MenuSub.vue create mode 100644 vue/primitives/src/menu/MenuSubContent.vue create mode 100644 vue/primitives/src/menu/MenuSubTrigger.vue create mode 100644 vue/primitives/src/menu/context.ts create mode 100644 vue/primitives/src/menu/index.ts create mode 100644 vue/primitives/src/menu/types.ts create mode 100644 vue/primitives/src/menu/useIsUsingKeyboard.ts create mode 100644 vue/primitives/src/menu/utils.ts create mode 100644 vue/primitives/src/menubar/MenubarArrow.vue create mode 100644 vue/primitives/src/menubar/MenubarCheckboxItem.vue create mode 100644 vue/primitives/src/menubar/MenubarContent.vue create mode 100644 vue/primitives/src/menubar/MenubarGroup.vue create mode 100644 vue/primitives/src/menubar/MenubarItem.vue create mode 100644 vue/primitives/src/menubar/MenubarItemIndicator.vue create mode 100644 vue/primitives/src/menubar/MenubarLabel.vue create mode 100644 vue/primitives/src/menubar/MenubarMenu.vue create mode 100644 vue/primitives/src/menubar/MenubarPortal.vue create mode 100644 vue/primitives/src/menubar/MenubarRadioGroup.vue create mode 100644 vue/primitives/src/menubar/MenubarRadioItem.vue create mode 100644 vue/primitives/src/menubar/MenubarRoot.vue create mode 100644 vue/primitives/src/menubar/MenubarSeparator.vue create mode 100644 vue/primitives/src/menubar/MenubarSub.vue create mode 100644 vue/primitives/src/menubar/MenubarSubContent.vue create mode 100644 vue/primitives/src/menubar/MenubarSubTrigger.vue create mode 100644 vue/primitives/src/menubar/MenubarTrigger.vue create mode 100644 vue/primitives/src/menubar/context.ts create mode 100644 vue/primitives/src/menubar/index.ts diff --git a/vue/primitives/src/context-menu/ContextMenuArrow.vue b/vue/primitives/src/context-menu/ContextMenuArrow.vue new file mode 100644 index 0000000..bbc7866 --- /dev/null +++ b/vue/primitives/src/context-menu/ContextMenuArrow.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/context-menu/ContextMenuCheckboxItem.vue b/vue/primitives/src/context-menu/ContextMenuCheckboxItem.vue new file mode 100644 index 0000000..c3e6eba --- /dev/null +++ b/vue/primitives/src/context-menu/ContextMenuCheckboxItem.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/vue/primitives/src/context-menu/ContextMenuContent.vue b/vue/primitives/src/context-menu/ContextMenuContent.vue new file mode 100644 index 0000000..c996faa --- /dev/null +++ b/vue/primitives/src/context-menu/ContextMenuContent.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/vue/primitives/src/context-menu/ContextMenuGroup.vue b/vue/primitives/src/context-menu/ContextMenuGroup.vue new file mode 100644 index 0000000..00b6c90 --- /dev/null +++ b/vue/primitives/src/context-menu/ContextMenuGroup.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/context-menu/ContextMenuItem.vue b/vue/primitives/src/context-menu/ContextMenuItem.vue new file mode 100644 index 0000000..cddf23b --- /dev/null +++ b/vue/primitives/src/context-menu/ContextMenuItem.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/vue/primitives/src/context-menu/ContextMenuItemIndicator.vue b/vue/primitives/src/context-menu/ContextMenuItemIndicator.vue new file mode 100644 index 0000000..422c63c --- /dev/null +++ b/vue/primitives/src/context-menu/ContextMenuItemIndicator.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/context-menu/ContextMenuLabel.vue b/vue/primitives/src/context-menu/ContextMenuLabel.vue new file mode 100644 index 0000000..6bf59b7 --- /dev/null +++ b/vue/primitives/src/context-menu/ContextMenuLabel.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/context-menu/ContextMenuPortal.vue b/vue/primitives/src/context-menu/ContextMenuPortal.vue new file mode 100644 index 0000000..eed0027 --- /dev/null +++ b/vue/primitives/src/context-menu/ContextMenuPortal.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/context-menu/ContextMenuRadioGroup.vue b/vue/primitives/src/context-menu/ContextMenuRadioGroup.vue new file mode 100644 index 0000000..0761c76 --- /dev/null +++ b/vue/primitives/src/context-menu/ContextMenuRadioGroup.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/vue/primitives/src/context-menu/ContextMenuRadioItem.vue b/vue/primitives/src/context-menu/ContextMenuRadioItem.vue new file mode 100644 index 0000000..3206fec --- /dev/null +++ b/vue/primitives/src/context-menu/ContextMenuRadioItem.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/vue/primitives/src/context-menu/ContextMenuRoot.vue b/vue/primitives/src/context-menu/ContextMenuRoot.vue new file mode 100644 index 0000000..8448d1e --- /dev/null +++ b/vue/primitives/src/context-menu/ContextMenuRoot.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/vue/primitives/src/context-menu/ContextMenuSeparator.vue b/vue/primitives/src/context-menu/ContextMenuSeparator.vue new file mode 100644 index 0000000..f5e2f6c --- /dev/null +++ b/vue/primitives/src/context-menu/ContextMenuSeparator.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/context-menu/ContextMenuSub.vue b/vue/primitives/src/context-menu/ContextMenuSub.vue new file mode 100644 index 0000000..f7c9203 --- /dev/null +++ b/vue/primitives/src/context-menu/ContextMenuSub.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/vue/primitives/src/context-menu/ContextMenuSubContent.vue b/vue/primitives/src/context-menu/ContextMenuSubContent.vue new file mode 100644 index 0000000..210cb63 --- /dev/null +++ b/vue/primitives/src/context-menu/ContextMenuSubContent.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/vue/primitives/src/context-menu/ContextMenuSubTrigger.vue b/vue/primitives/src/context-menu/ContextMenuSubTrigger.vue new file mode 100644 index 0000000..58b8bec --- /dev/null +++ b/vue/primitives/src/context-menu/ContextMenuSubTrigger.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/context-menu/ContextMenuTrigger.vue b/vue/primitives/src/context-menu/ContextMenuTrigger.vue new file mode 100644 index 0000000..653d3c0 --- /dev/null +++ b/vue/primitives/src/context-menu/ContextMenuTrigger.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/vue/primitives/src/context-menu/context.ts b/vue/primitives/src/context-menu/context.ts new file mode 100644 index 0000000..af2f918 --- /dev/null +++ b/vue/primitives/src/context-menu/context.ts @@ -0,0 +1,14 @@ +import type { Ref } from 'vue'; + +import { useContextFactory } from '@robonen/vue'; + +export interface ContextMenuRootContext { + open: Ref; + onOpenChange: (open: boolean) => void; + modal: Ref; +} + +export const { + inject: useContextMenuRootContext, + provide: provideContextMenuRootContext, +} = useContextFactory('ContextMenuRootContext'); diff --git a/vue/primitives/src/context-menu/index.ts b/vue/primitives/src/context-menu/index.ts new file mode 100644 index 0000000..b0153f3 --- /dev/null +++ b/vue/primitives/src/context-menu/index.ts @@ -0,0 +1,17 @@ +export { useContextMenuRootContext } from './context'; +export { default as ContextMenuArrow, type ContextMenuArrowProps } from './ContextMenuArrow.vue'; +export { default as ContextMenuCheckboxItem, type ContextMenuCheckboxItemEmits, type ContextMenuCheckboxItemProps } from './ContextMenuCheckboxItem.vue'; +export { default as ContextMenuContent, type ContextMenuContentEmits, type ContextMenuContentProps } from './ContextMenuContent.vue'; +export { default as ContextMenuGroup, type ContextMenuGroupProps } from './ContextMenuGroup.vue'; +export { default as ContextMenuItem, type ContextMenuItemEmits, type ContextMenuItemProps } from './ContextMenuItem.vue'; +export { default as ContextMenuItemIndicator, type ContextMenuItemIndicatorProps } from './ContextMenuItemIndicator.vue'; +export { default as ContextMenuLabel, type ContextMenuLabelProps } from './ContextMenuLabel.vue'; +export { default as ContextMenuPortal, type ContextMenuPortalProps } from './ContextMenuPortal.vue'; +export { default as ContextMenuRadioGroup, type ContextMenuRadioGroupEmits, type ContextMenuRadioGroupProps } from './ContextMenuRadioGroup.vue'; +export { default as ContextMenuRadioItem, type ContextMenuRadioItemEmits, type ContextMenuRadioItemProps } from './ContextMenuRadioItem.vue'; +export { default as ContextMenuRoot, type ContextMenuRootEmits, type ContextMenuRootProps } from './ContextMenuRoot.vue'; +export { default as ContextMenuSeparator, type ContextMenuSeparatorProps } from './ContextMenuSeparator.vue'; +export { default as ContextMenuSub, type ContextMenuSubEmits, type ContextMenuSubProps } from './ContextMenuSub.vue'; +export { default as ContextMenuSubContent, type ContextMenuSubContentEmits, type ContextMenuSubContentProps } from './ContextMenuSubContent.vue'; +export { default as ContextMenuSubTrigger, type ContextMenuSubTriggerProps } from './ContextMenuSubTrigger.vue'; +export { default as ContextMenuTrigger, type ContextMenuTriggerProps } from './ContextMenuTrigger.vue'; diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuArrow.vue b/vue/primitives/src/dropdown-menu/DropdownMenuArrow.vue new file mode 100644 index 0000000..98b2a80 --- /dev/null +++ b/vue/primitives/src/dropdown-menu/DropdownMenuArrow.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuCheckboxItem.vue b/vue/primitives/src/dropdown-menu/DropdownMenuCheckboxItem.vue new file mode 100644 index 0000000..d2ecbf9 --- /dev/null +++ b/vue/primitives/src/dropdown-menu/DropdownMenuCheckboxItem.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuContent.vue b/vue/primitives/src/dropdown-menu/DropdownMenuContent.vue new file mode 100644 index 0000000..1d5f622 --- /dev/null +++ b/vue/primitives/src/dropdown-menu/DropdownMenuContent.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuGroup.vue b/vue/primitives/src/dropdown-menu/DropdownMenuGroup.vue new file mode 100644 index 0000000..355961e --- /dev/null +++ b/vue/primitives/src/dropdown-menu/DropdownMenuGroup.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuItem.vue b/vue/primitives/src/dropdown-menu/DropdownMenuItem.vue new file mode 100644 index 0000000..db12231 --- /dev/null +++ b/vue/primitives/src/dropdown-menu/DropdownMenuItem.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuItemIndicator.vue b/vue/primitives/src/dropdown-menu/DropdownMenuItemIndicator.vue new file mode 100644 index 0000000..bdab714 --- /dev/null +++ b/vue/primitives/src/dropdown-menu/DropdownMenuItemIndicator.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuLabel.vue b/vue/primitives/src/dropdown-menu/DropdownMenuLabel.vue new file mode 100644 index 0000000..82a917a --- /dev/null +++ b/vue/primitives/src/dropdown-menu/DropdownMenuLabel.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuPortal.vue b/vue/primitives/src/dropdown-menu/DropdownMenuPortal.vue new file mode 100644 index 0000000..7506f63 --- /dev/null +++ b/vue/primitives/src/dropdown-menu/DropdownMenuPortal.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuRadioGroup.vue b/vue/primitives/src/dropdown-menu/DropdownMenuRadioGroup.vue new file mode 100644 index 0000000..63a431e --- /dev/null +++ b/vue/primitives/src/dropdown-menu/DropdownMenuRadioGroup.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuRadioItem.vue b/vue/primitives/src/dropdown-menu/DropdownMenuRadioItem.vue new file mode 100644 index 0000000..1c98568 --- /dev/null +++ b/vue/primitives/src/dropdown-menu/DropdownMenuRadioItem.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuRoot.vue b/vue/primitives/src/dropdown-menu/DropdownMenuRoot.vue new file mode 100644 index 0000000..e5d7686 --- /dev/null +++ b/vue/primitives/src/dropdown-menu/DropdownMenuRoot.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuSeparator.vue b/vue/primitives/src/dropdown-menu/DropdownMenuSeparator.vue new file mode 100644 index 0000000..4930a23 --- /dev/null +++ b/vue/primitives/src/dropdown-menu/DropdownMenuSeparator.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuSub.vue b/vue/primitives/src/dropdown-menu/DropdownMenuSub.vue new file mode 100644 index 0000000..62648f6 --- /dev/null +++ b/vue/primitives/src/dropdown-menu/DropdownMenuSub.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuSubContent.vue b/vue/primitives/src/dropdown-menu/DropdownMenuSubContent.vue new file mode 100644 index 0000000..891d270 --- /dev/null +++ b/vue/primitives/src/dropdown-menu/DropdownMenuSubContent.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuSubTrigger.vue b/vue/primitives/src/dropdown-menu/DropdownMenuSubTrigger.vue new file mode 100644 index 0000000..ae0e888 --- /dev/null +++ b/vue/primitives/src/dropdown-menu/DropdownMenuSubTrigger.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/dropdown-menu/DropdownMenuTrigger.vue b/vue/primitives/src/dropdown-menu/DropdownMenuTrigger.vue new file mode 100644 index 0000000..800dc45 --- /dev/null +++ b/vue/primitives/src/dropdown-menu/DropdownMenuTrigger.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/vue/primitives/src/dropdown-menu/context.ts b/vue/primitives/src/dropdown-menu/context.ts new file mode 100644 index 0000000..b8304ac --- /dev/null +++ b/vue/primitives/src/dropdown-menu/context.ts @@ -0,0 +1,15 @@ +import type { ComputedRef, ShallowRef } from 'vue'; + +import { useContextFactory } from '@robonen/vue'; + +export interface DropdownMenuRootContext { + triggerId: ComputedRef; + triggerRef: ShallowRef; + contentId: ComputedRef; + onTriggerChange: (el: HTMLElement | null) => void; +} + +export const { + inject: useDropdownMenuRootContext, + provide: provideDropdownMenuRootContext, +} = useContextFactory('DropdownMenuRootContext'); diff --git a/vue/primitives/src/dropdown-menu/index.ts b/vue/primitives/src/dropdown-menu/index.ts new file mode 100644 index 0000000..2b9989a --- /dev/null +++ b/vue/primitives/src/dropdown-menu/index.ts @@ -0,0 +1,17 @@ +export { useDropdownMenuRootContext } from './context'; +export { default as DropdownMenuArrow, type DropdownMenuArrowProps } from './DropdownMenuArrow.vue'; +export { default as DropdownMenuCheckboxItem, type DropdownMenuCheckboxItemEmits, type DropdownMenuCheckboxItemProps } from './DropdownMenuCheckboxItem.vue'; +export { default as DropdownMenuContent, type DropdownMenuContentEmits, type DropdownMenuContentProps } from './DropdownMenuContent.vue'; +export { default as DropdownMenuGroup, type DropdownMenuGroupProps } from './DropdownMenuGroup.vue'; +export { default as DropdownMenuItem, type DropdownMenuItemEmits, type DropdownMenuItemProps } from './DropdownMenuItem.vue'; +export { default as DropdownMenuItemIndicator, type DropdownMenuItemIndicatorProps } from './DropdownMenuItemIndicator.vue'; +export { default as DropdownMenuLabel, type DropdownMenuLabelProps } from './DropdownMenuLabel.vue'; +export { default as DropdownMenuPortal, type DropdownMenuPortalProps } from './DropdownMenuPortal.vue'; +export { default as DropdownMenuRadioGroup, type DropdownMenuRadioGroupEmits, type DropdownMenuRadioGroupProps } from './DropdownMenuRadioGroup.vue'; +export { default as DropdownMenuRadioItem, type DropdownMenuRadioItemEmits, type DropdownMenuRadioItemProps } from './DropdownMenuRadioItem.vue'; +export { default as DropdownMenuRoot, type DropdownMenuRootEmits, type DropdownMenuRootProps } from './DropdownMenuRoot.vue'; +export { default as DropdownMenuSeparator, type DropdownMenuSeparatorProps } from './DropdownMenuSeparator.vue'; +export { default as DropdownMenuSub, type DropdownMenuSubEmits, type DropdownMenuSubProps } from './DropdownMenuSub.vue'; +export { default as DropdownMenuSubContent, type DropdownMenuSubContentEmits, type DropdownMenuSubContentProps } from './DropdownMenuSubContent.vue'; +export { default as DropdownMenuSubTrigger, type DropdownMenuSubTriggerProps } from './DropdownMenuSubTrigger.vue'; +export { default as DropdownMenuTrigger, type DropdownMenuTriggerProps } from './DropdownMenuTrigger.vue'; diff --git a/vue/primitives/src/index.ts b/vue/primitives/src/index.ts index ded042a..d3767ee 100644 --- a/vue/primitives/src/index.ts +++ b/vue/primitives/src/index.ts @@ -1,5 +1,44 @@ export * from './config-provider'; export * from './primitive'; export * from './presence'; +export * from './collection'; +export * from './roving-focus'; export * from './pagination'; export * from './focus-scope'; +export * from './visually-hidden'; +export * from './teleport'; +export * from './dismissable-layer'; +export * from './dialog'; +export * from './alert-dialog'; +export * from './scroll-area'; +export * from './separator'; +export * from './label'; +export * from './aspect-ratio'; +export * from './toggle'; +export * from './switch'; +export * from './progress'; +export * from './collapsible'; +export * from './avatar'; +export * from './slider'; +export * from './checkbox'; +export * from './toolbar'; +export * from './radio-group'; +export * from './toggle-group'; +export * from './number-field'; +export * from './pin-input'; +export * from './tabs'; +export * from './accordion'; +export * from './popper'; +export * from './hover-card'; +export * from './popover'; +export * from './tooltip'; +export * from './tree'; +export * from './stepper'; +export * from './editable'; +export * from './tags-input'; +export * from './listbox'; + +export * from './menu'; +export * from './dropdown-menu'; +export * from './context-menu'; +export * from './menubar'; diff --git a/vue/primitives/src/menu/MenuAnchor.vue b/vue/primitives/src/menu/MenuAnchor.vue new file mode 100644 index 0000000..0a3f9ae --- /dev/null +++ b/vue/primitives/src/menu/MenuAnchor.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/vue/primitives/src/menu/MenuArrow.vue b/vue/primitives/src/menu/MenuArrow.vue new file mode 100644 index 0000000..13d62f7 --- /dev/null +++ b/vue/primitives/src/menu/MenuArrow.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/vue/primitives/src/menu/MenuCheckboxItem.vue b/vue/primitives/src/menu/MenuCheckboxItem.vue new file mode 100644 index 0000000..a91cfc8 --- /dev/null +++ b/vue/primitives/src/menu/MenuCheckboxItem.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/vue/primitives/src/menu/MenuContent.vue b/vue/primitives/src/menu/MenuContent.vue new file mode 100644 index 0000000..1ff4dd6 --- /dev/null +++ b/vue/primitives/src/menu/MenuContent.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/vue/primitives/src/menu/MenuContentImpl.vue b/vue/primitives/src/menu/MenuContentImpl.vue new file mode 100644 index 0000000..74cca31 --- /dev/null +++ b/vue/primitives/src/menu/MenuContentImpl.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/vue/primitives/src/menu/MenuGroup.vue b/vue/primitives/src/menu/MenuGroup.vue new file mode 100644 index 0000000..f6a5ca9 --- /dev/null +++ b/vue/primitives/src/menu/MenuGroup.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/vue/primitives/src/menu/MenuItem.vue b/vue/primitives/src/menu/MenuItem.vue new file mode 100644 index 0000000..97bc355 --- /dev/null +++ b/vue/primitives/src/menu/MenuItem.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/vue/primitives/src/menu/MenuItemImpl.vue b/vue/primitives/src/menu/MenuItemImpl.vue new file mode 100644 index 0000000..9249ad7 --- /dev/null +++ b/vue/primitives/src/menu/MenuItemImpl.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/vue/primitives/src/menu/MenuItemIndicator.vue b/vue/primitives/src/menu/MenuItemIndicator.vue new file mode 100644 index 0000000..bcab342 --- /dev/null +++ b/vue/primitives/src/menu/MenuItemIndicator.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/vue/primitives/src/menu/MenuLabel.vue b/vue/primitives/src/menu/MenuLabel.vue new file mode 100644 index 0000000..994cdb7 --- /dev/null +++ b/vue/primitives/src/menu/MenuLabel.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/vue/primitives/src/menu/MenuPortal.vue b/vue/primitives/src/menu/MenuPortal.vue new file mode 100644 index 0000000..f27d2d6 --- /dev/null +++ b/vue/primitives/src/menu/MenuPortal.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/vue/primitives/src/menu/MenuRadioGroup.vue b/vue/primitives/src/menu/MenuRadioGroup.vue new file mode 100644 index 0000000..e1cc51a --- /dev/null +++ b/vue/primitives/src/menu/MenuRadioGroup.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/vue/primitives/src/menu/MenuRadioItem.vue b/vue/primitives/src/menu/MenuRadioItem.vue new file mode 100644 index 0000000..89f258e --- /dev/null +++ b/vue/primitives/src/menu/MenuRadioItem.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/vue/primitives/src/menu/MenuRoot.vue b/vue/primitives/src/menu/MenuRoot.vue new file mode 100644 index 0000000..f845355 --- /dev/null +++ b/vue/primitives/src/menu/MenuRoot.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/vue/primitives/src/menu/MenuRootContentModal.vue b/vue/primitives/src/menu/MenuRootContentModal.vue new file mode 100644 index 0000000..140203b --- /dev/null +++ b/vue/primitives/src/menu/MenuRootContentModal.vue @@ -0,0 +1,41 @@ + + + diff --git a/vue/primitives/src/menu/MenuRootContentNonModal.vue b/vue/primitives/src/menu/MenuRootContentNonModal.vue new file mode 100644 index 0000000..24bf5f2 --- /dev/null +++ b/vue/primitives/src/menu/MenuRootContentNonModal.vue @@ -0,0 +1,34 @@ + + + diff --git a/vue/primitives/src/menu/MenuSeparator.vue b/vue/primitives/src/menu/MenuSeparator.vue new file mode 100644 index 0000000..9538de3 --- /dev/null +++ b/vue/primitives/src/menu/MenuSeparator.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/vue/primitives/src/menu/MenuSub.vue b/vue/primitives/src/menu/MenuSub.vue new file mode 100644 index 0000000..fdaaa9d --- /dev/null +++ b/vue/primitives/src/menu/MenuSub.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/vue/primitives/src/menu/MenuSubContent.vue b/vue/primitives/src/menu/MenuSubContent.vue new file mode 100644 index 0000000..889887d --- /dev/null +++ b/vue/primitives/src/menu/MenuSubContent.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/vue/primitives/src/menu/MenuSubTrigger.vue b/vue/primitives/src/menu/MenuSubTrigger.vue new file mode 100644 index 0000000..85deebe --- /dev/null +++ b/vue/primitives/src/menu/MenuSubTrigger.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/vue/primitives/src/menu/context.ts b/vue/primitives/src/menu/context.ts new file mode 100644 index 0000000..aebb2f1 --- /dev/null +++ b/vue/primitives/src/menu/context.ts @@ -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; + onOpenChange: (open: boolean) => void; + content: Ref; + onContentChange: (el: HTMLElement | null) => void; +} +export const { inject: useMenuContext, provide: provideMenuContext } + = useContextFactory('MenuContext'); + +export interface MenuRootContext { + onClose: () => void; + dir: Ref; + isUsingKeyboardRef: Ref; + modal: Ref; +} +export const { inject: useMenuRootContext, provide: provideMenuRootContext } + = useContextFactory('MenuRootContext'); + +export interface MenuContentContext { + onItemEnter: (event: PointerEvent) => boolean; + onItemLeave: (event: PointerEvent) => void; + onTriggerLeave: (event: PointerEvent) => boolean; + searchRef: Ref; + pointerGraceTimerRef: Ref; + onPointerGraceIntentChange: (intent: { area: Array<{ x: number; y: number }>; side: 'left' | 'right' } | null) => void; +} +export const { inject: useMenuContentContext, provide: provideMenuContentContext } + = useContextFactory('MenuContentContext'); + +export interface MenuSubContext { + contentId: ComputedRef; + triggerId: ComputedRef; + trigger: ShallowRef; + onTriggerChange: (el: HTMLElement | null) => void; +} +export const { inject: useMenuSubContext, provide: provideMenuSubContext } + = useContextFactory('MenuSubContext'); + +export interface MenuRadioGroupContext { + modelValue: Ref; + onValueChange: (value: string) => void; +} +export const { inject: useMenuRadioGroupContext, provide: provideMenuRadioGroupContext } + = useContextFactory('MenuRadioGroupContext'); + +export interface MenuItemIndicatorContext { + checkedState: Ref; +} +export const { inject: useMenuItemIndicatorContext, provide: provideMenuItemIndicatorContext } + = useContextFactory('MenuItemIndicatorContext'); + +export interface MenuGroupContext { + id: string; +} +export const { inject: useMenuGroupContext, provide: provideMenuGroupContext } + = useContextFactory('MenuGroupContext'); diff --git a/vue/primitives/src/menu/index.ts b/vue/primitives/src/menu/index.ts new file mode 100644 index 0000000..d3078e9 --- /dev/null +++ b/vue/primitives/src/menu/index.ts @@ -0,0 +1,20 @@ +export type { CheckedState } from './types'; + +export { useMenuContext, useMenuContentContext, useMenuRootContext, useMenuSubContext } from './context'; +export { default as MenuAnchor, type MenuAnchorProps } from './MenuAnchor.vue'; +export { default as MenuArrow, type MenuArrowProps } from './MenuArrow.vue'; +export { default as MenuCheckboxItem, type MenuCheckboxItemEmits, type MenuCheckboxItemProps } from './MenuCheckboxItem.vue'; +export { default as MenuContent, type MenuContentEmits, type MenuContentProps } from './MenuContent.vue'; +export { default as MenuGroup, type MenuGroupProps } from './MenuGroup.vue'; +export { default as MenuItem, type MenuItemEmits, type MenuItemProps } from './MenuItem.vue'; +export { default as MenuItemImpl, type MenuItemImplEmits, type MenuItemImplProps } from './MenuItemImpl.vue'; +export { default as MenuItemIndicator, type MenuItemIndicatorProps } from './MenuItemIndicator.vue'; +export { default as MenuLabel, type MenuLabelProps } from './MenuLabel.vue'; +export { default as MenuPortal, type MenuPortalProps } from './MenuPortal.vue'; +export { default as MenuRadioGroup, type MenuRadioGroupEmits, type MenuRadioGroupProps } from './MenuRadioGroup.vue'; +export { default as MenuRadioItem, type MenuRadioItemEmits, type MenuRadioItemProps } from './MenuRadioItem.vue'; +export { default as MenuRoot, type MenuRootEmits, type MenuRootProps } from './MenuRoot.vue'; +export { default as MenuSeparator, type MenuSeparatorProps } from './MenuSeparator.vue'; +export { default as MenuSub, type MenuSubEmits, type MenuSubProps } from './MenuSub.vue'; +export { default as MenuSubContent, type MenuSubContentEmits, type MenuSubContentProps } from './MenuSubContent.vue'; +export { default as MenuSubTrigger, type MenuSubTriggerProps } from './MenuSubTrigger.vue'; diff --git a/vue/primitives/src/menu/types.ts b/vue/primitives/src/menu/types.ts new file mode 100644 index 0000000..6df28d2 --- /dev/null +++ b/vue/primitives/src/menu/types.ts @@ -0,0 +1 @@ +export type CheckedState = boolean | 'indeterminate'; diff --git a/vue/primitives/src/menu/useIsUsingKeyboard.ts b/vue/primitives/src/menu/useIsUsingKeyboard.ts new file mode 100644 index 0000000..c5c0b98 --- /dev/null +++ b/vue/primitives/src/menu/useIsUsingKeyboard.ts @@ -0,0 +1,23 @@ +import { ref } from 'vue'; + +const isUsingKeyboard = ref(false); +let initialized = false; + +function init() { + if (initialized || typeof document === 'undefined') return; + initialized = true; + document.addEventListener('keydown', () => { + isUsingKeyboard.value = true; + }, { capture: true, passive: true }); + document.addEventListener('pointerdown', () => { + isUsingKeyboard.value = false; + }, { capture: true, passive: true }); + document.addEventListener('pointermove', () => { + isUsingKeyboard.value = false; + }, { capture: true, passive: true }); +} + +export function useIsUsingKeyboard() { + init(); + return isUsingKeyboard; +} diff --git a/vue/primitives/src/menu/utils.ts b/vue/primitives/src/menu/utils.ts new file mode 100644 index 0000000..1b06501 --- /dev/null +++ b/vue/primitives/src/menu/utils.ts @@ -0,0 +1,86 @@ +import type { CheckedState } from './types'; + +import { getActiveElement } from '@robonen/platform/browsers'; + +export const ITEM_SELECT = 'menu.itemSelect'; +export const SELECTION_KEYS = ['Enter', ' ']; +export const FIRST_KEYS = ['ArrowDown', 'PageUp', 'Home']; +export const LAST_KEYS = ['ArrowUp', 'PageDown', 'End']; +export const FIRST_LAST_KEYS = [...FIRST_KEYS, ...LAST_KEYS]; +export const SUB_OPEN_KEYS: Record = { + ltr: [...SELECTION_KEYS, 'ArrowRight'], + rtl: [...SELECTION_KEYS, 'ArrowLeft'], +}; +export const SUB_CLOSE_KEYS: Record = { + ltr: ['ArrowLeft'], + rtl: ['ArrowRight'], +}; + +export function getOpenState(open: boolean): 'open' | 'closed' { + return open ? 'open' : 'closed'; +} + +export function isIndeterminate(checked: CheckedState): checked is 'indeterminate' { + return checked === 'indeterminate'; +} + +export function getCheckedState(checked: CheckedState): 'checked' | 'unchecked' | 'indeterminate' { + if (isIndeterminate(checked)) return 'indeterminate'; + return checked ? 'checked' : 'unchecked'; +} + +export function focusFirst(candidates: HTMLElement[]): void { + for (const candidate of candidates) { + const prev = getActiveElement(); + candidate.focus({ preventScroll: true }); + if (getActiveElement() !== prev) return; + } +} + +export function getNextMatch( + items: HTMLElement[], + search: string, + currentItem?: HTMLElement | null, +): HTMLElement | undefined { + const isRepeating = search.length > 1 && Array.from(search).every(c => c === search[0]); + const normalizedSearch = isRepeating ? search[0]! : search; + + const currentIndex = currentItem ? items.indexOf(currentItem) : -1; + const wrappedItems = currentIndex !== -1 + ? [...items.slice(currentIndex + 1), ...items.slice(0, currentIndex + 1)] + : items; + + const getText = (el: HTMLElement) => + el.dataset['primitiveMenuItemTextValue'] ?? el.textContent?.trim() ?? ''; + + return wrappedItems.find(item => + getText(item).toLowerCase().startsWith(normalizedSearch.toLowerCase()), + ); +} + +export interface Point { x: number; y: number }; +export type Polygon = Point[]; +export type Side = 'left' | 'right'; +export interface GraceIntent { area: Polygon; side: Side } + +export function isPointInPolygon(point: Point, polygon: Polygon): boolean { + const { x, y } = point; + let inside = false; + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const xi = polygon[i]!.x; + const yi = polygon[i]!.y; + const xj = polygon[j]!.x; + const yj = polygon[j]!.y; + const intersects = (yi > y) !== (yj > y) && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi; + if (intersects) inside = !inside; + } + return inside; +} + +export function isPointerInGraceArea(event: PointerEvent, area: Polygon): boolean { + return isPointInPolygon({ x: event.clientX, y: event.clientY }, area); +} + +export function isMouseEvent(event: Event): event is MouseEvent { + return ['mousedown', 'mouseup', 'mousemove', 'click'].includes(event.type); +} diff --git a/vue/primitives/src/menubar/MenubarArrow.vue b/vue/primitives/src/menubar/MenubarArrow.vue new file mode 100644 index 0000000..eb82864 --- /dev/null +++ b/vue/primitives/src/menubar/MenubarArrow.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/menubar/MenubarCheckboxItem.vue b/vue/primitives/src/menubar/MenubarCheckboxItem.vue new file mode 100644 index 0000000..bd8fce4 --- /dev/null +++ b/vue/primitives/src/menubar/MenubarCheckboxItem.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/vue/primitives/src/menubar/MenubarContent.vue b/vue/primitives/src/menubar/MenubarContent.vue new file mode 100644 index 0000000..19fc7e6 --- /dev/null +++ b/vue/primitives/src/menubar/MenubarContent.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/vue/primitives/src/menubar/MenubarGroup.vue b/vue/primitives/src/menubar/MenubarGroup.vue new file mode 100644 index 0000000..aec69d7 --- /dev/null +++ b/vue/primitives/src/menubar/MenubarGroup.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/menubar/MenubarItem.vue b/vue/primitives/src/menubar/MenubarItem.vue new file mode 100644 index 0000000..671cd8e --- /dev/null +++ b/vue/primitives/src/menubar/MenubarItem.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/vue/primitives/src/menubar/MenubarItemIndicator.vue b/vue/primitives/src/menubar/MenubarItemIndicator.vue new file mode 100644 index 0000000..dce9565 --- /dev/null +++ b/vue/primitives/src/menubar/MenubarItemIndicator.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/menubar/MenubarLabel.vue b/vue/primitives/src/menubar/MenubarLabel.vue new file mode 100644 index 0000000..d15ec5f --- /dev/null +++ b/vue/primitives/src/menubar/MenubarLabel.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/menubar/MenubarMenu.vue b/vue/primitives/src/menubar/MenubarMenu.vue new file mode 100644 index 0000000..80f29a3 --- /dev/null +++ b/vue/primitives/src/menubar/MenubarMenu.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/vue/primitives/src/menubar/MenubarPortal.vue b/vue/primitives/src/menubar/MenubarPortal.vue new file mode 100644 index 0000000..b2eb31a --- /dev/null +++ b/vue/primitives/src/menubar/MenubarPortal.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/menubar/MenubarRadioGroup.vue b/vue/primitives/src/menubar/MenubarRadioGroup.vue new file mode 100644 index 0000000..a82fe52 --- /dev/null +++ b/vue/primitives/src/menubar/MenubarRadioGroup.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/vue/primitives/src/menubar/MenubarRadioItem.vue b/vue/primitives/src/menubar/MenubarRadioItem.vue new file mode 100644 index 0000000..c206501 --- /dev/null +++ b/vue/primitives/src/menubar/MenubarRadioItem.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/vue/primitives/src/menubar/MenubarRoot.vue b/vue/primitives/src/menubar/MenubarRoot.vue new file mode 100644 index 0000000..40ed22a --- /dev/null +++ b/vue/primitives/src/menubar/MenubarRoot.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/vue/primitives/src/menubar/MenubarSeparator.vue b/vue/primitives/src/menubar/MenubarSeparator.vue new file mode 100644 index 0000000..103e646 --- /dev/null +++ b/vue/primitives/src/menubar/MenubarSeparator.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/menubar/MenubarSub.vue b/vue/primitives/src/menubar/MenubarSub.vue new file mode 100644 index 0000000..d140014 --- /dev/null +++ b/vue/primitives/src/menubar/MenubarSub.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/vue/primitives/src/menubar/MenubarSubContent.vue b/vue/primitives/src/menubar/MenubarSubContent.vue new file mode 100644 index 0000000..daf1e27 --- /dev/null +++ b/vue/primitives/src/menubar/MenubarSubContent.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/vue/primitives/src/menubar/MenubarSubTrigger.vue b/vue/primitives/src/menubar/MenubarSubTrigger.vue new file mode 100644 index 0000000..2536df4 --- /dev/null +++ b/vue/primitives/src/menubar/MenubarSubTrigger.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/vue/primitives/src/menubar/MenubarTrigger.vue b/vue/primitives/src/menubar/MenubarTrigger.vue new file mode 100644 index 0000000..f53d828 --- /dev/null +++ b/vue/primitives/src/menubar/MenubarTrigger.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/vue/primitives/src/menubar/context.ts b/vue/primitives/src/menubar/context.ts new file mode 100644 index 0000000..54d4b6f --- /dev/null +++ b/vue/primitives/src/menubar/context.ts @@ -0,0 +1,32 @@ +import type { ComputedRef, Ref, ShallowRef } from 'vue'; +import type { Direction } from '../config-provider'; + +import { useContextFactory } from '@robonen/vue'; + +export interface MenubarRootContext { + value: Ref; + dir: Ref; + loop: Ref; + onMenuOpen: (value: string) => void; + onMenuClose: () => void; + onMenuToggle: (value: string) => void; +} + +export const { + inject: useMenubarRootContext, + provide: provideMenubarRootContext, +} = useContextFactory('MenubarRootContext'); + +export interface MenubarMenuContext { + value: string; + triggerId: ComputedRef; + contentId: ComputedRef; + triggerRef: ShallowRef; + onTriggerChange: (el: HTMLElement | null) => void; + wasKeyboardTriggerOpenRef: Ref; +} + +export const { + inject: useMenubarMenuContext, + provide: provideMenubarMenuContext, +} = useContextFactory('MenubarMenuContext'); diff --git a/vue/primitives/src/menubar/index.ts b/vue/primitives/src/menubar/index.ts new file mode 100644 index 0000000..490f39e --- /dev/null +++ b/vue/primitives/src/menubar/index.ts @@ -0,0 +1,50 @@ +export { default as MenubarRoot } from './MenubarRoot.vue'; +export type { MenubarRootProps, MenubarRootEmits } from './MenubarRoot.vue'; + +export { default as MenubarMenu } from './MenubarMenu.vue'; +export type { MenubarMenuProps } from './MenubarMenu.vue'; + +export { default as MenubarTrigger } from './MenubarTrigger.vue'; +export type { MenubarTriggerProps } from './MenubarTrigger.vue'; + +export { default as MenubarContent } from './MenubarContent.vue'; +export type { MenubarContentProps, MenubarContentEmits } from './MenubarContent.vue'; + +export { default as MenubarPortal } from './MenubarPortal.vue'; +export type { MenubarPortalProps } from './MenubarPortal.vue'; + +export { default as MenubarArrow } from './MenubarArrow.vue'; +export type { MenubarArrowProps } from './MenubarArrow.vue'; + +export { default as MenubarSeparator } from './MenubarSeparator.vue'; +export type { MenubarSeparatorProps } from './MenubarSeparator.vue'; + +export { default as MenubarLabel } from './MenubarLabel.vue'; +export type { MenubarLabelProps } from './MenubarLabel.vue'; + +export { default as MenubarGroup } from './MenubarGroup.vue'; +export type { MenubarGroupProps } from './MenubarGroup.vue'; + +export { default as MenubarItem } from './MenubarItem.vue'; +export type { MenubarItemProps, MenubarItemEmits } from './MenubarItem.vue'; + +export { default as MenubarCheckboxItem } from './MenubarCheckboxItem.vue'; +export type { MenubarCheckboxItemProps, MenubarCheckboxItemEmits } from './MenubarCheckboxItem.vue'; + +export { default as MenubarRadioGroup } from './MenubarRadioGroup.vue'; +export type { MenubarRadioGroupProps, MenubarRadioGroupEmits } from './MenubarRadioGroup.vue'; + +export { default as MenubarRadioItem } from './MenubarRadioItem.vue'; +export type { MenubarRadioItemProps, MenubarRadioItemEmits } from './MenubarRadioItem.vue'; + +export { default as MenubarItemIndicator } from './MenubarItemIndicator.vue'; +export type { MenubarItemIndicatorProps } from './MenubarItemIndicator.vue'; + +export { default as MenubarSub } from './MenubarSub.vue'; +export type { MenubarSubProps, MenubarSubEmits } from './MenubarSub.vue'; + +export { default as MenubarSubTrigger } from './MenubarSubTrigger.vue'; +export type { MenubarSubTriggerProps } from './MenubarSubTrigger.vue'; + +export { default as MenubarSubContent } from './MenubarSubContent.vue'; +export type { MenubarSubContentProps, MenubarSubContentEmits } from './MenubarSubContent.vue';