CommonPopoverMenu.vue 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. <!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { computed, type SetupContext, toRefs, useSlots } from 'vue'
  4. import type { CommonPopoverInstance } from '#shared/components/CommonPopover/types.ts'
  5. import type { ObjectLike } from '#shared/types/utils.ts'
  6. import { usePopoverMenu } from '#desktop/components/CommonPopoverMenu/usePopoverMenu.ts'
  7. import CommonPopoverMenuItem from './CommonPopoverMenuItem.vue'
  8. import type { MenuItem, Variant } from './types'
  9. export interface Props {
  10. popover: CommonPopoverInstance | undefined
  11. headerLabel?: string
  12. items?: MenuItem[]
  13. entity?: ObjectLike
  14. }
  15. const props = defineProps<Props>()
  16. const { items, entity } = toRefs(props)
  17. const { filteredMenuItems } = usePopoverMenu(items, entity)
  18. /**
  19. * Workaround to satisfy linter
  20. * @bug https://github.com/vuejs/language-tools/issues/5082
  21. * Wait to be closed
  22. * */
  23. const slots: SetupContext['slots'] = useSlots()
  24. const showHeaderLabel = computed(() => {
  25. if (!filteredMenuItems.value && !slots.default) return false
  26. return slots.header || props.headerLabel
  27. })
  28. const onClickItem = (event: MouseEvent, item: MenuItem) => {
  29. if (item.onClick) {
  30. event.preventDefault()
  31. item.onClick(props.entity)
  32. }
  33. if (!item.noCloseOnClick) {
  34. props.popover?.closePopover()
  35. }
  36. }
  37. const getHoverFocusStyles = (variant?: Variant) => {
  38. if (variant === 'secondary') {
  39. return 'focus-within:bg-blue-500 hover:bg-blue-500 hover:focus-within:bg-blue-500 dark:focus-within:bg-blue-950 dark:hover:bg-blue-950 dark:hover:focus-within:bg-blue-950'
  40. }
  41. if (variant === 'danger') {
  42. return 'focus-within:bg-pink-100 hover:bg-pink-100 hover:focus-within:bg-pink-100 dark:focus-within:bg-red-900 dark:hover:bg-red-900 dark:hover:focus-within:bg-red-900'
  43. }
  44. return 'focus-within:bg-blue-800 focus-within:text-white hover:bg-blue-600 hover:focus-within:bg-blue-800 dark:hover:bg-blue-900 dark:hover:focus-within:bg-blue-800'
  45. }
  46. </script>
  47. <template>
  48. <section class="min-w-58 flex max-w-64 flex-col gap-0.5">
  49. <div
  50. v-if="showHeaderLabel"
  51. role="heading"
  52. aria-level="2"
  53. class="px-2 py-1.5"
  54. >
  55. <slot name="header">
  56. <CommonLabel
  57. size="small"
  58. class="line-clamp-1 text-stone-200 dark:text-neutral-500"
  59. >{{ i18n.t(headerLabel) }}
  60. </CommonLabel>
  61. </slot>
  62. </div>
  63. <template v-if="filteredMenuItems || $slots.default">
  64. <slot>
  65. <ul role="menu" v-bind="$attrs" class="flex w-full flex-col">
  66. <template v-for="item in filteredMenuItems" :key="item.key">
  67. <li
  68. v-if="'array' in item"
  69. class="flex flex-col overflow-clip pt-2.5 last:rounded-b-[10px] [&:nth-child(n+2)]:border-t [&:nth-child(n+2)]:border-neutral-100 [&:nth-child(n+2)]:dark:border-gray-900"
  70. role="menuitem"
  71. >
  72. <CommonLabel
  73. size="small"
  74. class="line-clamp-1 px-2 text-stone-200 dark:text-neutral-500"
  75. role="heading"
  76. aria-level="3"
  77. >{{ item.groupLabel }}</CommonLabel
  78. >
  79. <template v-for="subItem in item.array" :key="subItem.key">
  80. <slot :name="`item-${subItem.key}`" v-bind="subItem">
  81. <component
  82. :is="subItem.component || CommonPopoverMenuItem"
  83. class="flex grow p-2.5"
  84. :class="getHoverFocusStyles(subItem.variant)"
  85. :label="subItem.label"
  86. :variant="subItem.variant"
  87. :link="subItem.link"
  88. :icon="subItem.icon"
  89. :label-placeholder="subItem.labelPlaceholder"
  90. @click="onClickItem($event, subItem)"
  91. />
  92. <slot :name="`itemRight-${subItem.key}`" v-bind="subItem" />
  93. </slot>
  94. </template>
  95. </li>
  96. <li
  97. v-else
  98. role="menuitem"
  99. class="group flex items-center justify-between last:rounded-b-[10px]"
  100. :class="[
  101. {
  102. 'first:rounded-t-[10px]': !showHeaderLabel,
  103. 'border-t border-neutral-100 dark:border-gray-900':
  104. item.separatorTop,
  105. },
  106. getHoverFocusStyles(item.variant),
  107. ]"
  108. >
  109. <slot :name="`item-${item.key}`" v-bind="item">
  110. <component
  111. :is="item.component || CommonPopoverMenuItem"
  112. class="flex grow p-2.5"
  113. :label="item.label"
  114. :variant="item.variant"
  115. :link="item.link"
  116. :icon="item.icon"
  117. :label-placeholder="item.labelPlaceholder"
  118. @click="onClickItem($event, item)"
  119. />
  120. <slot :name="`itemRight-${item.key}`" v-bind="item" />
  121. </slot>
  122. </li>
  123. </template>
  124. </ul>
  125. </slot>
  126. </template>
  127. </section>
  128. </template>