CommonDialog.vue 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { onKeyUp } from '@vueuse/core'
  4. import { nextTick, onMounted, ref } from 'vue'
  5. import { useTrapTab } from '#shared/composables/useTrapTab.ts'
  6. import stopEvent from '#shared/utils/events.ts'
  7. import { getFirstFocusableElement } from '#shared/utils/getFocusableElements.ts'
  8. import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
  9. import CommonOverlayContainer from '#desktop/components/CommonOverlayContainer/CommonOverlayContainer.vue'
  10. import CommonDialogActionFooter, {
  11. type Props as ActionFooterProps,
  12. } from './CommonDialogActionFooter.vue'
  13. import { closeDialog } from './useDialog.ts'
  14. export interface Props {
  15. name: string
  16. headerTitle?: string
  17. headerIcon?: string
  18. content?: string
  19. contentPlaceholder?: string[]
  20. hideFooter?: boolean
  21. /**
  22. * Inner wrapper for the dialog content.
  23. * */
  24. wrapperTag?: 'div' | 'article'
  25. footerActionOptions?: ActionFooterProps
  26. // Don't focus the first element inside a Dialog after being mounted
  27. // if nothing is focusable, will focus "Close" button when dismissable is active.
  28. noAutofocus?: boolean
  29. }
  30. const props = withDefaults(defineProps<Props>(), {
  31. wrapperTag: 'div',
  32. })
  33. defineOptions({
  34. inheritAttrs: false,
  35. })
  36. const emit = defineEmits<{
  37. close: [cancel: boolean]
  38. }>()
  39. const dialogElement = ref<HTMLElement>()
  40. const footerElement = ref<HTMLElement>()
  41. const contentElement = ref<HTMLElement>()
  42. const close = async (cancel = true) => {
  43. emit('close', cancel)
  44. await closeDialog(props.name)
  45. }
  46. const dialogId = `dialog-${props.name}`
  47. onKeyUp('Escape', (e) => {
  48. stopEvent(e)
  49. close()
  50. })
  51. useTrapTab(dialogElement)
  52. onMounted(() => {
  53. if (props.noAutofocus) return
  54. // Will try to find focusable element inside dialog main and footer content.
  55. // If it won't find it, will try to find inside the header most likely will find "Close" button.
  56. const firstFocusable =
  57. getFirstFocusableElement(contentElement.value) ||
  58. getFirstFocusableElement(footerElement.value) ||
  59. getFirstFocusableElement(dialogElement.value)
  60. nextTick(() => {
  61. firstFocusable?.focus()
  62. firstFocusable?.scrollIntoView({ block: 'nearest' })
  63. })
  64. })
  65. </script>
  66. <template>
  67. <CommonOverlayContainer
  68. :id="dialogId"
  69. tag="div"
  70. class="fixed top-[50%] z-50 w-[500px] translate-y-[-50%] ltr:left-[50%] ltr:translate-x-[-50%] rtl:right-[50%] rtl:-translate-x-[-50%]"
  71. role="dialog"
  72. :aria-labelledby="`${dialogId}-title`"
  73. @click-background="close()"
  74. >
  75. <component
  76. :is="wrapperTag"
  77. ref="dialogElement"
  78. data-common-dialog
  79. class="flex flex-col gap-3 rounded-xl border border-neutral-100 bg-white p-3 dark:border-gray-900 dark:bg-gray-500"
  80. >
  81. <div class="flex items-center justify-between bg-white dark:bg-gray-500">
  82. <slot name="header">
  83. <div
  84. class="flex items-center gap-2 text-xl leading-snug text-gray-100 dark:text-neutral-400"
  85. >
  86. <CommonIcon v-if="headerIcon" size="small" :name="headerIcon" />
  87. <h3 :id="`${dialogId}-title`">{{ $t(headerTitle) }}</h3>
  88. </div>
  89. </slot>
  90. <CommonButton
  91. class="ms-auto"
  92. variant="neutral"
  93. size="medium"
  94. icon="x-lg"
  95. :aria-label="$t('Close dialog')"
  96. @click="close()"
  97. />
  98. </div>
  99. <div ref="contentElement" v-bind="$attrs" class="py-6 text-center">
  100. <slot>
  101. <CommonLabel size="large">{{
  102. $t(content, ...(contentPlaceholder || []))
  103. }}</CommonLabel>
  104. </slot>
  105. </div>
  106. <div v-if="$slots.footer || !hideFooter" ref="footerElement">
  107. <slot name="footer">
  108. <CommonDialogActionFooter
  109. v-bind="footerActionOptions"
  110. @cancel="close()"
  111. @action="close(false)"
  112. />
  113. </slot>
  114. </div>
  115. </component>
  116. </CommonOverlayContainer>
  117. </template>