CommonDialog.vue 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { onKeyUp } from '@vueuse/core'
  4. import { useTemplateRef, nextTick, onMounted } 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 = useTemplateRef<HTMLElement>('dialog')
  40. const footerElement = useTemplateRef('footer')
  41. const contentElement = useTemplateRef('content')
  42. const close = async (cancel?: boolean) => {
  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. backdrop-class="z-40"
  72. role="dialog"
  73. :aria-labelledby="`${dialogId}-title`"
  74. @click-background="close()"
  75. >
  76. <component
  77. :is="wrapperTag"
  78. ref="dialog"
  79. data-common-dialog
  80. class="flex flex-col gap-3 rounded-xl border border-neutral-100 bg-neutral-50 p-3 dark:border-gray-900 dark:bg-gray-500"
  81. >
  82. <div
  83. class="flex items-center justify-between bg-neutral-50 dark:bg-gray-500"
  84. >
  85. <slot name="header">
  86. <div
  87. class="flex items-center gap-2 text-xl leading-snug text-gray-100 dark:text-neutral-400"
  88. >
  89. <CommonIcon v-if="headerIcon" size="small" :name="headerIcon" />
  90. <h3 :id="`${dialogId}-title`">{{ $t(headerTitle) }}</h3>
  91. </div>
  92. </slot>
  93. <CommonButton
  94. class="ms-auto"
  95. variant="neutral"
  96. size="medium"
  97. icon="x-lg"
  98. :aria-label="$t('Close dialog')"
  99. @click="close()"
  100. />
  101. </div>
  102. <div ref="content" v-bind="$attrs" class="py-6 text-center">
  103. <slot>
  104. <CommonLabel size="large">{{
  105. $t(content, ...(contentPlaceholder || []))
  106. }}</CommonLabel>
  107. </slot>
  108. </div>
  109. <div v-if="$slots.footer || !hideFooter" ref="footer">
  110. <slot name="footer">
  111. <CommonDialogActionFooter
  112. v-bind="footerActionOptions"
  113. @cancel="close(true)"
  114. @action="close(false)"
  115. />
  116. </slot>
  117. </div>
  118. </component>
  119. </CommonOverlayContainer>
  120. </template>