drawer.vue 7.2 KB

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