CommonActionMenu.vue 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. <!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { computed, toRefs } from 'vue'
  4. import type { Sizes } from '#shared/components/CommonIcon/types.ts'
  5. import CommonPopover from '#shared/components/CommonPopover/CommonPopover.vue'
  6. import type {
  7. Orientation,
  8. Placement,
  9. } from '#shared/components/CommonPopover/types.ts'
  10. import { usePopover } from '#shared/components/CommonPopover/usePopover.ts'
  11. import type { ObjectLike } from '#shared/types/utils.ts'
  12. import getUuid from '#shared/utils/getUuid.ts'
  13. import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
  14. import type {
  15. ButtonSize,
  16. ButtonVariant,
  17. } from '#desktop/components/CommonButton/types.ts'
  18. import CommonPopoverMenu from '#desktop/components/CommonPopoverMenu/CommonPopoverMenu.vue'
  19. import type { MenuItem } from '#desktop/components/CommonPopoverMenu/types.ts'
  20. import { usePopoverMenu } from '#desktop/components/CommonPopoverMenu/usePopoverMenu.ts'
  21. export interface Props {
  22. actions: MenuItem[]
  23. entity?: ObjectLike
  24. buttonSize?: ButtonSize
  25. linkSize?: Sizes
  26. placement?: Placement
  27. orientation?: Orientation
  28. hideArrow?: boolean
  29. disabled?: boolean
  30. noSingleActionMode?: boolean
  31. customMenuButtonLabel?: string
  32. defaultIcon?: string
  33. defaultButtonVariant?: ButtonVariant | 'neutral-light' | 'neutral-dark'
  34. noPaddedDefaultButton?: boolean
  35. noSmallRoundingDefaultButton?: boolean
  36. }
  37. const props = withDefaults(defineProps<Props>(), {
  38. buttonSize: 'medium',
  39. placement: 'arrowStart',
  40. orientation: 'autoVertical',
  41. defaultButtonVariant: 'neutral',
  42. defaultIcon: 'three-dots-vertical',
  43. noPaddedDefaultButton: true,
  44. })
  45. const { popover, isOpen: popoverIsOpen, popoverTarget, toggle } = usePopover()
  46. const { actions, entity } = toRefs(props)
  47. const { filteredMenuItems, singleMenuItemPresent, singleMenuItem } =
  48. usePopoverMenu(actions, entity, { provides: true })
  49. const entityId = computed(() => props.entity?.id || getUuid())
  50. const menuId = computed(() => `popover-${entityId.value}`)
  51. const singleActionAriaLabel = computed(() => {
  52. if (typeof singleMenuItem.value?.ariaLabel === 'function') {
  53. return singleMenuItem.value.ariaLabel(props.entity)
  54. }
  55. return singleMenuItem.value?.ariaLabel || singleMenuItem.value?.label
  56. })
  57. const buttonVariantClassExtension = computed(() => {
  58. const hoverClass = 'dark:hover:border-blue-700! hover:border-blue-800!'
  59. if (props.defaultButtonVariant === 'neutral-dark')
  60. return ` border! border-neutral-100! bg-neutral-50! hover:bg-white! hover:dark:bg-gray-500! text-gray-100! dark:border-gray-900! dark:bg-gray-500! dark:text-neutral-400! ${
  61. popoverIsOpen.value ? 'border-transparent!' : hoverClass
  62. }`
  63. if (props.defaultButtonVariant === 'neutral-light')
  64. return ` border! border-neutral-100! bg-blue-100! text-gray-100! dark:border-gray-900! dark:bg-stone-500! dark:text-neutral-400! ${
  65. popoverIsOpen.value ? '' : hoverClass
  66. }`
  67. return ''
  68. })
  69. const singleActionMode = computed(() => {
  70. if (props.noSingleActionMode) return false
  71. return singleMenuItemPresent.value
  72. })
  73. const variantClasses = computed(() => {
  74. if (singleMenuItem.value?.variant === 'secondary') return 'text-blue-800!'
  75. if (singleMenuItem.value?.variant === 'danger') return 'text-red-500!'
  76. return 'text-stone-200! dark:text-neutral-500!'
  77. })
  78. </script>
  79. <template>
  80. <div
  81. v-if="filteredMenuItems && filteredMenuItems.length > 0"
  82. class="inline-block"
  83. >
  84. <template v-if="singleActionMode">
  85. <CommonLink
  86. v-if="singleMenuItem?.link"
  87. v-tooltip="$t(singleActionAriaLabel)"
  88. class="flex focus:outline-hidden focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-blue-800"
  89. :aria-label="$t(singleActionAriaLabel)"
  90. :disabled="disabled"
  91. :link="singleMenuItem.link"
  92. >
  93. <CommonIcon
  94. v-if="singleMenuItem?.icon"
  95. :size="linkSize"
  96. :class="variantClasses"
  97. :name="singleMenuItem?.icon"
  98. />
  99. </CommonLink>
  100. <CommonButton
  101. v-else
  102. v-tooltip="$t(singleActionAriaLabel)"
  103. class="rounded-xs p-0"
  104. :class="[variantClasses]"
  105. :size="buttonSize"
  106. :disabled="disabled"
  107. :aria-label="$t(singleActionAriaLabel)"
  108. :icon="singleMenuItem?.icon"
  109. @click="singleMenuItem?.onClick?.(props.entity as ObjectLike)"
  110. />
  111. </template>
  112. <template v-else>
  113. <CommonButton
  114. :id="`action-menu-${entityId}`"
  115. ref="popoverTarget"
  116. :aria-label="$t(customMenuButtonLabel || 'Action menu button')"
  117. aria-haspopup="true"
  118. :aria-controls="popoverIsOpen ? menuId : undefined"
  119. :disabled="disabled"
  120. class="outline-offset-0!"
  121. :class="[
  122. {
  123. 'outline! outline-blue-800!': popoverIsOpen,
  124. 'p-0': noPaddedDefaultButton,
  125. 'rounded-xs': !noSmallRoundingDefaultButton,
  126. },
  127. buttonVariantClassExtension,
  128. ]"
  129. :variant="defaultButtonVariant as ButtonVariant"
  130. :size="buttonSize"
  131. :icon="defaultIcon"
  132. @click="toggle"
  133. />
  134. <CommonPopover
  135. :id="menuId"
  136. ref="popover"
  137. :placement="placement"
  138. :hide-arrow="hideArrow"
  139. :orientation="orientation"
  140. :owner="popoverTarget"
  141. >
  142. <CommonPopoverMenu :entity="entity" :popover="popover" />
  143. </CommonPopover>
  144. </template>
  145. </div>
  146. </template>