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:
@@ -0,0 +1,90 @@
|
||||
<script lang="ts">
|
||||
import type { PrimitiveProps } from '../primitive';
|
||||
|
||||
export interface ContextMenuTriggerProps extends PrimitiveProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onScopeDispose, shallowRef } from 'vue';
|
||||
|
||||
import { useForwardExpose } from '@robonen/vue';
|
||||
import { MenuAnchor, useMenuContext } from '../menu';
|
||||
import { Primitive } from '../primitive';
|
||||
import { useContextMenuRootContext } from './context';
|
||||
|
||||
const { disabled = false, as = 'span', asChild } = defineProps<ContextMenuTriggerProps>();
|
||||
|
||||
const menuCtx = useMenuContext();
|
||||
const ctxMenuCtx = useContextMenuRootContext();
|
||||
const { forwardRef } = useForwardExpose();
|
||||
|
||||
const point = shallowRef({ x: 0, y: 0 });
|
||||
const virtualEl = computed(() => ({
|
||||
getBoundingClientRect: () => ({
|
||||
x: point.value.x,
|
||||
y: point.value.y,
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: point.value.y,
|
||||
right: point.value.x,
|
||||
bottom: point.value.y,
|
||||
left: point.value.x,
|
||||
toJSON: () => {},
|
||||
}),
|
||||
}));
|
||||
|
||||
let longPressTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
const LONG_PRESS_DELAY = 700;
|
||||
|
||||
function clearLongPress() {
|
||||
clearTimeout(longPressTimer);
|
||||
}
|
||||
|
||||
onScopeDispose(clearLongPress);
|
||||
|
||||
function handleContextMenu(event: MouseEvent) {
|
||||
if (disabled) return;
|
||||
clearLongPress();
|
||||
point.value = { x: event.clientX, y: event.clientY };
|
||||
event.preventDefault();
|
||||
ctxMenuCtx.onOpenChange(true);
|
||||
}
|
||||
|
||||
function handlePointerDown(event: PointerEvent) {
|
||||
if (disabled || event.button !== 0) return;
|
||||
if (event.pointerType !== 'touch') return;
|
||||
clearLongPress();
|
||||
longPressTimer = setTimeout(() => {
|
||||
point.value = { x: event.clientX, y: event.clientY };
|
||||
ctxMenuCtx.onOpenChange(true);
|
||||
}, LONG_PRESS_DELAY);
|
||||
}
|
||||
|
||||
function handlePointerCancel() {
|
||||
clearLongPress();
|
||||
}
|
||||
|
||||
function handlePointerUp() {
|
||||
clearLongPress();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenuAnchor as-child :reference="virtualEl">
|
||||
<Primitive
|
||||
:ref="forwardRef"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:data-state="menuCtx.open.value ? 'open' : 'closed'"
|
||||
:data-disabled="disabled ? '' : undefined"
|
||||
@contextmenu="handleContextMenu"
|
||||
@pointerdown="handlePointerDown"
|
||||
@pointercancel="handlePointerCancel"
|
||||
@pointerup="handlePointerUp"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</MenuAnchor>
|
||||
</template>
|
||||
Reference in New Issue
Block a user