drawer.vue 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. <script lang="ts" setup>
  2. import type { DrawerProps, ExtendedDrawerApi } from './drawer';
  3. import { ref, watch } from 'vue';
  4. import {
  5. useIsMobile,
  6. usePriorityValues,
  7. useSimpleLocale,
  8. } from '@vben-core/composables';
  9. import { X } from '@vben-core/icons';
  10. import {
  11. Sheet,
  12. SheetClose,
  13. SheetContent,
  14. SheetDescription,
  15. SheetFooter,
  16. SheetHeader,
  17. SheetTitle,
  18. VbenButton,
  19. VbenHelpTooltip,
  20. VbenIconButton,
  21. VbenLoading,
  22. VisuallyHidden,
  23. } from '@vben-core/shadcn-ui';
  24. import { cn } from '@vben-core/shared/utils';
  25. interface Props extends DrawerProps {
  26. drawerApi?: ExtendedDrawerApi;
  27. }
  28. const props = withDefaults(defineProps<Props>(), {
  29. drawerApi: undefined,
  30. });
  31. const wrapperRef = ref<HTMLElement>();
  32. const { $t } = useSimpleLocale();
  33. const { isMobile } = useIsMobile();
  34. const state = props.drawerApi?.useStore?.();
  35. const {
  36. cancelText,
  37. class: drawerClass,
  38. closable,
  39. closeOnClickModal,
  40. closeOnPressEscape,
  41. confirmLoading,
  42. confirmText,
  43. contentClass,
  44. description,
  45. footer: showFooter,
  46. loading: showLoading,
  47. modal,
  48. openAutoFocus,
  49. showCancelButton,
  50. showConfirmButton,
  51. title,
  52. titleTooltip,
  53. } = usePriorityValues(props, state);
  54. watch(
  55. () => showLoading.value,
  56. (v) => {
  57. if (v && wrapperRef.value) {
  58. wrapperRef.value.scrollTo({
  59. // behavior: 'smooth',
  60. top: 0,
  61. });
  62. }
  63. },
  64. );
  65. function interactOutside(e: Event) {
  66. if (!closeOnClickModal.value) {
  67. e.preventDefault();
  68. }
  69. }
  70. function escapeKeyDown(e: KeyboardEvent) {
  71. if (!closeOnPressEscape.value) {
  72. e.preventDefault();
  73. }
  74. }
  75. // pointer-down-outside
  76. function pointerDownOutside(e: Event) {
  77. const target = e.target as HTMLElement;
  78. const isDismissableModal = !!target?.dataset.dismissableModal;
  79. if (!closeOnClickModal.value || !isDismissableModal) {
  80. e.preventDefault();
  81. }
  82. }
  83. function handerOpenAutoFocus(e: Event) {
  84. if (!openAutoFocus.value) {
  85. e?.preventDefault();
  86. }
  87. }
  88. function handleFocusOutside(e: Event) {
  89. e.preventDefault();
  90. e.stopPropagation();
  91. }
  92. </script>
  93. <template>
  94. <Sheet
  95. :modal="false"
  96. :open="state?.isOpen"
  97. @update:open="() => drawerApi?.close()"
  98. >
  99. <SheetContent
  100. :class="
  101. cn('flex w-[520px] flex-col', drawerClass, {
  102. '!w-full': isMobile,
  103. })
  104. "
  105. :modal="modal"
  106. :open="state?.isOpen"
  107. @close-auto-focus="handleFocusOutside"
  108. @escape-key-down="escapeKeyDown"
  109. @focus-outside="handleFocusOutside"
  110. @interact-outside="interactOutside"
  111. @open-auto-focus="handerOpenAutoFocus"
  112. @pointer-down-outside="pointerDownOutside"
  113. >
  114. <SheetHeader
  115. :class="
  116. cn('!flex flex-row items-center justify-between border-b px-6 py-5', {
  117. 'px-4 py-3': closable,
  118. })
  119. "
  120. >
  121. <div>
  122. <SheetTitle v-if="title" class="text-left">
  123. <slot name="title">
  124. {{ title }}
  125. <VbenHelpTooltip v-if="titleTooltip" trigger-class="pb-1">
  126. {{ titleTooltip }}
  127. </VbenHelpTooltip>
  128. </slot>
  129. </SheetTitle>
  130. <SheetDescription v-if="description" class="mt-1 text-xs">
  131. <slot name="description">
  132. {{ description }}
  133. </slot>
  134. </SheetDescription>
  135. </div>
  136. <VisuallyHidden v-if="!title || !description">
  137. <SheetTitle v-if="!title" />
  138. <SheetDescription v-if="!description" />
  139. </VisuallyHidden>
  140. <div class="flex-center">
  141. <slot name="extra"></slot>
  142. <SheetClose
  143. v-if="closable"
  144. as-child
  145. class="data-[state=open]:bg-secondary ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
  146. >
  147. <VbenIconButton>
  148. <X class="size-4" />
  149. </VbenIconButton>
  150. </SheetClose>
  151. </div>
  152. </SheetHeader>
  153. <div
  154. ref="wrapperRef"
  155. :class="
  156. cn('relative flex-1 overflow-y-auto p-3', contentClass, {
  157. 'overflow-hidden': showLoading,
  158. })
  159. "
  160. >
  161. <VbenLoading v-if="showLoading" class="size-full" spinning />
  162. <slot></slot>
  163. </div>
  164. <SheetFooter
  165. v-if="showFooter"
  166. class="w-full flex-row items-center justify-end border-t p-2 px-3"
  167. >
  168. <slot name="prepend-footer"></slot>
  169. <slot name="footer">
  170. <VbenButton
  171. v-if="showCancelButton"
  172. variant="ghost"
  173. @click="() => drawerApi?.onCancel()"
  174. >
  175. <slot name="cancelText">
  176. {{ cancelText || $t('cancel') }}
  177. </slot>
  178. </VbenButton>
  179. <VbenButton
  180. v-if="showConfirmButton"
  181. :loading="confirmLoading"
  182. @click="() => drawerApi?.onConfirm()"
  183. >
  184. <slot name="confirmText">
  185. {{ confirmText || $t('confirm') }}
  186. </slot>
  187. </VbenButton>
  188. </slot>
  189. <slot name="append-footer"></slot>
  190. </SheetFooter>
  191. </SheetContent>
  192. </Sheet>
  193. </template>