drawer.vue 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. <script lang="ts" setup>
  2. import type { DrawerProps, ExtendedDrawerApi } from './drawer';
  3. import { provide, ref, useId, 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 { globalShareState } from '@vben-core/shared/global-state';
  25. import { cn } from '@vben-core/shared/utils';
  26. interface Props extends DrawerProps {
  27. drawerApi?: ExtendedDrawerApi;
  28. }
  29. const props = withDefaults(defineProps<Props>(), {
  30. drawerApi: undefined,
  31. });
  32. const components = globalShareState.getComponents();
  33. const id = useId();
  34. provide('DISMISSABLE_DRAWER_ID', id);
  35. const wrapperRef = ref<HTMLElement>();
  36. const { $t } = useSimpleLocale();
  37. const { isMobile } = useIsMobile();
  38. const state = props.drawerApi?.useStore?.();
  39. const {
  40. cancelText,
  41. class: drawerClass,
  42. closable,
  43. closeOnClickModal,
  44. closeOnPressEscape,
  45. confirmLoading,
  46. confirmText,
  47. contentClass,
  48. description,
  49. footer: showFooter,
  50. footerClass,
  51. header: showHeader,
  52. headerClass,
  53. loading: showLoading,
  54. modal,
  55. openAutoFocus,
  56. placement,
  57. showCancelButton,
  58. showConfirmButton,
  59. title,
  60. titleTooltip,
  61. } = usePriorityValues(props, state);
  62. watch(
  63. () => showLoading.value,
  64. (v) => {
  65. if (v && wrapperRef.value) {
  66. wrapperRef.value.scrollTo({
  67. // behavior: 'smooth',
  68. top: 0,
  69. });
  70. }
  71. },
  72. );
  73. function interactOutside(e: Event) {
  74. if (!closeOnClickModal.value) {
  75. e.preventDefault();
  76. }
  77. }
  78. function escapeKeyDown(e: KeyboardEvent) {
  79. if (!closeOnPressEscape.value) {
  80. e.preventDefault();
  81. }
  82. }
  83. // pointer-down-outside
  84. function pointerDownOutside(e: Event) {
  85. const target = e.target as HTMLElement;
  86. const dismissableDrawer = target?.dataset.dismissableDrawer;
  87. if (!closeOnClickModal.value || dismissableDrawer !== id) {
  88. e.preventDefault();
  89. }
  90. }
  91. function handerOpenAutoFocus(e: Event) {
  92. if (!openAutoFocus.value) {
  93. e?.preventDefault();
  94. }
  95. }
  96. function handleFocusOutside(e: Event) {
  97. e.preventDefault();
  98. e.stopPropagation();
  99. }
  100. </script>
  101. <template>
  102. <Sheet
  103. :modal="false"
  104. :open="state?.isOpen"
  105. @update:open="() => drawerApi?.close()"
  106. >
  107. <SheetContent
  108. :class="
  109. cn('flex w-[520px] flex-col', drawerClass, {
  110. '!w-full': isMobile || placement === 'bottom' || placement === 'top',
  111. 'max-h-[100vh]': placement === 'bottom' || placement === 'top',
  112. })
  113. "
  114. :modal="modal"
  115. :open="state?.isOpen"
  116. :side="placement"
  117. @close-auto-focus="handleFocusOutside"
  118. @escape-key-down="escapeKeyDown"
  119. @focus-outside="handleFocusOutside"
  120. @interact-outside="interactOutside"
  121. @open-auto-focus="handerOpenAutoFocus"
  122. @pointer-down-outside="pointerDownOutside"
  123. >
  124. <SheetHeader
  125. v-if="showHeader"
  126. :class="
  127. cn(
  128. '!flex flex-row items-center justify-between border-b px-6 py-5',
  129. headerClass,
  130. {
  131. 'px-4 py-3': closable,
  132. },
  133. )
  134. "
  135. >
  136. <div>
  137. <SheetTitle v-if="title" class="text-left">
  138. <slot name="title">
  139. {{ title }}
  140. <VbenHelpTooltip v-if="titleTooltip" trigger-class="pb-1">
  141. {{ titleTooltip }}
  142. </VbenHelpTooltip>
  143. </slot>
  144. </SheetTitle>
  145. <SheetDescription v-if="description" class="mt-1 text-xs">
  146. <slot name="description">
  147. {{ description }}
  148. </slot>
  149. </SheetDescription>
  150. </div>
  151. <VisuallyHidden v-if="!title || !description">
  152. <SheetTitle v-if="!title" />
  153. <SheetDescription v-if="!description" />
  154. </VisuallyHidden>
  155. <div class="flex-center">
  156. <slot name="extra"></slot>
  157. <SheetClose
  158. v-if="closable"
  159. as-child
  160. 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"
  161. >
  162. <VbenIconButton>
  163. <X class="size-4" />
  164. </VbenIconButton>
  165. </SheetClose>
  166. </div>
  167. </SheetHeader>
  168. <template v-else>
  169. <VisuallyHidden>
  170. <SheetTitle />
  171. <SheetDescription />
  172. </VisuallyHidden>
  173. </template>
  174. <div
  175. ref="wrapperRef"
  176. :class="
  177. cn('relative flex-1 overflow-y-auto p-3', contentClass, {
  178. 'overflow-hidden': showLoading,
  179. })
  180. "
  181. >
  182. <VbenLoading v-if="showLoading" class="size-full" spinning />
  183. <slot></slot>
  184. </div>
  185. <SheetFooter
  186. v-if="showFooter"
  187. :class="
  188. cn(
  189. 'w-full flex-row items-center justify-end border-t p-2 px-3',
  190. footerClass,
  191. )
  192. "
  193. >
  194. <slot name="prepend-footer"></slot>
  195. <slot name="footer">
  196. <component
  197. :is="components.DefaultButton || VbenButton"
  198. v-if="showCancelButton"
  199. variant="ghost"
  200. @click="() => drawerApi?.onCancel()"
  201. >
  202. <slot name="cancelText">
  203. {{ cancelText || $t('cancel') }}
  204. </slot>
  205. </component>
  206. <component
  207. :is="components.PrimaryButton || VbenButton"
  208. v-if="showConfirmButton"
  209. :loading="confirmLoading"
  210. @click="() => drawerApi?.onConfirm()"
  211. >
  212. <slot name="confirmText">
  213. {{ confirmText || $t('confirm') }}
  214. </slot>
  215. </component>
  216. </slot>
  217. <slot name="append-footer"></slot>
  218. </SheetFooter>
  219. </SheetContent>
  220. </Sheet>
  221. </template>