diff --git a/README.md b/README.md index 5176db3d3..3aa09e05b 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,7 @@ Enter the component you want most in the components, leave the emojis and follow | [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 | - | | [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 | - | +| [Dropdown Menu](https://oku-ui.com/primitives/components/dropdown-menu) | Version | Downloads | Website | | [Hover Card](https://oku-ui.com/primitives/components/hover-card) | Version | Downloads | Website | | [Label](https://oku-ui.com/primitives/components/label) | Version | Downloads | Website | | [Menubar](https://github.com/oku-ui/primitives/issues/13) | A menu that appears when a user interacts with an element's trigger | 🚧 In Progress | - | @@ -54,15 +53,19 @@ Enter the component you want most in the components, leave the emojis and follow | [Toggle Group](https://oku-ui.com/primitives/components/toggle-group) | Version | Downloads | Website | | [Toolbar](https://oku-ui.com/primitives/components/toolbar) | Version | Downloads | Website | | [Tooltip](https://oku-ui.com/primitives/components/tooltip) | Version | Downloads | Website | -[Primitives](https://oku-ui.com/primitives) | Version | Downloads | Website | +| [Primitives](https://oku-ui.com/primitives) | Version | Downloads | Website | ## Utils -[Nuxt Module](https://oku-ui.com/primitives/introduction/nuxt) | Version | Downloads | Website | +| Util | Description | Status | Docs | +| --- | --- | --- | --- | +| [Nuxt Module](https://oku-ui.com/primitives/introduction/nuxt) | Version | Downloads | Website | ## Core +| Core | Description | Status | Docs | +| --- | --- | --- | --- | | [Menu](https://oku-ui.com/primitives/core/menu) | Version | Downloads | Website | ## Community diff --git a/package.json b/package.json index 848053de1..31863ce14 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@oku-ui/dialog": "workspace:^", "@oku-ui/direction": "workspace:^", "@oku-ui/dismissable-layer": "workspace:^", + "@oku-ui/dropdown-menu": "workspace:^", "@oku-ui/focus-guards": "workspace:^", "@oku-ui/focus-scope": "workspace:^", "@oku-ui/hover-card": "workspace:^", @@ -137,6 +138,7 @@ "@oku-ui/dialog": "workspace:^", "@oku-ui/direction": "workspace:^", "@oku-ui/dismissable-layer": "workspace:^", + "@oku-ui/dropdown-menu": "workspace:^", "@oku-ui/focus-guards": "workspace:^", "@oku-ui/focus-scope": "workspace:^", "@oku-ui/hover-card": "workspace:^", diff --git a/packages/components/dropdown-menu/README.md b/packages/components/dropdown-menu/README.md new file mode 100644 index 000000000..87cecab34 --- /dev/null +++ b/packages/components/dropdown-menu/README.md @@ -0,0 +1,16 @@ +# Dropdown Menu +Displays a menu to the user—such as a set of actions or functions—triggered by a button. + +![@oku-ui/dropdown-menu](./../../../.github/assets/og/oku-dropdown-menu.jpg) + +| Description | Status | Docs | +| --- | --- | --- | +| Version | Downloads | Website + +## Installation + +```sh +$ pnpm add @oku-ui/dropdown-menu +``` + +[Documentation](https://oku-ui.com/primitives/components/dropdown-menu) diff --git a/packages/components/dropdown-menu/build.config.ts b/packages/components/dropdown-menu/build.config.ts new file mode 100644 index 000000000..e20ccb0a1 --- /dev/null +++ b/packages/components/dropdown-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/dropdown-menu/package.json b/packages/components/dropdown-menu/package.json new file mode 100644 index 000000000..4c850c62e --- /dev/null +++ b/packages/components/dropdown-menu/package.json @@ -0,0 +1,56 @@ +{ + "name": "@oku-ui/dropdown-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/dropdown-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/dismissable-layer": "latest", + "@oku-ui/focus-scope": "latest", + "@oku-ui/menu": "latest", + "@oku-ui/primitive": "latest", + "@oku-ui/provide": "latest", + "@oku-ui/use-composable": "latest", + "@oku-ui/utils": "latest" + }, + "devDependencies": { + "tsconfig": "workspace:^" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/components/dropdown-menu/src/dropdown-menu-arrow.ts b/packages/components/dropdown-menu/src/dropdown-menu-arrow.ts new file mode 100644 index 000000000..c0bd8f43c --- /dev/null +++ b/packages/components/dropdown-menu/src/dropdown-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 { DROPDOWN_MENU_ARROW_NAME, dropdownMenuArrowProps, scopedDropdownMenuProps, useMenuScope } from './props' +import type { DropdownMenuArrowNativeElement } from './props' + +const dropdownMenuArrow = defineComponent({ + name: DROPDOWN_MENU_ARROW_NAME, + components: { + OkuMenuArrow, + }, + inheritAttrs: false, + props: { + ...dropdownMenuArrowProps.props, + ...scopedDropdownMenuProps, + }, + emits: dropdownMenuArrowProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuDropdownMenu, + ...arrowProps + } = toRefs(props) + + const _other = reactive(arrowProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const menuScope = useMenuScope(scopeOkuDropdownMenu.value) + + return () => h(OkuMenuArrow, { + ...menuScope, + ...mergeProps(attrs, otherProps), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuDropdownMenuArrow = dropdownMenuArrow as typeof dropdownMenuArrow & +(new () => { $props: DropdownMenuArrowNativeElement }) diff --git a/packages/components/dropdown-menu/src/dropdown-menu-checkbox-item.ts b/packages/components/dropdown-menu/src/dropdown-menu-checkbox-item.ts new file mode 100644 index 000000000..42305c1a5 --- /dev/null +++ b/packages/components/dropdown-menu/src/dropdown-menu-checkbox-item.ts @@ -0,0 +1,42 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit, useForwardRef, useListeners } from '@oku-ui/use-composable' +import { OkuMenuCheckboxItem } from '@oku-ui/menu' +import { DROPDOWN_MENU_CHECKBOX_ITEM_NAME, dropdownMenuCheckboxItemProps, scopedDropdownMenuProps, useMenuScope } from './props' +import type { DropdownMenuCheckboxItemNativeElement } from './props' + +const dropdownMenuCheckboxItem = defineComponent({ + name: DROPDOWN_MENU_CHECKBOX_ITEM_NAME, + components: { + OkuMenuCheckboxItem, + }, + inheritAttrs: false, + props: { + ...dropdownMenuCheckboxItemProps.props, + ...scopedDropdownMenuProps, + }, + emits: dropdownMenuCheckboxItemProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuDropdownMenu, + ...checkboxItemProps + } = toRefs(props) + + const _other = reactive(checkboxItemProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + const emits = useListeners() + + const menuScope = useMenuScope(scopeOkuDropdownMenu.value) + + return () => h(OkuMenuCheckboxItem, { + ...menuScope, + ...mergeProps(attrs, otherProps, emits), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuDropdownMenuCheckboxItem = dropdownMenuCheckboxItem as typeof dropdownMenuCheckboxItem & +(new () => { $props: DropdownMenuCheckboxItemNativeElement }) diff --git a/packages/components/dropdown-menu/src/dropdown-menu-content.ts b/packages/components/dropdown-menu/src/dropdown-menu-content.ts new file mode 100644 index 000000000..d2456e8f1 --- /dev/null +++ b/packages/components/dropdown-menu/src/dropdown-menu-content.ts @@ -0,0 +1,74 @@ +import { defineComponent, h, mergeProps, reactive, ref, toRefs } from 'vue' +import { reactiveOmit, useForwardRef, useListeners } from '@oku-ui/use-composable' +import { composeEventHandlers } from '@oku-ui/utils' +import { OkuMenuContent } from '@oku-ui/menu' +import { DROPDOWN_MENU_CONTENT_NAME, dropdownMenuContentProps, scopedDropdownMenuProps, useDropdownMenuInject, useMenuScope } from './props' +import type { DropdownMenuContentEmits } from './props' + +const dropdownMenuContent = defineComponent({ + name: DROPDOWN_MENU_CONTENT_NAME, + components: { + OkuMenuContent, + }, + inheritAttrs: false, + props: { + ...dropdownMenuContentProps.props, + ...scopedDropdownMenuProps, + }, + emits: dropdownMenuContentProps.emits, + setup(props, { attrs, emit, slots }) { + const { + scopeOkuDropdownMenu, + ...contentProps + } = toRefs(props) + + const _other = reactive(contentProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + const emits = useListeners() + + const inject = useDropdownMenuInject(DROPDOWN_MENU_CONTENT_NAME, scopeOkuDropdownMenu.value) + const menuScope = useMenuScope(scopeOkuDropdownMenu.value) + const hasInteractedOutsideRef = ref(false) + + return () => h(OkuMenuContent, { + 'id': inject.contentId.value, + 'aria-labelledby': inject.triggerId.value, + ...menuScope, + ...mergeProps(attrs, otherProps, emits), + 'ref': forwardedRef, + 'onCloseAutoFocus': composeEventHandlers((event) => { + emit('closeAutoFocus', event) + }, (event) => { + if (!hasInteractedOutsideRef.value) + inject.triggerRef.value?.focus() + hasInteractedOutsideRef.value = false + // Always prevent auto focus because we either focus manually or want user agent focus + event.preventDefault() + }), + 'onInteractOutside': composeEventHandlers((event) => { + emit('interactOutside', event) + }, (event) => { + const originalEvent = event.detail.originalEvent as PointerEvent + const ctrlLeftClick = originalEvent.button === 0 && originalEvent.ctrlKey === true + const isRightClick = originalEvent.button === 2 || ctrlLeftClick + if (!inject.modal.value || isRightClick) + hasInteractedOutsideRef.value = true + }), + 'style': { + ...attrs.style as any, + // re-namespace exposed content custom properties + ...{ + '--oku-dropdown-menu-content-transform-origin': 'var(--oku-popper-transform-origin)', + '--oku-dropdown-menu-content-available-width': 'var(--oku-popper-available-width)', + '--oku-dropdown-menu-content-available-height': 'var(--oku-popper-available-height)', + '--oku-dropdown-menu-trigger-width': 'var(--oku-popper-anchor-width)', + '--oku-dropdown-menu-trigger-height': 'var(--oku-popper-anchor-height)', + }, + }, + }, slots) + }, +}) + +export const OkuDropdownMenuContent = dropdownMenuContent diff --git a/packages/components/dropdown-menu/src/dropdown-menu-group.ts b/packages/components/dropdown-menu/src/dropdown-menu-group.ts new file mode 100644 index 000000000..e7dfb3636 --- /dev/null +++ b/packages/components/dropdown-menu/src/dropdown-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 { DROPDOWN_MENU_GROUP_NAME, dropdownMenuGroupProps, scopedDropdownMenuProps, useMenuScope } from './props' +import type { DropdownMenuGroupNativeElement } from './props' + +const dropdownMenuGroup = defineComponent({ + name: DROPDOWN_MENU_GROUP_NAME, + components: { + OkuMenuGroup, + }, + inheritAttrs: false, + props: { + ...dropdownMenuGroupProps.props, + ...scopedDropdownMenuProps, + }, + emits: dropdownMenuGroupProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuDropdownMenu, + ...groupProps + } = toRefs(props) + + const _other = reactive(groupProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const menuScope = useMenuScope(scopeOkuDropdownMenu.value) + + return () => h(OkuMenuGroup, { + ...menuScope, + ...mergeProps(attrs, otherProps), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuDropdownMenuGroup = dropdownMenuGroup as typeof dropdownMenuGroup & +(new () => { $props: DropdownMenuGroupNativeElement }) diff --git a/packages/components/dropdown-menu/src/dropdown-menu-item-indicator.ts b/packages/components/dropdown-menu/src/dropdown-menu-item-indicator.ts new file mode 100644 index 000000000..77b1ee6c2 --- /dev/null +++ b/packages/components/dropdown-menu/src/dropdown-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 { DROPDOWN_MENU_ITEM_INDICATOR_NAME, dropdownMenuItemIndicatorProps, scopedDropdownMenuProps, useMenuScope } from './props' +import type { DropdownMenuItemIndicatorNativeElement } from './props' + +const dropdownMenuItemIndicator = defineComponent({ + name: DROPDOWN_MENU_ITEM_INDICATOR_NAME, + components: { + OkuMenuItemIndicator, + }, + inheritAttrs: false, + props: { + ...dropdownMenuItemIndicatorProps.props, + ...scopedDropdownMenuProps, + }, + emits: dropdownMenuItemIndicatorProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuDropdownMenu, + ...itemIndicatorProps + } = toRefs(props) + + const _other = reactive(itemIndicatorProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const menuScope = useMenuScope(scopeOkuDropdownMenu.value) + + return () => h(OkuMenuItemIndicator, { + ...menuScope, + ...mergeProps(attrs, otherProps), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuDropdownMenuItemIndicator = dropdownMenuItemIndicator as typeof dropdownMenuItemIndicator & +(new () => { $props: DropdownMenuItemIndicatorNativeElement }) diff --git a/packages/components/dropdown-menu/src/dropdown-menu-item.ts b/packages/components/dropdown-menu/src/dropdown-menu-item.ts new file mode 100644 index 000000000..faaf643c1 --- /dev/null +++ b/packages/components/dropdown-menu/src/dropdown-menu-item.ts @@ -0,0 +1,42 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit, useForwardRef, useListeners } from '@oku-ui/use-composable' +import { OkuMenuItem } from '@oku-ui/menu' +import { DROPDOWN_MENU_ITEM_NAME, dropdownMenuItemProps, scopedDropdownMenuProps, useMenuScope } from './props' +import type { DropdownMenuItemNativeElement } from './props' + +const dropdownMenuItem = defineComponent({ + name: DROPDOWN_MENU_ITEM_NAME, + components: { + OkuMenuItem, + }, + inheritAttrs: false, + props: { + ...dropdownMenuItemProps.props, + ...scopedDropdownMenuProps, + }, + emits: dropdownMenuItemProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuDropdownMenu, + ...itemProps + } = toRefs(props) + + const _other = reactive(itemProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + const emits = useListeners() + + const menuScope = useMenuScope(scopeOkuDropdownMenu.value) + + return () => h(OkuMenuItem, { + ...menuScope, + ...mergeProps(attrs, otherProps, emits), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuDropdownMenuItem = dropdownMenuItem as typeof dropdownMenuItem & +(new () => { $props: DropdownMenuItemNativeElement }) diff --git a/packages/components/dropdown-menu/src/dropdown-menu-label.ts b/packages/components/dropdown-menu/src/dropdown-menu-label.ts new file mode 100644 index 000000000..0c3dbdb32 --- /dev/null +++ b/packages/components/dropdown-menu/src/dropdown-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 { DROPDOWN_MENU_LABEL_NAME, dropdownMenuLabelProps, scopedDropdownMenuProps, useMenuScope } from './props' +import type { DropdownMenuLabelNativeElement } from './props' + +const dropdownMenuLabel = defineComponent({ + name: DROPDOWN_MENU_LABEL_NAME, + components: { + OkuMenuLabel, + }, + inheritAttrs: false, + props: { + ...dropdownMenuLabelProps.props, + ...scopedDropdownMenuProps, + }, + emits: dropdownMenuLabelProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuDropdownMenu, + ...labelProps + } = toRefs(props) + + const _other = reactive(labelProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const menuScope = useMenuScope(scopeOkuDropdownMenu.value) + + return () => h(OkuMenuLabel, { + ...menuScope, + ...mergeProps(attrs, otherProps), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuDropdownMenuLabel = dropdownMenuLabel as typeof dropdownMenuLabel & +(new () => { $props: DropdownMenuLabelNativeElement }) diff --git a/packages/components/dropdown-menu/src/dropdown-menu-portal.ts b/packages/components/dropdown-menu/src/dropdown-menu-portal.ts new file mode 100644 index 000000000..0dedaa634 --- /dev/null +++ b/packages/components/dropdown-menu/src/dropdown-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 { DROPDOWN_MENU_PORTAL_NAME, dropdownMenuPortalProps, scopedDropdownMenuProps, useMenuScope } from './props' + +const dropdownMenuPortal = defineComponent({ + name: DROPDOWN_MENU_PORTAL_NAME, + components: { + OkuMenuPortal, + }, + inheritAttrs: false, + props: { + ...dropdownMenuPortalProps.props, + ...scopedDropdownMenuProps, + }, + emits: dropdownMenuPortalProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuDropdownMenu, + ...portalProps + } = toRefs(props) + + const _other = reactive(portalProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const menuScope = useMenuScope(scopeOkuDropdownMenu.value) + + return () => h(OkuMenuPortal, { + ...menuScope, + ...mergeProps(attrs, otherProps), + }, slots) + }, +}) + +export const OkuDropdownMenuPortal = dropdownMenuPortal diff --git a/packages/components/dropdown-menu/src/dropdown-menu-radio-group.ts b/packages/components/dropdown-menu/src/dropdown-menu-radio-group.ts new file mode 100644 index 000000000..15d3ad79a --- /dev/null +++ b/packages/components/dropdown-menu/src/dropdown-menu-radio-group.ts @@ -0,0 +1,42 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit, useForwardRef, useListeners } from '@oku-ui/use-composable' +import { OkuMenuRadioGroup } from '@oku-ui/menu' +import { DROPDOWN_MENU_RADIO_GROUP_NAME, dropdownMenuRadioGroupProps, scopedDropdownMenuProps, useMenuScope } from './props' +import type { DropdownMenuRadioGroupNativeElement } from './props' + +const dropdownMenuRadioGroup = defineComponent({ + name: DROPDOWN_MENU_RADIO_GROUP_NAME, + components: { + OkuMenuRadioGroup, + }, + inheritAttrs: false, + props: { + ...dropdownMenuRadioGroupProps.props, + ...scopedDropdownMenuProps, + }, + emits: dropdownMenuRadioGroupProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuDropdownMenu, + ...radioGroupProps + } = toRefs(props) + + const _other = reactive(radioGroupProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + const emits = useListeners() + + const menuScope = useMenuScope(scopeOkuDropdownMenu.value) + + return () => h(OkuMenuRadioGroup, { + ...menuScope, + ...mergeProps(attrs, otherProps, emits), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuDropdownMenuRadioGroup = dropdownMenuRadioGroup as typeof dropdownMenuRadioGroup & +(new () => { $props: DropdownMenuRadioGroupNativeElement }) diff --git a/packages/components/dropdown-menu/src/dropdown-menu-radio-item.ts b/packages/components/dropdown-menu/src/dropdown-menu-radio-item.ts new file mode 100644 index 000000000..4db516409 --- /dev/null +++ b/packages/components/dropdown-menu/src/dropdown-menu-radio-item.ts @@ -0,0 +1,42 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit, useForwardRef, useListeners } from '@oku-ui/use-composable' +import { OkuMenuRadioItem } from '@oku-ui/menu' +import { DROPDOWN_MENU_RADIO_ITEM_NAME, dropdownMenuRadioItemProps, scopedDropdownMenuProps, useMenuScope } from './props' +import type { DropdownMenuRadioItemNativeElement } from './props' + +const dropdownMenuRadioItem = defineComponent({ + name: DROPDOWN_MENU_RADIO_ITEM_NAME, + components: { + OkuMenuRadioItem, + }, + inheritAttrs: false, + props: { + ...dropdownMenuRadioItemProps.props, + ...scopedDropdownMenuProps, + }, + emits: dropdownMenuRadioItemProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuDropdownMenu, + ...radioItemProps + } = toRefs(props) + + const _other = reactive(radioItemProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + const emits = useListeners() + + const menuScope = useMenuScope(scopeOkuDropdownMenu.value) + + return () => h(OkuMenuRadioItem, { + ...menuScope, + ...mergeProps(attrs, otherProps, emits), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuDropdownMenuRadioItem = dropdownMenuRadioItem as typeof dropdownMenuRadioItem & +(new () => { $props: DropdownMenuRadioItemNativeElement }) diff --git a/packages/components/dropdown-menu/src/dropdown-menu-separator.ts b/packages/components/dropdown-menu/src/dropdown-menu-separator.ts new file mode 100644 index 000000000..e7e9363d4 --- /dev/null +++ b/packages/components/dropdown-menu/src/dropdown-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 { DROPDOWN_MENU_SEPARATOR_NAME, dropdownMenuSeparatorProps, scopedDropdownMenuProps, useMenuScope } from './props' +import type { DropdownMenuSeparatorNativeElement } from './props' + +const dropdownMenuSeparator = defineComponent({ + name: DROPDOWN_MENU_SEPARATOR_NAME, + components: { + OkuMenuSeparator, + }, + inheritAttrs: false, + props: { + ...dropdownMenuSeparatorProps.props, + ...scopedDropdownMenuProps, + }, + emits: dropdownMenuSeparatorProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuDropdownMenu, + ...separatorProps + } = toRefs(props) + + const _other = reactive(separatorProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + + const menuScope = useMenuScope(scopeOkuDropdownMenu.value) + + return () => h(OkuMenuSeparator, { + ...menuScope, + ...mergeProps(attrs, otherProps), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuDropdownMenuSeparator = dropdownMenuSeparator as typeof dropdownMenuSeparator & +(new () => { $props: DropdownMenuSeparatorNativeElement }) diff --git a/packages/components/dropdown-menu/src/dropdown-menu-sub-content.ts b/packages/components/dropdown-menu/src/dropdown-menu-sub-content.ts new file mode 100644 index 000000000..0e94bbfd7 --- /dev/null +++ b/packages/components/dropdown-menu/src/dropdown-menu-sub-content.ts @@ -0,0 +1,53 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit, useForwardRef, useListeners } from '@oku-ui/use-composable' +import { OkuMenuSubContent } from '@oku-ui/menu' +import { DROPDOWN_MENU_SUB_CONTENT_NAME, dropdownMenuSubContentProps, scopedDropdownMenuProps, useMenuScope } from './props' +import type { DropdownMenuSubContentNativeElement } from './props' + +const dropdownMenuSubContent = defineComponent({ + name: DROPDOWN_MENU_SUB_CONTENT_NAME, + components: { + OkuMenuSubContent, + }, + inheritAttrs: false, + props: { + ...dropdownMenuSubContentProps.props, + ...scopedDropdownMenuProps, + }, + emits: dropdownMenuSubContentProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuDropdownMenu, + ...subContentProps + } = toRefs(props) + + const _other = reactive(subContentProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + const emits = useListeners() + + const menuScope = useMenuScope(scopeOkuDropdownMenu.value) + + return () => h(OkuMenuSubContent, { + ...menuScope, + ...mergeProps(attrs, otherProps, emits), + ref: forwardedRef, + style: { + ...attrs.style as any, + // re-namespace exposed content custom properties + ...{ + '--oku-dropdown-menu-content-transform-origin': 'var(--oku-popper-transform-origin)', + '--oku-dropdown-menu-content-available-width': 'var(--oku-popper-available-width)', + '--oku-dropdown-menu-content-available-height': 'var(--oku-popper-available-height)', + '--oku-dropdown-menu-trigger-width': 'var(--oku-popper-anchor-width)', + '--oku-dropdown-menu-trigger-height': 'var(--oku-popper-anchor-height)', + }, + }, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuDropdownMenuSubContent = dropdownMenuSubContent as typeof dropdownMenuSubContent & +(new () => { $props: DropdownMenuSubContentNativeElement }) diff --git a/packages/components/dropdown-menu/src/dropdown-menu-sub-trigger.ts b/packages/components/dropdown-menu/src/dropdown-menu-sub-trigger.ts new file mode 100644 index 000000000..20a5b9143 --- /dev/null +++ b/packages/components/dropdown-menu/src/dropdown-menu-sub-trigger.ts @@ -0,0 +1,42 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { reactiveOmit, useForwardRef, useListeners } from '@oku-ui/use-composable' +import { OkuMenuSubTrigger } from '@oku-ui/menu' +import { DROPDOWN_MENU_SUB_TRIGGER_NAME, dropdownMenuSubTriggerProps, scopedDropdownMenuProps, useMenuScope } from './props' +import type { DropdownMenuSubTriggerNativeElement } from './props' + +const dropdownMenuSubTrigger = defineComponent({ + name: DROPDOWN_MENU_SUB_TRIGGER_NAME, + components: { + OkuMenuSubTrigger, + }, + inheritAttrs: false, + props: { + ...dropdownMenuSubTriggerProps.props, + ...scopedDropdownMenuProps, + }, + emits: dropdownMenuSubTriggerProps.emits, + setup(props, { attrs, slots }) { + const { + scopeOkuDropdownMenu, + ...triggerItemProps + } = toRefs(props) + + const _other = reactive(triggerItemProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + const emits = useListeners() + + const menuScope = useMenuScope(scopeOkuDropdownMenu.value) + + return () => h(OkuMenuSubTrigger, { + ...menuScope, + ...mergeProps(attrs, otherProps, emits), + ref: forwardedRef, + }, slots) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuDropdownMenuSubTrigger = dropdownMenuSubTrigger as typeof dropdownMenuSubTrigger & +(new () => { $props: DropdownMenuSubTriggerNativeElement }) diff --git a/packages/components/dropdown-menu/src/dropdown-menu-sub.ts b/packages/components/dropdown-menu/src/dropdown-menu-sub.ts new file mode 100644 index 000000000..5d0686bf3 --- /dev/null +++ b/packages/components/dropdown-menu/src/dropdown-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 { DROPDOWN_MENU_RADIO_ITEM_NAME, dropdownMenuSubProps, scopedDropdownMenuProps, useMenuScope } from './props' + +const dropdownMenuSub = defineComponent({ + name: DROPDOWN_MENU_RADIO_ITEM_NAME, + components: { + OkuMenuSub, + }, + inheritAttrs: false, + props: { + ...dropdownMenuSubProps.props, + ...scopedDropdownMenuProps, + }, + emits: dropdownMenuSubProps.emits, + setup(props, { attrs, emit, slots }) { + const { + scopeOkuDropdownMenu, + open: openProp, + defaultOpen, + } = toRefs(props) + + const menuScope = useMenuScope(scopeOkuDropdownMenu.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) + emit('update:modelValue', result) + }, + }) + + return () => h(OkuMenuSub, { + ...attrs, + ...menuScope, + open: state.value, + onOpenChange: _open => updateValue(_open), + }, slots) + }, +}) + +export const OkuDropdownMenuSub = dropdownMenuSub diff --git a/packages/components/dropdown-menu/src/dropdown-menu-trigger.ts b/packages/components/dropdown-menu/src/dropdown-menu-trigger.ts new file mode 100644 index 000000000..815211b4c --- /dev/null +++ b/packages/components/dropdown-menu/src/dropdown-menu-trigger.ts @@ -0,0 +1,83 @@ +import { defineComponent, h, mergeProps, reactive, toRefs } from 'vue' +import { Primitive } from '@oku-ui/primitive' +import { reactiveOmit, useComposedRefs, useForwardRef, useListeners } from '@oku-ui/use-composable' +import { composeEventHandlers } from '@oku-ui/utils' +import { OkuMenuAnchor } from '@oku-ui/menu' +import { DROPDOWN_MENU_TRIGGER_NAME, dropdownMenuTriggerProps, scopedDropdownMenuProps, useDropdownMenuInject, useMenuScope } from './props' +import type { DropdownMenuTriggerEmits, DropdownMenuTriggerNativeElement } from './props' + +const dropdownMenuTrigger = defineComponent({ + name: DROPDOWN_MENU_TRIGGER_NAME, + components: { + OkuMenuAnchor, + }, + inheritAttrs: false, + props: { + ...dropdownMenuTriggerProps.props, + ...scopedDropdownMenuProps, + }, + emits: dropdownMenuTriggerProps.emits, + setup(props, { attrs, emit, slots }) { + const { + scopeOkuDropdownMenu, + disabled, + ...triggerProps + } = toRefs(props) + + const _other = reactive(triggerProps) + const otherProps = reactiveOmit(_other, (key, _value) => key === undefined) + + const forwardedRef = useForwardRef() + const emits = useListeners() + + const inject = useDropdownMenuInject(DROPDOWN_MENU_TRIGGER_NAME, scopeOkuDropdownMenu.value) + const menuScope = useMenuScope(scopeOkuDropdownMenu.value) + + return () => h(OkuMenuAnchor, { + asChild: true, + ...menuScope, + }, h(Primitive.button.value, { + 'type': 'button', + 'id': inject.triggerId.value, + 'aria-haspopup': 'menu', + 'aria-expanded': inject.open.value, + 'aria-controls': inject.open.value ? inject.contentId.value : undefined, + 'data-state': inject.open.value ? 'open' : 'closed', + 'data-disabled': disabled.value ? '' : undefined, + 'disabled': disabled.value, + ...mergeProps(attrs, otherProps, emits), + 'ref': useComposedRefs(forwardedRef, inject.triggerRef), + 'onPointerdown': composeEventHandlers((event) => { + emit('pointerdown', event) + }, (event) => { + // only call handler if it's the left button (mousedown gets triggered by all mouse buttons) + // but not when the control key is pressed (avoiding MacOS right click) + if (!disabled.value && event.button === 0 && event.ctrlKey === false) { + inject.onOpenToggle() + // prevent trigger focusing when opening + // this allows the content to be given focus without competition + if (!inject.open.value) + event.preventDefault() + } + }), + 'onKeydown': composeEventHandlers((event) => { + emit('keydown', event) + }, (event) => { + if (disabled.value) + return + if (['Enter', ' '].includes(event.key)) + inject.onOpenToggle() + if (event.key === 'ArrowDown') + inject.onOpenChange(true) + // prevent keydown from scrolling window / first focused item to execute + // that keydown (inadvertently closing the menu) + if (['Enter', ' ', 'ArrowDown'].includes(event.key)) + event.preventDefault() + }), + }, slots)) + }, +}) + +// TODO: https://github.com/vuejs/core/pull/7444 after delete +export const OkuDropdownMenuTrigger = dropdownMenuTrigger as typeof dropdownMenuTrigger & +(new () => { $props: DropdownMenuTriggerNativeElement }) diff --git a/packages/components/dropdown-menu/src/dropdown-menu.ts b/packages/components/dropdown-menu/src/dropdown-menu.ts new file mode 100644 index 000000000..3ec4b4026 --- /dev/null +++ b/packages/components/dropdown-menu/src/dropdown-menu.ts @@ -0,0 +1,69 @@ +import { computed, defineComponent, h, ref, toRefs, useModel } from 'vue' +import { useControllable, useId } from '@oku-ui/use-composable' +import { OkuMenu } from '@oku-ui/menu' +import { DROPDOWN_MENU_NAME, dropdownMenuProps, dropdownMenuProvider, scopedDropdownMenuProps, useMenuScope } from './props' + +const dropdownMenu = defineComponent({ + name: DROPDOWN_MENU_NAME, + components: { + OkuMenu, + }, + inheritAttrs: false, + props: { + ...dropdownMenuProps.props, + ...scopedDropdownMenuProps, + }, + emits: dropdownMenuProps.emits, + setup(props, { attrs, emit, slots }) { + const { + scopeOkuDropdownMenu, + dir, + open: openProp, + defaultOpen, + modal, + } = toRefs(props) + + const menuScope = useMenuScope(scopeOkuDropdownMenu.value) + const triggerRef = ref(null) + + 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) + emit('update:modelValue', result) + }, + initialValue: false, + }) + + dropdownMenuProvider({ + scope: scopeOkuDropdownMenu.value, + triggerId: computed(() => useId()), + triggerRef, + contentId: computed(() => useId()), + open: computed(() => state.value), + onOpenChange: _open => updateValue(_open), + onOpenToggle: () => updateValue(!state.value), + modal, + }) + + return () => h(OkuMenu, { + ...attrs, + ...menuScope, + open: state.value, + onOpenChange: _open => updateValue(_open), + dir: dir.value, + modal: modal.value, + }, slots) + }, +}) + +export const OkuDropdownMenu = dropdownMenu diff --git a/packages/components/dropdown-menu/src/index.ts b/packages/components/dropdown-menu/src/index.ts new file mode 100644 index 000000000..fe6b5cc9c --- /dev/null +++ b/packages/components/dropdown-menu/src/index.ts @@ -0,0 +1,117 @@ +import type { } from '@floating-ui/vue' + +export { OkuDropdownMenuArrow } from './dropdown-menu-arrow' +export { OkuDropdownMenuCheckboxItem } from './dropdown-menu-checkbox-item' +export { OkuDropdownMenuContent } from './dropdown-menu-content' +export { OkuDropdownMenuGroup } from './dropdown-menu-group' +export { OkuDropdownMenuItemIndicator } from './dropdown-menu-item-indicator' +export { OkuDropdownMenuItem } from './dropdown-menu-item' +export { OkuDropdownMenuLabel } from './dropdown-menu-label' +export { OkuDropdownMenuPortal } from './dropdown-menu-portal' +export { OkuDropdownMenuRadioGroup } from './dropdown-menu-radio-group' +export { OkuDropdownMenuRadioItem } from './dropdown-menu-radio-item' +export { OkuDropdownMenuSeparator } from './dropdown-menu-separator' +export { OkuDropdownMenuSubContent } from './dropdown-menu-sub-content' +export { OkuDropdownMenuSubTrigger } from './dropdown-menu-sub-trigger' +export { OkuDropdownMenuSub } from './dropdown-menu-sub' +export { OkuDropdownMenuTrigger } from './dropdown-menu-trigger' +export { OkuDropdownMenu } from './dropdown-menu' + +export type { + DropdownMenuArrowProps, + DropdownMenuArrowElement, + DropdownMenuArrowNativeElement, +} from './props' + +export type { + DropdownMenuCheckboxItemProps, + DropdownMenuCheckboxItemEmits, + DropdownMenuCheckboxItemElement, + DropdownMenuCheckboxItemNativeElement, +} from './props' + +export type { + DropdownMenuContentProps, + DropdownMenuContentEmits, + DropdownMenuContentElement, + DropdownMenuContentNativeElement, +} from './props' + +export type { + DropdownMenuGroupProps, + DropdownMenuGroupElement, + DropdownMenuGroupNativeElement, +} from './props' + +export type { + DropdownMenuItemIndicatorProps, + DropdownMenuItemIndicatorElement, + DropdownMenuItemIndicatorNativeElement, +} from './props' + +export type { + DropdownMenuItemProps, + DropdownMenuItemEmits, + DropdownMenuItemElement, + DropdownMenuItemNativeElement, +} from './props' + +export type { + DropdownMenuLabelProps, + DropdownMenuLabelElement, + DropdownMenuLabelNativeElement, +} from './props' + +export type { + DropdownMenuPortalProps, +} from './props' + +export type { + DropdownMenuRadioGroupProps, + DropdownMenuRadioGroupElement, + DropdownMenuRadioGroupNativeElement, +} from './props' + +export type { + DropdownMenuRadioItemProps, + DropdownMenuRadioItemEmits, + DropdownMenuRadioItemElement, + DropdownMenuRadioItemNativeElement, +} from './props' + +export type { + DropdownMenuSeparatorProps, + DropdownMenuSeparatorElement, + DropdownMenuSeparatorNativeElement, +} from './props' + +export type { + DropdownMenuSubContentProps, + DropdownMenuSubContentEmits, + DropdownMenuSubContentElement, + DropdownMenuSubContentNativeElement, +} from './props' + +export type { + DropdownMenuSubTriggerProps, + DropdownMenuSubTriggerEmits, + DropdownMenuSubTriggerElement, + DropdownMenuSubTriggerNativeElement, +} from './props' + +export type { + DropdownMenuSubProps, + DropdownMenuSubEmits, +} from './props' + +export type { + DropdownMenuTriggerProps, + DropdownMenuTriggerEmits, + DropdownMenuTriggerElement, + DropdownMenuTriggerNativeElement, +} from './props' + +export type { + DropdownMenuProps, + DropdownMenuEmits, +} from './props' diff --git a/packages/components/dropdown-menu/src/props.ts b/packages/components/dropdown-menu/src/props.ts new file mode 100644 index 000000000..09abc8c90 --- /dev/null +++ b/packages/components/dropdown-menu/src/props.ts @@ -0,0 +1,423 @@ +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 type { DismissableLayerEmits } from '@oku-ui/dismissable-layer' +import type { FocusScopeEmits } from '@oku-ui/focus-scope' +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 ScopedDropdownMenu

= P & { scopeOkuDropdownMenu?: Scope } + +export const scopedDropdownMenuProps = { + scopeOkuDropdownMenu: { + ...ScopePropObject, + }, +} + +// NAMES +export const DROPDOWN_MENU_NAME = 'OkuDropdownMenu' +export const DROPDOWN_MENU_TRIGGER_NAME = 'OkuDropdownMenuTrigger' +export const DROPDOWN_MENU_PORTAL_NAME = 'OkuDropdownMenuPortal' +export const DROPDOWN_MENU_CONTENT_NAME = 'OkuDropdownMenuContent' +export const DROPDOWN_MENU_GROUP_NAME = 'OkuDropdownMenuGroup' +export const DROPDOWN_MENU_LABEL_NAME = 'OkuDropdownMenuLabel' +export const DROPDOWN_MENU_ITEM_NAME = 'OkuDropdownMenuItem' +export const DROPDOWN_MENU_CHECKBOX_ITEM_NAME = 'OkuDropdownMenuCheckboxItem' +export const DROPDOWN_MENU_RADIO_GROUP_NAME = 'OkuDropdownMenuRadioGroup' +export const DROPDOWN_MENU_RADIO_ITEM_NAME = 'OkuDropdownMenuRadioItem' +export const DROPDOWN_MENU_ITEM_INDICATOR_NAME = 'OkuDropdownMenuItemIndicator' +export const DROPDOWN_MENU_SEPARATOR_NAME = 'OkuDropdownMenuSeparator' +export const DROPDOWN_MENU_ARROW_NAME = 'OkuDropdownMenuArrow' +export const DROPDOWN_MENU_SUB_NAME = 'OkuDropdownMenuSub' +export const DROPDOWN_MENU_SUB_TRIGGER_NAME = 'OkuDropdownMenuSubTrigger' +export const DROPDOWN_MENU_SUB_CONTENT_NAME = 'OkuDropdownMenuSubContent' + +/* ------------------------------------------------------------------------------------------------- + * DropdownMenu - dropdown-menu.ts + * ----------------------------------------------------------------------------------------------- */ + +export const [createDropdownMenuProvide, createDropdownMenuScop] = createProvideScope(DROPDOWN_MENU_NAME, [ + createMenuScope, +]) + +export const useMenuScope = createMenuScope() + +export type DropdownMenuInjectValue = { + triggerId: Ref + triggerRef: Ref + contentId: Ref + open: Ref + onOpenChange(open: boolean): void + onOpenToggle(): void + modal: Ref + +} + +export const [dropdownMenuProvider, useDropdownMenuInject] + = createDropdownMenuProvide(DROPDOWN_MENU_NAME) + +export interface DropdownMenuProps { + dir?: Direction + open?: boolean + defaultOpen?: boolean + modelValue?: boolean + modal: boolean +} + +export type DropdownMenuEmits = { + openChange: [open: boolean] + 'update:modelValue': [open: boolean] +} + +export const dropdownMenuProps = { + props: { + dir: { + type: String as PropType, + }, + open: { + type: Boolean as PropType, + default: undefined, + }, + defaultOpen: { + type: Boolean as PropType, + default: undefined, + }, + modelValue: { + type: Boolean as PropType, + default: undefined, + }, + modal: { + type: Boolean as PropType, + default: true, + }, + }, + emits: { + // eslint-disable-next-line unused-imports/no-unused-vars + 'openChange': (open: DropdownMenuEmits['openChange'][0]) => true, + // eslint-disable-next-line unused-imports/no-unused-vars + 'update:modelValue': (open: DropdownMenuEmits['update:modelValue'][0]) => true, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuTrigger - dropdown-menu-trigger.ts + * ----------------------------------------------------------------------------------------------- */ + +export type DropdownMenuTriggerNativeElement = OkuElement<'button'> +export type DropdownMenuTriggerElement = HTMLButtonElement + +export interface DropdownMenuTriggerProps extends PrimitiveProps { + disabled: boolean +} + +export type DropdownMenuTriggerEmits = { + pointerdown: [event: PointerEvent] + keydown: [event: KeyboardEvent] +} + +export const dropdownMenuTriggerProps = { + props: { + disabled: { + type: Boolean as PropType, + default: false, + }, + ...primitiveProps, + }, + emits: { + // eslint-disable-next-line unused-imports/no-unused-vars + pointerdown: (event: DropdownMenuTriggerEmits['pointerdown'][0]) => true, + // eslint-disable-next-line unused-imports/no-unused-vars + keydown: (event: DropdownMenuTriggerEmits['keydown'][0]) => true, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuPortal - dropdown-menu-portal.ts + * ----------------------------------------------------------------------------------------------- */ + +export interface DropdownMenuPortalProps extends MenuPortalProps { } + +export const dropdownMenuPortalProps = { + props: { + ...menuPortalProps.props, + }, + emits: { + ...menuPortalProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuContent - dropdown-menu-content.ts + * ----------------------------------------------------------------------------------------------- */ + +export type DropdownMenuContentNativeElement = MenuContentNativeElement +export type DropdownMenuContentElement = MenuContentElement + +export interface DropdownMenuContentProps extends MenuContentProps { } + +export type DropdownMenuContentEmits = { + closeAutoFocus: [event: FocusScopeEmits['unmountAutoFocus'][0]] + interactOutside: [event: DismissableLayerEmits['interactOutside'][0]] +} + +export const dropdownMenuContentProps = { + props: { + ...menuContentProps.props, + }, + emits: { + ...propsOmit(menuContentProps.emits, ['entryFocus']), + // eslint-disable-next-line unused-imports/no-unused-vars + closeAutoFocus: (event: DropdownMenuContentEmits['closeAutoFocus'][0]) => true, + // eslint-disable-next-line unused-imports/no-unused-vars + interactOutside: (event: DropdownMenuContentEmits['interactOutside'][0]) => true, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuGroup - dropdown-menu-group.ts + * ----------------------------------------------------------------------------------------------- */ + +export type DropdownMenuGroupNativeElement = MenuGroupNativeElement +export type DropdownMenuGroupElement = MenuGroupElement + +export interface DropdownMenuGroupProps extends MenuGroupProps { } + +export const dropdownMenuGroupProps = { + props: { + ...menuGroupProps.props, + }, + emits: { + ...menuGroupProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuLabel - dropdown-menu-label.ts + * ----------------------------------------------------------------------------------------------- */ + +export type DropdownMenuLabelNativeElement = MenuLabelNativeElement +export type DropdownMenuLabelElement = MenuLabelElement + +export interface DropdownMenuLabelProps extends MenuLabelProps { } + +export const dropdownMenuLabelProps = { + props: { + ...menuLabelProps.props, + }, + emits: { + ...menuLabelProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuItem - dropdown-menu-item.ts + * ----------------------------------------------------------------------------------------------- */ + +export type DropdownMenuItemNativeElement = MenuItemNativeElement +export type DropdownMenuItemElement = MenuItemElement + +export interface DropdownMenuItemProps extends MenuItemProps { } + +export interface DropdownMenuItemEmits extends MenuItemEmits { } + +export const dropdownMenuItemProps = { + props: { + ...menuItemProps.props, + }, + emits: { + ...menuItemProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuCheckboxItem - dropdown-menu-Checkbox-item.ts + * ----------------------------------------------------------------------------------------------- */ + +export type DropdownMenuCheckboxItemNativeElement = MenuCheckboxItemNativeElement +export type DropdownMenuCheckboxItemElement = MenuCheckboxItemElement + +export interface DropdownMenuCheckboxItemProps extends MenuCheckboxItemProps { } + +export interface DropdownMenuCheckboxItemEmits extends MenuCheckboxItemEmits { } + +export const dropdownMenuCheckboxItemProps = { + props: { + ...menuCheckboxItemProps.props, + }, + emits: { + ...menuCheckboxItemProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuRadioGroup - dropdown-menu-radio-group.ts + * ----------------------------------------------------------------------------------------------- */ + +export type DropdownMenuRadioGroupNativeElement = MenuRadioGroupNativeElement +export type DropdownMenuRadioGroupElement = MenuRadioGroupElement + +export interface DropdownMenuRadioGroupProps extends MenuRadioGroupProps { } + +export const dropdownMenuRadioGroupProps = { + props: { + ...menuRadioGroupProps.props, + }, + emits: { + ...menuRadioGroupProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuRadioItem - dropdown-menu-radio-item.ts + * ----------------------------------------------------------------------------------------------- */ + +export type DropdownMenuRadioItemNativeElement = MenuRadioItemNativeElement +export type DropdownMenuRadioItemElement = MenuRadioItemElement + +export interface DropdownMenuRadioItemProps extends MenuRadioItemProps { } + +export interface DropdownMenuRadioItemEmits extends MenuRadioItemEmits { } + +export const dropdownMenuRadioItemProps = { + props: { + ...menuRadioItemProps.props, + }, + emits: { + ...menuRadioItemProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuItemIndicator - dropdown-menu--item-indicator.ts + * ----------------------------------------------------------------------------------------------- */ + +export type DropdownMenuItemIndicatorNativeElement = MenuItemIndicatorNativeElement +export type DropdownMenuItemIndicatorElement = MenuItemIndicatorElement + +export interface DropdownMenuItemIndicatorProps extends MenuItemIndicatorProps { } + +export const dropdownMenuItemIndicatorProps = { + props: { + ...menuItemIndicatorProps.props, + }, + emits: { + ...menuItemIndicatorProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuSeparator - dropdown-menu-separator.ts + * ----------------------------------------------------------------------------------------------- */ + +export type DropdownMenuSeparatorNativeElement = MenuSeparatorNativeElement +export type DropdownMenuSeparatorElement = MenuSeparatorElement + +export interface DropdownMenuSeparatorProps extends MenuSeparatorProps { } + +export const dropdownMenuSeparatorProps = { + props: { + ...menuSeparatorProps.props, + }, + emits: { + ...menuSeparatorProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuArrow - dropdown-menu-arrow.ts + * ----------------------------------------------------------------------------------------------- */ + +export type DropdownMenuArrowNativeElement = MenuArrowNativeElement +export type DropdownMenuArrowElement = MenuArrowElement + +export interface DropdownMenuArrowProps extends MenuArrowProps { } + +export const dropdownMenuArrowProps = { + props: { + ...menuArrowProps.props, + }, + emits: { + ...menuArrowProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuSub - dropdown-menu-sub.ts + * ----------------------------------------------------------------------------------------------- */ + +export interface DropdownMenuSubProps { + open?: boolean + defaultOpen?: boolean + modelValue?: boolean +} + +export type DropdownMenuSubEmits = { + openChange: [open: boolean] + 'update:modelValue': [open: boolean] +} + +export const dropdownMenuSubProps = { + props: { + open: { + type: Boolean as PropType, + default: undefined, + }, + defaultOpen: { + type: Boolean as PropType, + default: undefined, + }, + modelValue: { + type: Boolean as PropType, + default: undefined, + }, + }, + emits: { + // eslint-disable-next-line unused-imports/no-unused-vars + 'openChange': (open: DropdownMenuSubEmits['openChange'][0]) => true, + // eslint-disable-next-line unused-imports/no-unused-vars + 'update:modelValue': (open: DropdownMenuSubEmits['update:modelValue'][0]) => true, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuSubTrigger - dropdown-menu-sub-trigger.ts + * ----------------------------------------------------------------------------------------------- */ + +export type DropdownMenuSubTriggerNativeElement = MenuSubTriggerNativeElement +export type DropdownMenuSubTriggerElement = MenuSubTriggerElement + +export interface DropdownMenuSubTriggerProps extends MenuSubTriggerProps { } + +export interface DropdownMenuSubTriggerEmits extends MenuSubTriggerEmits { } + +export const dropdownMenuSubTriggerProps = { + props: { + ...menuSubTriggerProps.props, + }, + emits: { + ...menuSubTriggerProps.emits, + }, +} + +/* ------------------------------------------------------------------------------------------------- + * DropdownMenuSubContent - dropdown-menu-sub-content.ts + * ----------------------------------------------------------------------------------------------- */ + +export type DropdownMenuSubContentNativeElement = MenuSubContentNativeElement +export type DropdownMenuSubContentElement = MenuSubContentElement + +export interface DropdownMenuSubContentProps extends MenuSubContentProps { } + +export interface DropdownMenuSubContentEmits extends MenuSubContentEmits { } + +export const dropdownMenuSubContentProps = { + props: { + ...menuSubContentProps.props, + }, + emits: { + ...menuSubContentProps.emits, + }, +} diff --git a/packages/components/dropdown-menu/tsconfig.json b/packages/components/dropdown-menu/tsconfig.json new file mode 100644 index 000000000..887182612 --- /dev/null +++ b/packages/components/dropdown-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..c76c6003a 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' @@ -51,12 +52,14 @@ export type { export type { MenuContentImplProps, + MenuContentImplEmits, MenuContentImplElement, MenuContentImplNativeElement, } from './props' export type { MenuRootContentTypeProps, + MenuRootContentTypeEmits, MenuRootContentTypeElement, MenuRootContentTypeNativeElement, } from './props' @@ -75,12 +78,14 @@ export type { export type { MenuItemProps, + MenuItemEmits, MenuItemElement, MenuItemNativeElement, } from './props' export type { MenuItemImplProps, + MenuItemImplEmits, MenuItemImplElement, MenuItemImplNativeElement, } from './props' @@ -105,6 +110,7 @@ export type { export type { MenuRadioItemProps, + MenuRadioItemEmits, MenuRadioItemElement, MenuRadioItemNativeElement, } from './props' @@ -117,16 +123,36 @@ export type { export type { MenuSubProps, + MenuSubEmits, } from './props' 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/packages/core/menu/src/props.ts b/packages/core/menu/src/props.ts index 36331be35..5cc72e6dd 100644 --- a/packages/core/menu/src/props.ts +++ b/packages/core/menu/src/props.ts @@ -178,7 +178,7 @@ export const menuPortalProps = { } /* ------------------------------------------------------------------------------------------------- - * MenuContentImpl - menu-content-impl + * MenuContentImpl - menu-content-impl * ----------------------------------------------------------------------------------------------- */ export type MenuContentImplNativeElement = PopperContentNaviteElement @@ -292,7 +292,7 @@ export const menuContentImplProps = { } /* ------------------------------------------------------------------------------------------------- - * MenuRootContentModal - menu-root-content-modal.ts + * MenuRootContentModal - menu-root-content-modal.ts * ----------------------------------------------------------------------------------------------- */ export type MenuRootContentTypeNativeElement = MenuContentImplNativeElement @@ -301,6 +301,7 @@ export type MenuRootContentTypeElement = MenuContentImplElement export interface MenuRootContentTypeProps extends Omit { } export type MenuRootContentTypeEmits = Omit + export const menuRootContentTypeProps = { props: { ...propsOmit(menuContentImplProps.props, menuContentImplPrivateProps.propsKeys), @@ -444,6 +445,7 @@ export const menuItemImplProps = { export const ITEM_SELECT = 'menu.itemSelect' export type MenuItemNativeElement = MenuItemImplNativeElement + export type MenuItemElement = MenuItemImplElement export interface MenuItemProps extends MenuItemImplProps { } @@ -619,6 +621,7 @@ export const menuSeparatorProps = { export type MenuArrowNativeElement = PopperArrowNaviteElement export type MenuArrowElement = PopperArrowElement + export interface MenuArrowProps extends PopperArrowProps { } export const menuArrowProps = { diff --git a/playground/nuxt3/package.json b/playground/nuxt3/package.json index e5fb570c8..6f62eb148 100644 --- a/playground/nuxt3/package.json +++ b/playground/nuxt3/package.json @@ -22,6 +22,7 @@ "@oku-ui/collection": "workspace:^", "@oku-ui/dialog": "workspace:^", "@oku-ui/dismissable-layer": "workspace:^", + "@oku-ui/dropdown-menu": "workspace:^", "@oku-ui/focus-scope": "workspace:^", "@oku-ui/hover-card": "workspace:^", "@oku-ui/label": "workspace:^", diff --git a/playground/vue3/package.json b/playground/vue3/package.json index 84028855d..fb53bf092 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/dropdown-menu": "workspace:^", "@oku-ui/label": "workspace:^", "@oku-ui/menu": "workspace:^", "@oku-ui/primitives": "workspace:^", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aa0ece46..c3297b186 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,7 @@ overrides: '@oku-ui/hover-card': workspace:^ '@oku-ui/label': workspace:^ '@oku-ui/menu': workspace:^ + '@oku-ui/dropdown-menu': workspace:^ '@oku-ui/popover': workspace:^ '@oku-ui/popper': workspace:^ '@oku-ui/portal': workspace:^ @@ -97,6 +98,9 @@ importers: '@oku-ui/dismissable-layer': specifier: workspace:^ version: link:packages/core/dismissable-layer + '@oku-ui/dropdown-menu': + specifier: workspace:^ + version: link:packages/components/dropdown-menu '@oku-ui/focus-guards': specifier: workspace:^ version: link:packages/core/focus-guards @@ -507,6 +511,43 @@ importers: specifier: workspace:^ version: link:../../tsconfig + packages/components/dropdown-menu: + dependencies: + '@floating-ui/vue': + specifier: ^1.0.2 + version: 1.0.2(vue@3.3.7) + '@oku-ui/direction': + specifier: workspace:^ + version: link:../../core/direction + '@oku-ui/dismissable-layer': + specifier: workspace:^ + version: link:../../core/dismissable-layer + '@oku-ui/focus-scope': + specifier: workspace:^ + version: link:../../core/focus-scope + '@oku-ui/menu': + specifier: workspace:^ + version: link:../../core/menu + '@oku-ui/primitive': + specifier: workspace:^ + version: link:../../core/primitive + '@oku-ui/provide': + specifier: workspace:^ + version: link:../../core/provide + '@oku-ui/use-composable': + specifier: workspace:^ + version: link:../../core/use-composable + '@oku-ui/utils': + specifier: workspace:^ + version: link:../../core/utils + vue: + specifier: 3.3.7 + version: 3.3.7(typescript@5.2.2) + devDependencies: + tsconfig: + specifier: workspace:^ + version: link:../../tsconfig + packages/components/hover-card: dependencies: '@floating-ui/vue': @@ -1525,6 +1566,9 @@ importers: '@oku-ui/dismissable-layer': specifier: workspace:^ version: link:../../packages/core/dismissable-layer + '@oku-ui/dropdown-menu': + specifier: workspace:^ + version: link:../../packages/components/dropdown-menu '@oku-ui/focus-scope': specifier: workspace:^ version: link:../../packages/core/focus-scope @@ -1616,6 +1660,9 @@ importers: '@oku-ui/checkbox': specifier: workspace:^ version: link:../../packages/components/checkbox + '@oku-ui/dropdown-menu': + specifier: workspace:^ + version: link:../../packages/components/dropdown-menu '@oku-ui/label': specifier: workspace:^ version: link:../../packages/components/label diff --git a/vitest.config.ts b/vitest.config.ts index 3aa08533f..cad41e1f3 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -44,6 +44,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/dropdown': 'packages/components/dropdown/src', '@oku-ui/menu': 'packages/core/menu/src', '@oku-ui/dismissable-layer': 'packages/core/dismissable-layer/src',