From a06b61e5781a1c31571b027dbaee8b5d7244d325 Mon Sep 17 00:00:00 2001 From: Shehab Rahal Date: Tue, 7 Nov 2023 01:23:12 +0800 Subject: [PATCH 01/15] feat: context-menu component --- README.md | 2 +- package.json | 2 + packages/components/context-menu/README.md | 14 + .../components/context-menu/build.config.ts | 7 + packages/components/context-menu/package.json | 54 +++ .../context-menu/src/context-menu-arrow.ts | 41 ++ .../src/context-menu-checkbox-item.ts | 41 ++ .../context-menu/src/context-menu-content.ts | 68 +++ .../context-menu/src/context-menu-group.ts | 41 ++ .../src/context-menu-item-indicator.ts | 41 ++ .../context-menu/src/context-menu-item.ts | 41 ++ .../context-menu/src/context-menu-label.ts | 41 ++ .../context-menu/src/context-menu-portal.ts | 35 ++ .../src/context-menu-radio-group.ts | 41 ++ .../src/context-menu-radio-item.ts | 41 ++ .../src/context-menu-separator.ts | 41 ++ .../src/context-menu-sub-content.ts | 52 +++ .../src/context-menu-sub-trigger.ts | 41 ++ .../context-menu/src/context-menu-sub.ts | 52 +++ .../context-menu/src/context-menu-trigger.ts | 104 +++++ .../context-menu/src/context-menu.ts | 49 +++ packages/components/context-menu/src/index.ts | 101 +++++ packages/components/context-menu/src/props.ts | 387 ++++++++++++++++++ packages/components/context-menu/src/utils.ts | 3 + .../components/context-menu/tsconfig.json | 8 + packages/core/menu/src/index.ts | 22 + playground/nuxt3/package.json | 1 + playground/vue3/package.json | 1 + vitest.config.ts | 1 + 29 files changed, 1372 insertions(+), 1 deletion(-) create mode 100644 packages/components/context-menu/README.md create mode 100644 packages/components/context-menu/build.config.ts create mode 100644 packages/components/context-menu/package.json create mode 100644 packages/components/context-menu/src/context-menu-arrow.ts create mode 100644 packages/components/context-menu/src/context-menu-checkbox-item.ts create mode 100644 packages/components/context-menu/src/context-menu-content.ts create mode 100644 packages/components/context-menu/src/context-menu-group.ts create mode 100644 packages/components/context-menu/src/context-menu-item-indicator.ts create mode 100644 packages/components/context-menu/src/context-menu-item.ts create mode 100644 packages/components/context-menu/src/context-menu-label.ts create mode 100644 packages/components/context-menu/src/context-menu-portal.ts create mode 100644 packages/components/context-menu/src/context-menu-radio-group.ts create mode 100644 packages/components/context-menu/src/context-menu-radio-item.ts create mode 100644 packages/components/context-menu/src/context-menu-separator.ts create mode 100644 packages/components/context-menu/src/context-menu-sub-content.ts create mode 100644 packages/components/context-menu/src/context-menu-sub-trigger.ts create mode 100644 packages/components/context-menu/src/context-menu-sub.ts create mode 100644 packages/components/context-menu/src/context-menu-trigger.ts create mode 100644 packages/components/context-menu/src/context-menu.ts create mode 100644 packages/components/context-menu/src/index.ts create mode 100644 packages/components/context-menu/src/props.ts create mode 100644 packages/components/context-menu/src/utils.ts create mode 100644 packages/components/context-menu/tsconfig.json diff --git a/README.md b/README.md index 70196211a..6e910a8d4 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Enter the component you want most in the components, leave the emojis and follow | [Avatar](https://oku-ui.com/primitives/components/avatar) | Version | Downloads | Website | | [Checkbox](https://oku-ui.com/primitives/components/checkbox) | Version | Downloads | Website | | [Collapsible](https://oku-ui.com/primitives/components/collapsible) | Version | Downloads | Website | -| [Context Menu](https://github.com/oku-ui/primitives/issues/8) | A menu that appears when a user interacts with an element's trigger | Not Started | - | +| [Context Menu](https://oku-ui.com/primitives/components/context-menu) | Version | Downloads | Website | | [Dialog](https://oku-ui.com/primitives/components/dialog) | Version | Downloads | Website | | [Dropdown Menu](https://github.com/oku-ui/primitives/issues/10) | A menu that appears when a user interacts with an element's trigger | Not Started | - | | [Form](https://github.com/oku-ui/primitives/issues/11) | A group of form controls | Not Started | - | diff --git a/package.json b/package.json index d753ad106..39d2b9097 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@oku-ui/checkbox": "workspace:^", "@oku-ui/collapsible": "workspace:^", "@oku-ui/collection": "workspace:^", + "@oku-ui/context-menu": "workspace:^", "@oku-ui/dialog": "workspace:^", "@oku-ui/direction": "workspace:^", "@oku-ui/dismissable-layer": "workspace:^", @@ -135,6 +136,7 @@ "@oku-ui/checkbox": "workspace:^", "@oku-ui/collapsible": "workspace:^", "@oku-ui/collection": "workspace:^", + "@oku-ui/context-menu": "workspace:^", "@oku-ui/dialog": "workspace:^", "@oku-ui/direction": "workspace:^", "@oku-ui/dismissable-layer": "workspace:^", diff --git a/packages/components/context-menu/README.md b/packages/components/context-menu/README.md new file mode 100644 index 000000000..9db8c84fc --- /dev/null +++ b/packages/components/context-menu/README.md @@ -0,0 +1,14 @@ +# Scroll Area +Displays a menu located at the pointer, triggered by a right-click or a long-press. + +![@oku-ui/context-menu](./../../../.github/assets/og/oku-context-menu.jpg) + +Version | Downloads | Website + +## Installation + +```sh +$ pnpm add @oku-ui/context-menu +``` + +[Documentation](https://oku-ui.com/primitives/components/context-menu) diff --git a/packages/components/context-menu/build.config.ts b/packages/components/context-menu/build.config.ts new file mode 100644 index 000000000..e20ccb0a1 --- /dev/null +++ b/packages/components/context-menu/build.config.ts @@ -0,0 +1,7 @@ +import { defineBuildConfig } from 'unbuild' + +const isClean = (process.env.CLEAN || 'false') === 'true' +export default defineBuildConfig({ + declaration: true, + clean: isClean, +}) diff --git a/packages/components/context-menu/package.json b/packages/components/context-menu/package.json new file mode 100644 index 000000000..5a1604828 --- /dev/null +++ b/packages/components/context-menu/package.json @@ -0,0 +1,54 @@ +{ + "name": "@oku-ui/context-menu", + "type": "module", + "version": "0.5.0", + "license": "MIT", + "source": "src/index.ts", + "funding": "https://github.com/sponsors/productdevbook", + "homepage": "https://oku-ui.com/primitives", + "repository": { + "type": "git", + "url": "git+https://github.com/oku-ui/primitives.git", + "directory": "packages/components/context-menu" + }, + "bugs": { + "url": "https://github.com/oku-ui/primitives/issues" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs" + } + }, + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "engines": { + "node": ">=18" + }, + "scripts": { + "build": "unbuild", + "dev": "unbuild --stub", + "clean": "rimraf ./dist && rimraf ./node_modules" + }, + "peerDependencies": { + "vue": "^3.3.0" + }, + "dependencies": { + "@floating-ui/vue": "^1.0.2", + "@oku-ui/direction": "latest", + "@oku-ui/menu": "latest", + "@oku-ui/presence": "latest", + "@oku-ui/primitive": "latest", + "@oku-ui/provide": "latest", + "@oku-ui/use-composable": "latest" + }, + "devDependencies": { + "tsconfig": "workspace:^" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/components/context-menu/src/context-menu-arrow.ts b/packages/components/context-menu/src/context-menu-arrow.ts new file mode 100644 index 000000000..f5d23ea0a --- /dev/null +++ b/packages/components/context-menu/src/context-menu-arrow.ts @@ -0,0 +1,41 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit, useForwardRef } from '@oku-ui/use-composable' +import { OkuMenuArrow } from '@oku-ui/menu' +import { CONTEXT_MENU_ARROW_NAME, contextMenuArrowProps, scopedContextMenuProps, useMenuScope } from './props' +import type { ContextMenuArrowNativeElement } from './props' + +const contextMenuArrow = defineComponent({ + name: CONTEXT_MENU_ARROW_NAME, + components: { + OkuMenuArrow, + }, + inheritAttrs: false, + props: { + ...contextMenuArrowProps.props, + ...scopedContextMenuProps, + }, + emits: contextMenuArrowProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuContextMenu, + ...arrowProps + } = toRefs(props) + + const _other = reactive(arrowProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const menuScope = useMenuScope(scopeOkuContextMenu.value) + + return () => h(OkuMenuArrow, { + ...menuScope, + ...mergeProps(attrs, otherProps), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuContextMenuArrow = contextMenuArrow as typeof contextMenuArrow & +(new () => { $props: ContextMenuArrowNativeElement }) diff --git a/packages/components/context-menu/src/context-menu-checkbox-item.ts b/packages/components/context-menu/src/context-menu-checkbox-item.ts new file mode 100644 index 000000000..c2a85d56b --- /dev/null +++ b/packages/components/context-menu/src/context-menu-checkbox-item.ts @@ -0,0 +1,41 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit, useForwardRef } from '@oku-ui/use-composable' +import { OkuMenuCheckboxItem } from '@oku-ui/menu' +import { CONTEXT_MENU_CHECKBOX_ITEM_NAME, contextMenuCheckboxItemProps, scopedContextMenuProps, useMenuScope } from './props' +import type { ContextMenuCheckboxItemNativeElement } from './props' + +const contextMenuCheckboxItem = defineComponent({ + name: CONTEXT_MENU_CHECKBOX_ITEM_NAME, + components: { + OkuMenuCheckboxItem, + }, + inheritAttrs: false, + props: { + ...contextMenuCheckboxItemProps.props, + ...scopedContextMenuProps, + }, + emits: contextMenuCheckboxItemProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuContextMenu, + ...checkboxItemProps + } = toRefs(props) + + const _other = reactive(checkboxItemProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const menuScope = useMenuScope(scopeOkuContextMenu.value) + + return () => h(OkuMenuCheckboxItem, { + ...menuScope, + ...mergeProps(attrs, otherProps), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuContextMenuCheckboxItem = contextMenuCheckboxItem as typeof contextMenuCheckboxItem & +(new () => { $props: ContextMenuCheckboxItemNativeElement }) diff --git a/packages/components/context-menu/src/context-menu-content.ts b/packages/components/context-menu/src/context-menu-content.ts new file mode 100644 index 000000000..1b603a375 --- /dev/null +++ b/packages/components/context-menu/src/context-menu-content.ts @@ -0,0 +1,68 @@ +import { defineComponent, h, mergeProps, reactive, ref, toRefs } from 'vue' +import { reactiveOmit, useForwardRef } from '@oku-ui/use-composable' +import { OkuMenuContent } from '@oku-ui/menu' +import { CONTEXT_MENU_CONTENT_NAME, contextMenuContentProps, scopedContextMenuProps, useContextMenuInject, useMenuScope } from './props' + +const contextMenuContent = defineComponent({ + name: CONTEXT_MENU_CONTENT_NAME, + components: { + OkuMenuContent, + }, + inheritAttrs: false, + props: { + ...contextMenuContentProps.props, + ...scopedContextMenuProps, + }, + emits: contextMenuContentProps.emits, + setup(props, { attrs, emit, slots }) { + const { + scopeOkuContextMenu, + ...contentProps + } = toRefs(props) + + const _other = reactive(contentProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const inject = useContextMenuInject(CONTEXT_MENU_CONTENT_NAME, scopeOkuContextMenu.value) + const menuScope = useMenuScope(scopeOkuContextMenu.value) + const hasInteractedOutsideRef = ref(false) + + return () => h(OkuMenuContent, { + ...menuScope, + ...mergeProps(attrs, otherProps), + ref: forwardedRef, + side: 'right', + sideOffset: 2, + align: 'start', + onCloseAutoFocus: (event) => { + emit('closeAutoFocus', event) + + if (!event.defaultPrevented && hasInteractedOutsideRef.value) + event.preventDefault() + + hasInteractedOutsideRef.value = false + }, + onInteractOutside: (event) => { + emit('interactOutside', event) + + if (!event.defaultPrevented && !inject.modal) + hasInteractedOutsideRef.value = true + }, + style: { + ...attrs.style as any, + // re-namespace exposed content custom properties + ...{ + '--oku-context-menu-content-transform-origin': 'var(--oku-popper-transform-origin)', + '--oku-context-menu-content-available-width': 'var(--oku-popper-available-width)', + '--oku-context-menu-content-available-height': 'var(--oku-popper-available-height)', + '--oku-context-menu-trigger-width': 'var(--oku-popper-anchor-width)', + '--oku-context-menu-trigger-height': 'var(--oku-popper-anchor-height)', + }, + }, + }, slots) + }, +}) + +export const OkuContextMenuContent = contextMenuContent diff --git a/packages/components/context-menu/src/context-menu-group.ts b/packages/components/context-menu/src/context-menu-group.ts new file mode 100644 index 000000000..c490ab3c9 --- /dev/null +++ b/packages/components/context-menu/src/context-menu-group.ts @@ -0,0 +1,41 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit, useForwardRef } from '@oku-ui/use-composable' +import { OkuMenuGroup } from '@oku-ui/menu' +import { CONTEXT_MENU_GROUP_NAME, contextMenuGroupProps, scopedContextMenuProps, useMenuScope } from './props' +import type { ContextMenuGroupNativeElement } from './props' + +const contextMenuGroup = defineComponent({ + name: CONTEXT_MENU_GROUP_NAME, + components: { + OkuMenuGroup, + }, + inheritAttrs: false, + props: { + ...contextMenuGroupProps.props, + ...scopedContextMenuProps, + }, + emits: contextMenuGroupProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuContextMenu, + ...groupProps + } = toRefs(props) + + const _other = reactive(groupProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const menuScope = useMenuScope(scopeOkuContextMenu.value) + + return () => h(OkuMenuGroup, { + ...menuScope, + ...mergeProps(attrs, otherProps), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuContextMenuGroup = contextMenuGroup as typeof contextMenuGroup & +(new () => { $props: ContextMenuGroupNativeElement }) diff --git a/packages/components/context-menu/src/context-menu-item-indicator.ts b/packages/components/context-menu/src/context-menu-item-indicator.ts new file mode 100644 index 000000000..4083e05c9 --- /dev/null +++ b/packages/components/context-menu/src/context-menu-item-indicator.ts @@ -0,0 +1,41 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit, useForwardRef } from '@oku-ui/use-composable' +import { OkuMenuItemIndicator } from '@oku-ui/menu' +import { CONTEXT_MENU_ITEM_INDICATOR_NAME, contextMenuItemIndicatorProps, scopedContextMenuProps, useMenuScope } from './props' +import type { ContextMenuItemIndicatorNativeElement } from './props' + +const contextMenuItemIndicator = defineComponent({ + name: CONTEXT_MENU_ITEM_INDICATOR_NAME, + components: { + OkuMenuItemIndicator, + }, + inheritAttrs: false, + props: { + ...contextMenuItemIndicatorProps.props, + ...scopedContextMenuProps, + }, + emits: contextMenuItemIndicatorProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuContextMenu, + ...itemIndicatorProps + } = toRefs(props) + + const _other = reactive(itemIndicatorProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const menuScope = useMenuScope(scopeOkuContextMenu.value) + + return () => h(OkuMenuItemIndicator, { + ...menuScope, + ...mergeProps(attrs, otherProps), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuContextMenuItemIndicator = contextMenuItemIndicator as typeof contextMenuItemIndicator & +(new () => { $props: ContextMenuItemIndicatorNativeElement }) diff --git a/packages/components/context-menu/src/context-menu-item.ts b/packages/components/context-menu/src/context-menu-item.ts new file mode 100644 index 000000000..8e0642d2c --- /dev/null +++ b/packages/components/context-menu/src/context-menu-item.ts @@ -0,0 +1,41 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit, useForwardRef } from '@oku-ui/use-composable' +import { OkuMenuItem } from '@oku-ui/menu' +import { CONTEXT_MENU_ITEM_NAME, contextMenuItemProps, scopedContextMenuProps, useMenuScope } from './props' +import type { ContextMenuItemNativeElement } from './props' + +const contextMenuItem = defineComponent({ + name: CONTEXT_MENU_ITEM_NAME, + components: { + OkuMenuItem, + }, + inheritAttrs: false, + props: { + ...contextMenuItemProps.props, + ...scopedContextMenuProps, + }, + emits: contextMenuItemProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuContextMenu, + ...itemProps + } = toRefs(props) + + const _other = reactive(itemProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const menuScope = useMenuScope(scopeOkuContextMenu.value) + + return () => h(OkuMenuItem, { + ...menuScope, + ...mergeProps(attrs, otherProps), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuContextMenuItem = contextMenuItem as typeof contextMenuItem & +(new () => { $props: ContextMenuItemNativeElement }) diff --git a/packages/components/context-menu/src/context-menu-label.ts b/packages/components/context-menu/src/context-menu-label.ts new file mode 100644 index 000000000..45b58bebd --- /dev/null +++ b/packages/components/context-menu/src/context-menu-label.ts @@ -0,0 +1,41 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit, useForwardRef } from '@oku-ui/use-composable' +import { OkuMenuLabel } from '@oku-ui/menu' +import { CONTEXT_MENU_LABEL_NAME, contextMenuLabelProps, scopedContextMenuProps, useMenuScope } from './props' +import type { ContextMenuLabelNativeElement } from './props' + +const contextMenuLabel = defineComponent({ + name: CONTEXT_MENU_LABEL_NAME, + components: { + OkuMenuLabel, + }, + inheritAttrs: false, + props: { + ...contextMenuLabelProps.props, + ...scopedContextMenuProps, + }, + emits: contextMenuLabelProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuContextMenu, + ...labelProps + } = toRefs(props) + + const _other = reactive(labelProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const menuScope = useMenuScope(scopeOkuContextMenu.value) + + return () => h(OkuMenuLabel, { + ...menuScope, + ...mergeProps(attrs, otherProps), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuContextMenuLabel = contextMenuLabel as typeof contextMenuLabel & +(new () => { $props: ContextMenuLabelNativeElement }) diff --git a/packages/components/context-menu/src/context-menu-portal.ts b/packages/components/context-menu/src/context-menu-portal.ts new file mode 100644 index 000000000..8fc7b33bc --- /dev/null +++ b/packages/components/context-menu/src/context-menu-portal.ts @@ -0,0 +1,35 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit } from '@oku-ui/use-composable' +import { OkuMenuPortal } from '@oku-ui/menu' +import { CONTEXT_MENU_PORTAL_NAME, contextMenuPortalProps, scopedContextMenuProps, useMenuScope } from './props' + +const contextMenuPortal = defineComponent({ + name: CONTEXT_MENU_PORTAL_NAME, + components: { + OkuMenuPortal, + }, + inheritAttrs: false, + props: { + ...contextMenuPortalProps.props, + ...scopedContextMenuProps, + }, + emits: contextMenuPortalProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuContextMenu, + ...portalProps + } = toRefs(props) + + const _other = reactive(portalProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const menuScope = useMenuScope(scopeOkuContextMenu.value) + + return () => h(OkuMenuPortal, { + ...menuScope, + ...mergeProps(attrs, otherProps), + }, slots) + }, +}) + +export const OkuContextMenuPortal = contextMenuPortal diff --git a/packages/components/context-menu/src/context-menu-radio-group.ts b/packages/components/context-menu/src/context-menu-radio-group.ts new file mode 100644 index 000000000..688c42c5b --- /dev/null +++ b/packages/components/context-menu/src/context-menu-radio-group.ts @@ -0,0 +1,41 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit, useForwardRef } from '@oku-ui/use-composable' +import { OkuMenuRadioGroup } from '@oku-ui/menu' +import { CONTEXT_MENU_RADIO_GROUP_NAME, contextMenuRadioGroupProps, scopedContextMenuProps, useMenuScope } from './props' +import type { ContextMenuRadioGroupNativeElement } from './props' + +const contextMenuRadioGroup = defineComponent({ + name: CONTEXT_MENU_RADIO_GROUP_NAME, + components: { + OkuMenuRadioGroup, + }, + inheritAttrs: false, + props: { + ...contextMenuRadioGroupProps.props, + ...scopedContextMenuProps, + }, + emits: contextMenuRadioGroupProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuContextMenu, + ...radioGroupProps + } = toRefs(props) + + const _other = reactive(radioGroupProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const menuScope = useMenuScope(scopeOkuContextMenu.value) + + return () => h(OkuMenuRadioGroup, { + ...menuScope, + ...mergeProps(attrs, otherProps), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuContextMenuRadioGroup = contextMenuRadioGroup as typeof contextMenuRadioGroup & +(new () => { $props: ContextMenuRadioGroupNativeElement }) diff --git a/packages/components/context-menu/src/context-menu-radio-item.ts b/packages/components/context-menu/src/context-menu-radio-item.ts new file mode 100644 index 000000000..5a06785cc --- /dev/null +++ b/packages/components/context-menu/src/context-menu-radio-item.ts @@ -0,0 +1,41 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit, useForwardRef } from '@oku-ui/use-composable' +import { OkuMenuRadioItem } from '@oku-ui/menu' +import { CONTEXT_MENU_RADIO_ITEM_NAME, contextMenuRadioItemProps, scopedContextMenuProps, useMenuScope } from './props' +import type { ContextMenuRadioItemNativeElement } from './props' + +const contextMenuRadioItem = defineComponent({ + name: CONTEXT_MENU_RADIO_ITEM_NAME, + components: { + OkuMenuRadioItem, + }, + inheritAttrs: false, + props: { + ...contextMenuRadioItemProps.props, + ...scopedContextMenuProps, + }, + emits: contextMenuRadioItemProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuContextMenu, + ...radioItemProps + } = toRefs(props) + + const _other = reactive(radioItemProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const menuScope = useMenuScope(scopeOkuContextMenu.value) + + return () => h(OkuMenuRadioItem, { + ...menuScope, + ...mergeProps(attrs, otherProps), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuContextMenuRadioItem = contextMenuRadioItem as typeof contextMenuRadioItem & +(new () => { $props: ContextMenuRadioItemNativeElement }) diff --git a/packages/components/context-menu/src/context-menu-separator.ts b/packages/components/context-menu/src/context-menu-separator.ts new file mode 100644 index 000000000..9d9cbaf91 --- /dev/null +++ b/packages/components/context-menu/src/context-menu-separator.ts @@ -0,0 +1,41 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit, useForwardRef } from '@oku-ui/use-composable' +import { OkuMenuSeparator } from '@oku-ui/menu' +import { CONTEXT_MENU_SEPARATOR_NAME, contextMenuSeparatorProps, scopedContextMenuProps, useMenuScope } from './props' +import type { ContextMenuSeparatorNativeElement } from './props' + +const contextMenuSeparator = defineComponent({ + name: CONTEXT_MENU_SEPARATOR_NAME, + components: { + OkuMenuSeparator, + }, + inheritAttrs: false, + props: { + ...contextMenuSeparatorProps.props, + ...scopedContextMenuProps, + }, + emits: contextMenuSeparatorProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuContextMenu, + ...separatorProps + } = toRefs(props) + + const _other = reactive(separatorProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const menuScope = useMenuScope(scopeOkuContextMenu.value) + + return () => h(OkuMenuSeparator, { + ...menuScope, + ...mergeProps(attrs, otherProps), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuContextMenuSeparator = contextMenuSeparator as typeof contextMenuSeparator & +(new () => { $props: ContextMenuSeparatorNativeElement }) diff --git a/packages/components/context-menu/src/context-menu-sub-content.ts b/packages/components/context-menu/src/context-menu-sub-content.ts new file mode 100644 index 000000000..7de1d0362 --- /dev/null +++ b/packages/components/context-menu/src/context-menu-sub-content.ts @@ -0,0 +1,52 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit, useForwardRef } from '@oku-ui/use-composable' +import { OkuMenuSubContent } from '@oku-ui/menu' +import { CONTEXT_MENU_SUB_CONTENT_NAME, contextMenuSubContentProps, scopedContextMenuProps, useMenuScope } from './props' +import type { ContextMenuSubContentNativeElement } from './props' + +const contextMenuSubContent = defineComponent({ + name: CONTEXT_MENU_SUB_CONTENT_NAME, + components: { + OkuMenuSubContent, + }, + inheritAttrs: false, + props: { + ...contextMenuSubContentProps.props, + ...scopedContextMenuProps, + }, + emits: contextMenuSubContentProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuContextMenu, + ...subContentProps + } = toRefs(props) + + const _other = reactive(subContentProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const menuScope = useMenuScope(scopeOkuContextMenu.value) + + return () => h(OkuMenuSubContent, { + ...menuScope, + ...mergeProps(attrs, otherProps), + ref: forwardedRef, + style: { + ...attrs.style as any, + // re-namespace exposed content custom properties + ...{ + '--oku-context-menu-content-transform-origin': 'var(--oku-popper-transform-origin)', + '--oku-context-menu-content-available-width': 'var(--oku-popper-available-width)', + '--oku-context-menu-content-available-height': 'var(--oku-popper-available-height)', + '--oku-context-menu-trigger-width': 'var(--oku-popper-anchor-width)', + '--oku-context-menu-trigger-height': 'var(--oku-popper-anchor-height)', + }, + }, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuContextMenuSubContent = contextMenuSubContent as typeof contextMenuSubContent & +(new () => { $props: ContextMenuSubContentNativeElement }) diff --git a/packages/components/context-menu/src/context-menu-sub-trigger.ts b/packages/components/context-menu/src/context-menu-sub-trigger.ts new file mode 100644 index 000000000..b13697ac1 --- /dev/null +++ b/packages/components/context-menu/src/context-menu-sub-trigger.ts @@ -0,0 +1,41 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit, useForwardRef } from '@oku-ui/use-composable' +import { OkuMenuSubTrigger } from '@oku-ui/menu' +import { CONTEXT_MENU_SUB_TRIGGER_NAME, contextMenuSubTriggerProps, scopedContextMenuProps, useMenuScope } from './props' +import type { ContextMenuSubTriggerNativeElement } from './props' + +const contextMenuSubTrigger = defineComponent({ + name: CONTEXT_MENU_SUB_TRIGGER_NAME, + components: { + OkuMenuSubTrigger, + }, + inheritAttrs: false, + props: { + ...contextMenuSubTriggerProps.props, + ...scopedContextMenuProps, + }, + emits: contextMenuSubTriggerProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuContextMenu, + ...triggerItemProps + } = toRefs(props) + + const _other = reactive(triggerItemProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const menuScope = useMenuScope(scopeOkuContextMenu.value) + + return () => h(OkuMenuSubTrigger, { + ...menuScope, + ...mergeProps(attrs, otherProps), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuContextMenuSubTrigger = contextMenuSubTrigger as typeof contextMenuSubTrigger & +(new () => { $props: ContextMenuSubTriggerNativeElement }) diff --git a/packages/components/context-menu/src/context-menu-sub.ts b/packages/components/context-menu/src/context-menu-sub.ts new file mode 100644 index 000000000..eb082796a --- /dev/null +++ b/packages/components/context-menu/src/context-menu-sub.ts @@ -0,0 +1,52 @@ +import { computed, defineComponent, h, toRefs, useModel } from 'vue' +import { useControllable } from '@oku-ui/use-composable' +import { OkuMenuSub } from '@oku-ui/menu' +import { CONTEXT_MENU_RADIO_ITEM_NAME, contextMenuSubProps, scopedContextMenuProps, useMenuScope } from './props' + +const contextMenuSub = defineComponent({ + name: CONTEXT_MENU_RADIO_ITEM_NAME, + components: { + OkuMenuSub, + }, + inheritAttrs: false, + props: { + ...contextMenuSubProps.props, + ...scopedContextMenuProps, + }, + emits: contextMenuSubProps.emits, + setup(props, { attrs, emit, slots }) { + const { + scopeOkuContextMenu, + open: openProp, + defaultOpen, + } = toRefs(props) + + const menuScope = useMenuScope(scopeOkuContextMenu.value) + + const modelValue = useModel(props, 'modelValue') + const proxyChecked = computed({ + get: () => modelValue.value !== undefined ? modelValue.value : openProp.value !== undefined ? openProp.value : undefined, + set: () => { + }, + }) + + const { state, updateValue } = useControllable({ + prop: computed(() => proxyChecked.value), + defaultProp: computed(() => defaultOpen.value), + onChange: (result: any) => { + modelValue.value = result + emit('openChange', result) + }, + // initialValue: true, + }) + + return () => h(OkuMenuSub, { + ...attrs, + ...menuScope, + open: state.value, + onOpenChange: open => updateValue(open), + }, slots) + }, +}) + +export const OkuContextMenuSub = contextMenuSub diff --git a/packages/components/context-menu/src/context-menu-trigger.ts b/packages/components/context-menu/src/context-menu-trigger.ts new file mode 100644 index 000000000..a62752b05 --- /dev/null +++ b/packages/components/context-menu/src/context-menu-trigger.ts @@ -0,0 +1,104 @@ +import { Fragment, defineComponent, h, mergeProps, onMounted, reactive, ref, toRefs } from 'vue' +import { Primitive } from '@oku-ui/primitive' +import { reactiveOmit, useForwardRef } from '@oku-ui/use-composable' +import { composeEventHandlers } from '@oku-ui/utils' +import { OkuMenuAnchor } from '@oku-ui/menu' +import { CONTEXT_MENU_TRIGGER_NAME, contextMenuTriggerProps, scopedContextMenuProps, useContextMenuInject, useMenuScope } from './props' +import type { ContextMenuTriggerEmits, ContextMenuTriggerNativeElement, Point } from './props' +import { whenTouchOrPen } from './utils' + +const contextMenuTrigger = defineComponent({ + name: CONTEXT_MENU_TRIGGER_NAME, + components: { + OkuMenuAnchor, + }, + inheritAttrs: false, + props: { + ...contextMenuTriggerProps.props, + ...scopedContextMenuProps, + }, + emits: contextMenuTriggerProps.emits, + setup(props, { attrs, emit, slots }) { + const { + scopeOkuContextMenu, + disabled, + ...triggerProps + } = toRefs(props) + + const _other = reactive(triggerProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const inject = useContextMenuInject(CONTEXT_MENU_TRIGGER_NAME, scopeOkuContextMenu.value) + const menuScope = useMenuScope(scopeOkuContextMenu.value) + const pointRef = ref({ x: 0, y: 0 }) + const virtualRef = ref({ getBoundingClientRect: () => DOMRect.fromRect({ width: 0, height: 0, ...pointRef.value }) }) + const longPressTimerRef = ref(0) + const clearLongPress = () => window.clearTimeout(longPressTimerRef.value) + const handleOpen = (event: MouseEvent | PointerEvent) => { + pointRef.value = { x: event.clientX, y: event.clientY } + inject.onOpenChange(true) + } + + onMounted(() => clearLongPress()) + + onMounted(() => { + if (disabled.value) + clearLongPress() + }) + + return () => h(Fragment, [ + h(OkuMenuAnchor, { + // ...attrs, + ...menuScope, + // ...mergeProps(attrs, otherProps), + virtualRef: virtualRef.value, + }), + + h(Primitive.span, { + 'data-state': inject.open.value ? 'open' : 'closed', + 'data-disabled': disabled.value ? '' : undefined, + ...mergeProps(attrs, otherProps), + 'ref': forwardedRef, + // prevent iOS context menu from appearing + 'style': { WebkitTouchCallout: 'none', ...attrs.style as any }, + // if trigger is disabled, enable the native Context Menu + 'onContextMenu': composeEventHandlers((event) => { + if (disabled.value) + emit('contextMenu', event) + }, (event: any) => { + // clearing the long press here because some platforms already support + // long press to trigger a `contextmenu` event + clearLongPress() + handleOpen(event) + event.preventDefault() + }), + 'onPointerdown': composeEventHandlers((event) => { + if (disabled.value) + emit('pointerdown', event) + }, (event) => { + // clear the long press here in case there's multiple touch points + clearLongPress() + longPressTimerRef.value = window.setTimeout(() => handleOpen(event), 700) + }), + 'onPointermove': composeEventHandlers((event) => { + if (disabled.value) + emit('pointermove', event) + }, whenTouchOrPen(clearLongPress)), + 'onPointercancel': composeEventHandlers((event) => { + if (disabled.value) + emit('pointercancel', event) + }, whenTouchOrPen(clearLongPress)), + 'onPointerup': composeEventHandlers((event) => { + if (disabled.value) + emit('pointerup', event) + }, whenTouchOrPen(clearLongPress)), + }, slots), + ]) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuContextMenuTrigger = contextMenuTrigger as typeof contextMenuTrigger & +(new () => { $props: ContextMenuTriggerNativeElement }) diff --git a/packages/components/context-menu/src/context-menu.ts b/packages/components/context-menu/src/context-menu.ts new file mode 100644 index 000000000..c8408da72 --- /dev/null +++ b/packages/components/context-menu/src/context-menu.ts @@ -0,0 +1,49 @@ +import { defineComponent, h, onMounted, ref, toRefs } from 'vue' +import { OkuMenu } from '@oku-ui/menu' +import { CONTEXT_MENU_NAME, ContextMenuProvider, contextMenuProps, scopedContextMenuProps, useMenuScope } from './props' + +const contextMenu = defineComponent({ + name: CONTEXT_MENU_NAME, + components: { + OkuMenu, + }, + inheritAttrs: false, + props: { + ...contextMenuProps.props, + ...scopedContextMenuProps, + }, + emits: contextMenuProps.emits, + setup(props, { attrs, emit, slots }) { + const { + scopeOkuContextMenu, + dir, + modal, + } = toRefs(props) + + const open = ref(false) + const menuScope = useMenuScope(scopeOkuContextMenu.value) + const handleOpenChangeProp = (open: boolean) => emit('openChange', open) + + const handleOpenChange = (_open: boolean) => { + open.value = _open + handleOpenChangeProp(_open) + } + + ContextMenuProvider({ + scope: scopeOkuContextMenu.value, + open, + onOpenChange: open => handleOpenChange(open), + modal, + }) + + return () => h(OkuMenu, { + ...attrs, + ...menuScope, + dir: dir.value, + open: open.value, + onOpenChange: open => handleOpenChange(open), + }, slots) + }, +}) + +export const OkuContextMenu = contextMenu diff --git a/packages/components/context-menu/src/index.ts b/packages/components/context-menu/src/index.ts new file mode 100644 index 000000000..936c2c27b --- /dev/null +++ b/packages/components/context-menu/src/index.ts @@ -0,0 +1,101 @@ +import type { } from '@floating-ui/vue' + +export { OkuContextMenu } from './context-menu' +export { OkuContextMenuArrow } from './context-menu-arrow' +export { OkuContextMenuCheckboxItem } from './context-menu-checkbox-item' +export { OkuContextMenuContent } from './context-menu-content' +export { OkuContextMenuGroup } from './context-menu-group' +export { OkuContextMenuItemIndicator } from './context-menu-item-indicator' +export { OkuContextMenuItem } from './context-menu-item' +export { OkuContextMenuLabel } from './context-menu-label' +export { OkuContextMenuPortal } from './context-menu-portal' +export { OkuContextMenuRadioGroup } from './context-menu-radio-group' +export { OkuContextMenuRadioItem } from './context-menu-radio-item' +export { OkuContextMenuSeparator } from './context-menu-separator' +export { OkuContextMenuSub } from './context-menu-sub' +export { OkuContextMenuSubContent } from './context-menu-sub-content' +export { OkuContextMenuSubTrigger } from './context-menu-sub-trigger' + +export type { + ContextMenuProps, +} from './props' + +export type { + ContextMenuArrowProps, + ContextMenuArrowElement, + ContextMenuArrowNativeElement, +} from './props' + +export type { + ContextMenuCheckboxItemProps, + ContextMenuCheckboxItemElement, + ContextMenuCheckboxItemNativeElement, +} from './props' + +export type { + ContextMenuContentProps, + ContextMenuContentElement, + ContextMenuContentNativeElement, +} from './props' + +export type { + ContextMenuGroupProps, + ContextMenuGroupElement, + ContextMenuGroupNativeElement, +} from './props' + +export type { + ContextMenuItemIndicatorProps, + ContextMenuItemIndicatorElement, + ContextMenuItemIndicatorNativeElement, +} from './props' + +export type { + ContextMenuItemProps, + ContextMenuItemElement, + ContextMenuItemNativeElement, +} from './props' + +export type { + ContextMenuLabelProps, + ContextMenuLabelElement, + ContextMenuLabelNativeElement, +} from './props' + +export type { + ContextMenuPortalProps, +} from './props' + +export type { + ContextMenuRadioGroupProps, + ContextMenuRadioGroupElement, + ContextMenuRadioGroupNativeElement, +} from './props' + +export type { + ContextMenuRadioItemProps, + ContextMenuRadioItemElement, + ContextMenuRadioItemNativeElement, +} from './props' + +export type { + ContextMenuSeparatorProps, + ContextMenuSeparatorElement, + ContextMenuSeparatorNativeElement, +} from './props' + +export type { + ContextMenuSubProps, +} from './props' + +export type { + ContextMenuSubContentProps, + ContextMenuSubContentElement, + ContextMenuSubContentNativeElement, +} from './props' + +export type { + ContextMenuSubTriggerProps, + ContextMenuSubTriggerElement, + ContextMenuSubTriggerNativeElement, +} from './props' diff --git a/packages/components/context-menu/src/props.ts b/packages/components/context-menu/src/props.ts new file mode 100644 index 000000000..5d168d5db --- /dev/null +++ b/packages/components/context-menu/src/props.ts @@ -0,0 +1,387 @@ +import type { PropType, Ref } from 'vue' +import { primitiveProps, propsOmit } from '@oku-ui/primitive' +import type { OkuElement, PrimitiveProps } from '@oku-ui/primitive' +import { ScopePropObject, createProvideScope } from '@oku-ui/provide' +import type { Scope } from '@oku-ui/provide' +import type { Direction } from '@oku-ui/direction' +import { createMenuScope, menuArrowProps, menuCheckboxItemProps, menuContentProps, menuGroupProps, menuItemIndicatorProps, menuItemProps, menuLabelProps, menuPortalProps, menuRadioGroupProps, menuRadioItemProps, menuSeparatorProps, menuSubContentProps, menuSubTriggerProps } from '@oku-ui/menu' +import type { MenuArrowElement, MenuArrowNativeElement, MenuArrowProps, MenuCheckboxItemElement, MenuCheckboxItemEmits, MenuCheckboxItemNativeElement, MenuCheckboxItemProps, MenuContentElement, MenuContentNativeElement, MenuContentProps, MenuGroupElement, MenuGroupNativeElement, MenuGroupProps, MenuItemElement, MenuItemEmits, MenuItemIndicatorElement, MenuItemIndicatorNativeElement, MenuItemIndicatorProps, MenuItemNativeElement, MenuItemProps, MenuLabelElement, MenuLabelNativeElement, MenuLabelProps, MenuPortalProps, MenuRadioGroupElement, MenuRadioGroupNativeElement, MenuRadioGroupProps, MenuRadioItemElement, MenuRadioItemEmits, MenuRadioItemNativeElement, MenuRadioItemProps, MenuSeparatorElement, MenuSeparatorNativeElement, MenuSeparatorProps, MenuSubContentElement, MenuSubContentEmits, MenuSubContentNativeElement, MenuSubContentProps, MenuSubTriggerElement, MenuSubTriggerEmits, MenuSubTriggerNativeElement, MenuSubTriggerProps } from '@oku-ui/menu' + +export type ScopedContextMenu

= P & { scopeOkuContextMenu?: Scope } + +export const scopedContextMenuProps = { + scopeOkuContextMenu: { + ...ScopePropObject, + }, +} + +// NAMES +export const CONTEXT_MENU_NAME = 'OkuContextMenu' +export const CONTEXT_MENU_TRIGGER_NAME = 'OkuContextMenuTrigger' +export const CONTEXT_MENU_PORTAL_NAME = 'OkuContextMenuPortal' +export const CONTEXT_MENU_CONTENT_NAME = 'OkuContextMenuContent' +export const CONTEXT_MENU_GROUP_NAME = 'OkuContextMenuGroup' +export const CONTEXT_MENU_LABEL_NAME = 'OkuContextMenuLabel' +export const CONTEXT_MENU_ITEM_NAME = 'OkuContextMenuItem' +export const CONTEXT_MENU_CHECKBOX_ITEM_NAME = 'OkuContextMenuCheckboxItem' +export const CONTEXT_MENU_RADIO_GROUP_NAME = 'OkuContextMenuRadioGroup' +export const CONTEXT_MENU_RADIO_ITEM_NAME = 'OkuContextMenuRadioItem' +export const CONTEXT_MENU_ITEM_INDICATOR_NAME = 'OkuContextMenuItemIndicator' +export const CONTEXT_MENU_SEPARATOR_NAME = 'OkuContextMenuSeparator' +export const CONTEXT_MENU_ARROW_NAME = 'OkuContextMenuArrow' +export const CONTEXT_MENU_SUB_NAME = 'OkuContextMenuSub' +export const CONTEXT_MENU_SUB_TRIGGER_NAME = 'OkuContextMenuSubTrigger' +export const CONTEXT_MENU_SUB_CONTENT_NAME = 'OkuContextMenuSubContent' + +/* ------------------------------------------------------------------------------------------------- + * ContextMenu - Context-menu.ts + * ----------------------------------------------------------------------------------------------- */ + +export type Point = { x: number; y: number } + +export const [createMenuContextProvide, createContextMenuScop] = createProvideScope(CONTEXT_MENU_NAME, [ + createMenuScope, +]) + +export const useMenuScope = createMenuScope() + +export type ContextMenuProvideValue = { + open: Ref + onOpenChange(open: boolean): void + modal: Ref +} + +export const [ContextMenuProvider, useContextMenuInject] + = createMenuContextProvide(CONTEXT_MENU_NAME) + +export interface ContextMenuProps { + dir?: Direction + modal: boolean +} + +export const contextMenuProps = { + props: { + dir: { + type: String as PropType, + }, + modal: { + type: Boolean as PropType, + default: true, + }, + }, + emits: { + // eslint-disable-next-line unused-imports/no-unused-vars + openChange: (open: boolean) => true, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * ContextMenuTrigger - context-menu-trigger.ts + * ----------------------------------------------------------------------------------------------- */ + +export type ContextMenuTriggerNativeElement = OkuElement<'span'> +export type ContextMenuTriggerElement = HTMLSpanElement + +export interface ContextMenuTriggerProps extends PrimitiveProps { + disabled?: boolean +} + +export type ContextMenuTriggerEmits = { + contextMenu: [event: Event] + pointerdown: [event: PointerEvent] + pointermove: [event: PointerEvent] + pointercancel: [event: PointerEvent] + pointerup: [event: PointerEvent] +} + +export const contextMenuTriggerProps = { + props: { + disabled: { + type: Boolean as PropType, + default: false, + }, + ...primitiveProps, + }, + emits: { + // eslint-disable-next-line unused-imports/no-unused-vars + contextMenu: (event: ContextMenuTriggerEmits['contextMenu'][0]) => true, + // eslint-disable-next-line unused-imports/no-unused-vars + pointerdown: (event: ContextMenuTriggerEmits['pointerdown'][0]) => true, + // eslint-disable-next-line unused-imports/no-unused-vars + pointermove: (event: ContextMenuTriggerEmits['pointermove'][0]) => true, + // eslint-disable-next-line unused-imports/no-unused-vars + pointercancel: (event: ContextMenuTriggerEmits['pointercancel'][0]) => true, + // eslint-disable-next-line unused-imports/no-unused-vars + pointerup: (event: ContextMenuTriggerEmits['pointerup'][0]) => true, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * ContextMenuPortal - context-menu-portal.ts + * ----------------------------------------------------------------------------------------------- */ + +export interface ContextMenuPortalProps extends MenuPortalProps { } + +export const contextMenuPortalProps = { + props: { + ...menuPortalProps.props, + }, + emits: { + ...menuPortalProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * ContextMenuContent - context-menu-content.ts + * ----------------------------------------------------------------------------------------------- */ + +export type ContextMenuContentNativeElement = MenuContentNativeElement +export type ContextMenuContentElement = MenuContentElement + +export interface ContextMenuContentProps extends Omit { } + +export const contextMenuContentProps = { + props: { + ...propsOmit(menuContentProps.props, ['side', 'sideOffset', 'align']), + }, + emits: { + ...propsOmit(menuContentProps.emits, ['entryFocus']), + }, +} +/* ------------------------------------------------------------------------------------------------- + * ContextMenuGroup - context-menu-group.ts + * ----------------------------------------------------------------------------------------------- */ + +export type ContextMenuGroupNativeElement = MenuGroupNativeElement +export type ContextMenuGroupElement = MenuGroupElement + +export interface ContextMenuGroupProps extends MenuGroupProps { } + +export const contextMenuGroupProps = { + props: { + ...menuGroupProps.props, + }, + emits: { + ...menuGroupProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * ContextMenuLabel - context-menu-label.ts + * ----------------------------------------------------------------------------------------------- */ + +export type ContextMenuLabelNativeElement = MenuLabelNativeElement +export type ContextMenuLabelElement = MenuLabelElement + +export interface ContextMenuLabelProps extends MenuLabelProps { } + +export const contextMenuLabelProps = { + props: { + ...menuLabelProps.props, + }, + emits: { + ...menuLabelProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * ContextMenuItem - context-menu-item.ts + * ----------------------------------------------------------------------------------------------- */ + +export type ContextMenuItemNativeElement = MenuItemNativeElement +export type ContextMenuItemElement = MenuItemElement + +export interface ContextMenuItemProps extends MenuItemProps { } + +export interface ContextMenuItemEmits extends MenuItemEmits { } + +export const contextMenuItemProps = { + props: { + ...menuItemProps.props, + }, + emits: { + ...menuItemProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * ContextMenuCheckboxItem - context-menu-Checkbox-item.ts + * ----------------------------------------------------------------------------------------------- */ + +export type ContextMenuCheckboxItemNativeElement = MenuCheckboxItemNativeElement +export type ContextMenuCheckboxItemElement = MenuCheckboxItemElement + +export interface ContextMenuCheckboxItemProps extends MenuCheckboxItemProps { } +export interface ContextMenuCheckboxItemEmits extends MenuCheckboxItemEmits { } + +export const contextMenuCheckboxItemProps = { + props: { + ...menuCheckboxItemProps.props, + }, + emits: { + ...menuCheckboxItemProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * ContextMenuRadioGroup - context-menu-radio-group.ts + * ----------------------------------------------------------------------------------------------- */ + +export type ContextMenuRadioGroupNativeElement = MenuRadioGroupNativeElement +export type ContextMenuRadioGroupElement = MenuRadioGroupElement + +export interface ContextMenuRadioGroupProps extends MenuRadioGroupProps { } + +export const contextMenuRadioGroupProps = { + props: { + ...menuRadioGroupProps.props, + }, + emits: { + ...menuRadioGroupProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * ContextMenuRadioItem - context-menu-radio-item.ts + * ----------------------------------------------------------------------------------------------- */ + +export type ContextMenuRadioItemNativeElement = MenuRadioItemNativeElement +export type ContextMenuRadioItemElement = MenuRadioItemElement + +export interface ContextMenuRadioItemProps extends MenuRadioItemProps { } +export interface ContextMenuRadioItemEmits extends MenuRadioItemEmits { } + +export const contextMenuRadioItemProps = { + props: { + ...menuRadioItemProps.props, + }, + emits: { + ...menuRadioItemProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * ContextMenuItemIndicator - context-menu--item-indicator.ts + * ----------------------------------------------------------------------------------------------- */ + +export type ContextMenuItemIndicatorNativeElement = MenuItemIndicatorNativeElement +export type ContextMenuItemIndicatorElement = MenuItemIndicatorElement + +export interface ContextMenuItemIndicatorProps extends MenuItemIndicatorProps { } + +export const contextMenuItemIndicatorProps = { + props: { + ...menuItemIndicatorProps.props, + }, + emits: { + ...menuItemIndicatorProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * ContextMenuSeparator - context-menu-separator.ts + * ----------------------------------------------------------------------------------------------- */ + +export type ContextMenuSeparatorNativeElement = MenuSeparatorNativeElement +export type ContextMenuSeparatorElement = MenuSeparatorElement + +export interface ContextMenuSeparatorProps extends MenuSeparatorProps { } + +export const contextMenuSeparatorProps = { + props: { + ...menuSeparatorProps.props, + }, + emits: { + ...menuSeparatorProps.emits, + }, +} +/* ------------------------------------------------------------------------------------------------- + * ContextMenuArrow - context-menu-arrow.ts + * ----------------------------------------------------------------------------------------------- */ + +export type ContextMenuArrowNativeElement = MenuArrowNativeElement +export type ContextMenuArrowElement = MenuArrowElement + +export interface ContextMenuArrowProps extends MenuArrowProps { } + +export const contextMenuArrowProps = { + props: { + ...menuArrowProps.props, + }, + emits: { + ...menuArrowProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * ContextMenuSub - context-menu-sub.ts + * ----------------------------------------------------------------------------------------------- */ + +export interface ContextMenuSubProps { + open?: boolean + defaultOpen?: boolean +} + +export type ContextMenuSubEmits = { + 'update:modelValue': [value: boolean] + openChange: [open: boolean] +} + +export const contextMenuSubProps = { + props: { + modelValue: { + type: Boolean as PropType, + default: undefined, + }, + open: { + type: Boolean as PropType, + }, + defaultOpen: { + type: Boolean as PropType, + }, + }, + emits: { + // eslint-disable-next-line unused-imports/no-unused-vars + 'update:modelValue': (value: boolean) => true, + // eslint-disable-next-line unused-imports/no-unused-vars + 'openChange': (value: boolean) => true, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * ContextMenuSubTrigger - context-menu-sub-trigger.ts + * ----------------------------------------------------------------------------------------------- */ + +export type ContextMenuSubTriggerNativeElement = MenuSubTriggerNativeElement +export type ContextMenuSubTriggerElement = MenuSubTriggerElement + +export interface ContextMenuSubTriggerProps extends MenuSubTriggerProps { } +export interface ContextMenuSubTriggerEmits extends MenuSubTriggerEmits { } + +export const contextMenuSubTriggerProps = { + props: { + ...menuSubTriggerProps.props, + }, + emits: { + ...menuSubTriggerProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * ContextMenuSubContent - context-menu-sub-content.ts + * ----------------------------------------------------------------------------------------------- */ + +export type ContextMenuSubContentNativeElement = MenuSubContentNativeElement +export type ContextMenuSubContentElement = MenuSubContentElement + +export interface ContextMenuSubContentProps extends MenuSubContentProps { } +export interface ContextMenuSubContentProps extends MenuSubContentEmits { } + +export const contextMenuSubContentProps = { + props: { + ...menuSubContentProps.props, + }, + emits: { + ...menuSubContentProps.emits, + }, +} diff --git a/packages/components/context-menu/src/utils.ts b/packages/components/context-menu/src/utils.ts new file mode 100644 index 000000000..fb5e56b7b --- /dev/null +++ b/packages/components/context-menu/src/utils.ts @@ -0,0 +1,3 @@ +export function whenTouchOrPen(handler: ((event: E) => void)): (event: E) => void { + return (event: E) => (event.pointerType !== 'mouse' ? handler(event) : undefined) +} diff --git a/packages/components/context-menu/tsconfig.json b/packages/components/context-menu/tsconfig.json new file mode 100644 index 000000000..887182612 --- /dev/null +++ b/packages/components/context-menu/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "tsconfig/node18.json", + "include": [ + "src/**/*", + "tests/**/*", + "node_modules/tsconfig" + ] +} diff --git a/packages/core/menu/src/index.ts b/packages/core/menu/src/index.ts index ab2daf21e..4d9228509 100644 --- a/packages/core/menu/src/index.ts +++ b/packages/core/menu/src/index.ts @@ -39,6 +39,7 @@ export type { export type { MenuCheckboxItemProps, + MenuCheckboxItemEmits, MenuCheckboxItemElement, MenuCheckboxItemNativeElement, } from './props' @@ -75,6 +76,7 @@ export type { export type { MenuItemProps, + MenuItemEmits, MenuItemElement, MenuItemNativeElement, } from './props' @@ -105,6 +107,7 @@ export type { export type { MenuRadioItemProps, + MenuRadioItemEmits, MenuRadioItemElement, MenuRadioItemNativeElement, } from './props' @@ -121,12 +124,31 @@ export type { export type { MenuSubContentProps, + MenuSubContentEmits, MenuSubContentElement, MenuSubContentNativeElement, } from './props' export type { MenuSubTriggerProps, + MenuSubTriggerEmits, MenuSubTriggerElement, MenuSubTriggerNativeElement, } from './props' + +export { + createMenuScope, + menuPortalProps, + menuContentProps, + menuGroupProps, + menuLabelProps, + menuItemProps, + menuCheckboxItemProps, + menuRadioGroupProps, + menuRadioItemProps, + menuItemIndicatorProps, + menuSeparatorProps, + menuArrowProps, + menuSubTriggerProps, + menuSubContentProps, +} from './props' diff --git a/playground/nuxt3/package.json b/playground/nuxt3/package.json index e5fb570c8..488078e34 100644 --- a/playground/nuxt3/package.json +++ b/playground/nuxt3/package.json @@ -20,6 +20,7 @@ "@oku-ui/checkbox": "workspace:^", "@oku-ui/collapsible": "workspace:^", "@oku-ui/collection": "workspace:^", + "@oku-ui/context-menu": "workspace:^", "@oku-ui/dialog": "workspace:^", "@oku-ui/dismissable-layer": "workspace:^", "@oku-ui/focus-scope": "workspace:^", diff --git a/playground/vue3/package.json b/playground/vue3/package.json index 84028855d..0d53f9775 100644 --- a/playground/vue3/package.json +++ b/playground/vue3/package.json @@ -14,6 +14,7 @@ "@oku-ui/aspect-ratio": "workspace:^", "@oku-ui/avatar": "workspace:^", "@oku-ui/checkbox": "workspace:^", + "@oku-ui/context-menu": "workspace:^", "@oku-ui/label": "workspace:^", "@oku-ui/menu": "workspace:^", "@oku-ui/primitives": "workspace:^", diff --git a/vitest.config.ts b/vitest.config.ts index 4942189d3..ce7635e4a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -41,6 +41,7 @@ export default defineConfig({ '@oku-ui/dialog': 'packages/components/dialog/src', '@oku-ui/primitives': 'packages/components/primitives/src', '@oku-ui/accordion': 'packages/components/accordion/src', + '@oku-ui/context-menu': 'packages/components/context-menu/src', '@oku-ui/menu': 'packages/core/menu/src', '@oku-ui/dismissable-layer': 'packages/core/dismissable-layer/src', From 60824dde54cc0406c098285be5f370ca4ee855dc Mon Sep 17 00:00:00 2001 From: Shehab Rahal Date: Tue, 7 Nov 2023 01:24:17 +0800 Subject: [PATCH 02/15] chore: add stories --- .../src/stories/CheckboxItems.vue | 53 ++++ .../src/stories/ContextMenu.stories.ts | 157 ++++++++++++ .../src/stories/ContextMenuDemo.vue | 229 ++++++++++++++++++ .../context-menu/src/stories/Modality.vue | 152 ++++++++++++ .../context-menu/src/stories/Multiple.vue | 74 ++++++ .../context-menu/src/stories/Nested.vue | 96 ++++++++ .../src/stories/PreventClosing.vue | 34 +++ .../context-menu/src/stories/RadioItems.vue | 41 ++++ .../context-menu/src/stories/Styled.vue | 42 ++++ .../context-menu/src/stories/Submenus.vue | 186 ++++++++++++++ .../context-menu/src/stories/TickIcon.vue | 15 ++ .../context-menu/src/stories/WithLabels.vue | 33 +++ .../context-menu/src/stories/foods.ts | 41 ++++ 13 files changed, 1153 insertions(+) create mode 100644 packages/components/context-menu/src/stories/CheckboxItems.vue create mode 100644 packages/components/context-menu/src/stories/ContextMenu.stories.ts create mode 100644 packages/components/context-menu/src/stories/ContextMenuDemo.vue create mode 100644 packages/components/context-menu/src/stories/Modality.vue create mode 100644 packages/components/context-menu/src/stories/Multiple.vue create mode 100644 packages/components/context-menu/src/stories/Nested.vue create mode 100644 packages/components/context-menu/src/stories/PreventClosing.vue create mode 100644 packages/components/context-menu/src/stories/RadioItems.vue create mode 100644 packages/components/context-menu/src/stories/Styled.vue create mode 100644 packages/components/context-menu/src/stories/Submenus.vue create mode 100644 packages/components/context-menu/src/stories/TickIcon.vue create mode 100644 packages/components/context-menu/src/stories/WithLabels.vue create mode 100644 packages/components/context-menu/src/stories/foods.ts diff --git a/packages/components/context-menu/src/stories/CheckboxItems.vue b/packages/components/context-menu/src/stories/CheckboxItems.vue new file mode 100644 index 000000000..78bc12496 --- /dev/null +++ b/packages/components/context-menu/src/stories/CheckboxItems.vue @@ -0,0 +1,53 @@ + + + diff --git a/packages/components/context-menu/src/stories/ContextMenu.stories.ts b/packages/components/context-menu/src/stories/ContextMenu.stories.ts new file mode 100644 index 000000000..7d77cd5a2 --- /dev/null +++ b/packages/components/context-menu/src/stories/ContextMenu.stories.ts @@ -0,0 +1,157 @@ +import type { Meta, StoryObj } from '@storybook/vue3' + +import type { IContextMenuProps } from './ContextMenuDemo.vue' +import OkuContextMenu from './ContextMenuDemo.vue' + +interface StoryProps extends IContextMenuProps { +} + +const meta = { + title: 'components/ContextMenu', + component: OkuContextMenu, + args: { + template: 'Styled', + }, +} satisfies Meta & { + args: StoryProps +} + +export default meta +type Story = StoryObj & { + args: StoryProps +} + +export const Styled: Story = { + args: { + template: 'Styled', + }, + render: (args: any) => ({ + components: { OkuContextMenu }, + setup() { + return { args } + }, + template: ` + + `, + }), +} + +export const Modality: Story = { + args: { + template: 'Modality', + }, + render: (args: any) => ({ + components: { OkuContextMenu }, + setup() { + return { args } + }, + template: ` + + `, + }), +} + +export const Submenus: Story = { + args: { + template: 'Submenus', + }, + render: (args: any) => ({ + components: { OkuContextMenu }, + setup() { + return { args } + }, + template: ` + + `, + }), +} + +export const WithLabels: Story = { + args: { + template: 'WithLabels', + }, + render: (args: any) => ({ + components: { OkuContextMenu }, + setup() { + return { args } + }, + template: ` + + `, + }), +} + +export const CheckboxItems: Story = { + args: { + template: 'CheckboxItems', + }, + render: (args: any) => ({ + components: { OkuContextMenu }, + setup() { + return { args } + }, + template: ` + + `, + }), +} + +export const RadioItems: Story = { + args: { + template: 'RadioItems', + }, + render: (args: any) => ({ + components: { OkuContextMenu }, + setup() { + return { args } + }, + template: ` + + `, + }), +} + +export const PreventClosing: Story = { + args: { + template: 'PreventClosing', + }, + render: (args: any) => ({ + components: { OkuContextMenu }, + setup() { + return { args } + }, + template: ` + + `, + }), +} + +export const Multiple: Story = { + args: { + template: 'Multiple', + }, + render: (args: any) => ({ + components: { OkuContextMenu }, + setup() { + return { args } + }, + template: ` + + `, + }), +} + +export const Nested: Story = { + args: { + template: 'Nested', + }, + render: (args: any) => ({ + components: { OkuContextMenu }, + setup() { + return { args } + }, + template: ` + + `, + }), +} diff --git a/packages/components/context-menu/src/stories/ContextMenuDemo.vue b/packages/components/context-menu/src/stories/ContextMenuDemo.vue new file mode 100644 index 000000000..9a4333db3 --- /dev/null +++ b/packages/components/context-menu/src/stories/ContextMenuDemo.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/packages/components/context-menu/src/stories/Modality.vue b/packages/components/context-menu/src/stories/Modality.vue new file mode 100644 index 000000000..5d6cd7610 --- /dev/null +++ b/packages/components/context-menu/src/stories/Modality.vue @@ -0,0 +1,152 @@ + + +