fix: improve the dialog and drawer scrollbar experience, fix internal click failure problems and warnings (#4391)
* fix: improve the dialog and drawer scrollbar experience, fix internal click failure problems and warnings * chore: remove test codepull/48/MERGE
							parent
							
								
									bd6b724aaf
								
							
						
					
					
						commit
						d27e5eeef7
					
				|  | @ -37,10 +37,10 @@ | |||
|   } | ||||
| 
 | ||||
|   body { | ||||
|     @apply !pointer-events-auto; | ||||
| 
 | ||||
|     min-height: 100vh; | ||||
| 
 | ||||
|     /* pointer-events: auto !important; */ | ||||
| 
 | ||||
|     /* overflow: overlay; */ | ||||
| 
 | ||||
|     /* -webkit-font-smoothing: antialiased; */ | ||||
|  |  | |||
|  | @ -50,3 +50,22 @@ export function getElementVisibleRect( | |||
|     width: Math.max(0, right - left), | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export function getScrollbarWidth() { | ||||
|   const scrollDiv = document.createElement('div'); | ||||
| 
 | ||||
|   scrollDiv.style.visibility = 'hidden'; | ||||
|   scrollDiv.style.overflow = 'scroll'; | ||||
|   scrollDiv.style.position = 'absolute'; | ||||
|   scrollDiv.style.top = '-9999px'; | ||||
| 
 | ||||
|   document.body.append(scrollDiv); | ||||
| 
 | ||||
|   const innerDiv = document.createElement('div'); | ||||
|   scrollDiv.append(innerDiv); | ||||
| 
 | ||||
|   const scrollbarWidth = scrollDiv.offsetWidth - innerDiv.offsetWidth; | ||||
| 
 | ||||
|   scrollDiv.remove(); | ||||
|   return scrollbarWidth; | ||||
| } | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ export * from './use-content-style'; | |||
| export * from './use-is-mobile'; | ||||
| export * from './use-namespace'; | ||||
| export * from './use-priority-value'; | ||||
| export * from './use-scroll-lock'; | ||||
| export * from './use-simple-locale'; | ||||
| export * from './use-sortable'; | ||||
| export { | ||||
|  |  | |||
|  | @ -0,0 +1,48 @@ | |||
| import { getScrollbarWidth } from '@vben-core/shared/utils'; | ||||
| 
 | ||||
| import { | ||||
|   useScrollLock as _useScrollLock, | ||||
|   tryOnBeforeMount, | ||||
|   tryOnBeforeUnmount, | ||||
| } from '@vueuse/core'; | ||||
| 
 | ||||
| export const SCROLL_FIXED_CLASS = `_scroll__fixed_`; | ||||
| 
 | ||||
| export function useScrollLock() { | ||||
|   const isLocked = _useScrollLock(document.body); | ||||
|   const scrollbarWidth = getScrollbarWidth(); | ||||
| 
 | ||||
|   tryOnBeforeMount(() => { | ||||
|     document.body.style.paddingRight = `${scrollbarWidth}px`; | ||||
| 
 | ||||
|     const layoutFixedNodes = document.querySelectorAll<HTMLElement>( | ||||
|       `.${SCROLL_FIXED_CLASS}`, | ||||
|     ); | ||||
|     const nodes = [...layoutFixedNodes]; | ||||
|     if (nodes.length > 0) { | ||||
|       nodes.forEach((node) => { | ||||
|         node.dataset.transition = node.style.transition; | ||||
|         node.style.transition = 'none'; | ||||
|         node.style.paddingRight = `${scrollbarWidth}px`; | ||||
|       }); | ||||
|     } | ||||
|     isLocked.value = true; | ||||
|   }); | ||||
| 
 | ||||
|   tryOnBeforeUnmount(() => { | ||||
|     isLocked.value = false; | ||||
|     const layoutFixedNodes = document.querySelectorAll<HTMLElement>( | ||||
|       `.${SCROLL_FIXED_CLASS}`, | ||||
|     ); | ||||
|     const nodes = [...layoutFixedNodes]; | ||||
|     if (nodes.length > 0) { | ||||
|       nodes.forEach((node) => { | ||||
|         node.style.paddingRight = ''; | ||||
|         requestAnimationFrame(() => { | ||||
|           node.style.transition = node.dataset.transition || ''; | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|     document.body.style.paddingRight = ''; | ||||
|   }); | ||||
| } | ||||
|  | @ -4,6 +4,7 @@ import type { VbenLayoutProps } from './vben-layout'; | |||
| import type { CSSProperties } from 'vue'; | ||||
| import { computed, ref, watch } from 'vue'; | ||||
| 
 | ||||
| import { SCROLL_FIXED_CLASS } from '@vben-core/composables'; | ||||
| import { Menu } from '@vben-core/icons'; | ||||
| import { VbenIconButton } from '@vben-core/shadcn-ui'; | ||||
| 
 | ||||
|  | @ -478,9 +479,12 @@ function handleHeaderToggle() { | |||
|       class="flex flex-1 flex-col overflow-hidden transition-all duration-300 ease-in" | ||||
|     > | ||||
|       <div | ||||
|         :class="{ | ||||
|           'shadow-[0_16px_24px_hsl(var(--background))]': scrollY > 20, | ||||
|         }" | ||||
|         :class="[ | ||||
|           { | ||||
|             'shadow-[0_16px_24px_hsl(var(--background))]': scrollY > 20, | ||||
|           }, | ||||
|           SCROLL_FIXED_CLASS, | ||||
|         ]" | ||||
|         :style="headerWrapperStyle" | ||||
|         class="overflow-hidden transition-all duration-200" | ||||
|       > | ||||
|  |  | |||
|  | @ -39,6 +39,7 @@ export class DrawerApi { | |||
|       isOpen: false, | ||||
|       loading: false, | ||||
|       modal: true, | ||||
|       openAutoFocus: false, | ||||
|       showCancelButton: true, | ||||
|       showConfirmButton: true, | ||||
|       title: '', | ||||
|  |  | |||
|  | @ -52,6 +52,10 @@ export interface DrawerProps { | |||
|    * @default true | ||||
|    */ | ||||
|   modal?: boolean; | ||||
|   /** | ||||
|    * 是否自动聚焦 | ||||
|    */ | ||||
|   openAutoFocus?: boolean; | ||||
|   /** | ||||
|    * 是否显示取消按钮 | ||||
|    * @default true | ||||
|  |  | |||
|  | @ -51,6 +51,7 @@ const { | |||
|   footer: showFooter, | ||||
|   loading: showLoading, | ||||
|   modal, | ||||
|   openAutoFocus, | ||||
|   showCancelButton, | ||||
|   showConfirmButton, | ||||
|   title, | ||||
|  | @ -87,10 +88,21 @@ function pointerDownOutside(e: Event) { | |||
|     e.preventDefault(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function handerOpenAutoFocus(e: Event) { | ||||
|   if (!openAutoFocus.value) { | ||||
|     e?.preventDefault(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function handleFocusOutside(e: Event) { | ||||
|   e.preventDefault(); | ||||
|   e.stopPropagation(); | ||||
| } | ||||
| </script> | ||||
| <template> | ||||
|   <Sheet | ||||
|     :modal="modal" | ||||
|     :modal="false" | ||||
|     :open="state?.isOpen" | ||||
|     @update:open="() => drawerApi?.close()" | ||||
|   > | ||||
|  | @ -100,8 +112,13 @@ function pointerDownOutside(e: Event) { | |||
|           '!w-full': isMobile, | ||||
|         }) | ||||
|       " | ||||
|       :modal="modal" | ||||
|       :open="state?.isOpen" | ||||
|       @close-auto-focus="handleFocusOutside" | ||||
|       @escape-key-down="escapeKeyDown" | ||||
|       @focus-outside="handleFocusOutside" | ||||
|       @interact-outside="interactOutside" | ||||
|       @open-auto-focus="handerOpenAutoFocus" | ||||
|       @pointer-down-outside="pointerDownOutside" | ||||
|     > | ||||
|       <SheetHeader | ||||
|  |  | |||
|  | @ -95,7 +95,7 @@ async function checkProps(api: ExtendedDrawerApi, attrs: Record<string, any>) { | |||
|   const stateKeys = new Set(Object.keys(state)); | ||||
| 
 | ||||
|   for (const attr of Object.keys(attrs)) { | ||||
|     if (stateKeys.has(attr)) { | ||||
|     if (stateKeys.has(attr) && !['class'].includes(attr)) { | ||||
|       // connectedComponent存在时,不要传入Drawer的props,会造成复杂度提升,如果你需要修改Drawer的props,请使用 useVbenDrawer 或者api
 | ||||
|       console.warn( | ||||
|         `[Vben Drawer]: When 'connectedComponent' exists, do not set props or slots '${attr}', which will increase complexity. If you need to modify the props of Drawer, please use useVbenDrawer or api.`, | ||||
|  |  | |||
|  | @ -123,6 +123,7 @@ function handleFullscreen() { | |||
| function interactOutside(e: Event) { | ||||
|   if (!closeOnClickModal.value) { | ||||
|     e.preventDefault(); | ||||
|     e.stopPropagation(); | ||||
|   } | ||||
| } | ||||
| function escapeKeyDown(e: KeyboardEvent) { | ||||
|  | @ -143,12 +144,18 @@ function pointerDownOutside(e: Event) { | |||
|   const isDismissableModal = !!target?.dataset.dismissableModal; | ||||
|   if (!closeOnClickModal.value || !isDismissableModal) { | ||||
|     e.preventDefault(); | ||||
|     e.stopPropagation(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function handleFocusOutside(e: Event) { | ||||
|   e.preventDefault(); | ||||
|   e.stopPropagation(); | ||||
| } | ||||
| </script> | ||||
| <template> | ||||
|   <Dialog | ||||
|     :modal="modal" | ||||
|     :modal="false" | ||||
|     :open="state?.isOpen" | ||||
|     @update:open="() => modalApi?.close()" | ||||
|   > | ||||
|  | @ -166,9 +173,13 @@ function pointerDownOutside(e: Event) { | |||
|           }, | ||||
|         ) | ||||
|       " | ||||
|       :modal="modal" | ||||
|       :open="state?.isOpen" | ||||
|       :show-close="closable" | ||||
|       close-class="top-3" | ||||
|       @close-auto-focus="handleFocusOutside" | ||||
|       @escape-key-down="escapeKeyDown" | ||||
|       @focus-outside="handleFocusOutside" | ||||
|       @interact-outside="interactOutside" | ||||
|       @open-auto-focus="handerOpenAutoFocus" | ||||
|       @pointer-down-outside="pointerDownOutside" | ||||
|  |  | |||
|  | @ -107,7 +107,7 @@ async function checkProps(api: ExtendedModalApi, attrs: Record<string, any>) { | |||
|   const stateKeys = new Set(Object.keys(state)); | ||||
| 
 | ||||
|   for (const attr of Object.keys(attrs)) { | ||||
|     if (stateKeys.has(attr)) { | ||||
|     if (stateKeys.has(attr) && !['class'].includes(attr)) { | ||||
|       // connectedComponent存在时,不要传入Modal的props,会造成复杂度提升,如果你需要修改Modal的props,请使用 useModal 或者api
 | ||||
|       console.warn( | ||||
|         `[Vben Modal]: When 'connectedComponent' exists, do not set props or slots '${attr}', which will increase complexity. If you need to modify the props of Modal, please use useVbenModal or api.`, | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ export default defineBuildConfig({ | |||
|     { | ||||
|       builder: 'mkdist', | ||||
|       input: './src', | ||||
| 
 | ||||
|       pattern: ['**/*'], | ||||
|     }, | ||||
|     { | ||||
|  |  | |||
|  | @ -41,6 +41,7 @@ | |||
|   }, | ||||
|   "dependencies": { | ||||
|     "@radix-icons/vue": "^1.0.0", | ||||
|     "@vben-core/composables": "workspace:*", | ||||
|     "@vben-core/icons": "workspace:*", | ||||
|     "@vben-core/shared": "workspace:*", | ||||
|     "@vben-core/typings": "workspace:*", | ||||
|  |  | |||
|  | @ -9,16 +9,19 @@ import { | |||
|   DialogContent, | ||||
|   type DialogContentEmits, | ||||
|   type DialogContentProps, | ||||
|   DialogOverlay, | ||||
|   DialogPortal, | ||||
|   useForwardPropsEmits, | ||||
| } from 'radix-vue'; | ||||
| 
 | ||||
| import DialogOverlay from './DialogOverlay.vue'; | ||||
| 
 | ||||
| const props = withDefaults( | ||||
|   defineProps< | ||||
|     { | ||||
|       class?: any; | ||||
|       closeClass?: any; | ||||
|       modal?: boolean; | ||||
|       open?: boolean; | ||||
|       showClose?: boolean; | ||||
|     } & DialogContentProps | ||||
|   >(), | ||||
|  | @ -27,7 +30,13 @@ const props = withDefaults( | |||
| const emits = defineEmits<{ close: [] } & DialogContentEmits>(); | ||||
| 
 | ||||
| const delegatedProps = computed(() => { | ||||
|   const { class: _, showClose: __, ...delegated } = props; | ||||
|   const { | ||||
|     class: _, | ||||
|     modal: _modal, | ||||
|     open: _open, | ||||
|     showClose: __, | ||||
|     ...delegated | ||||
|   } = props; | ||||
| 
 | ||||
|   return delegated; | ||||
| }); | ||||
|  | @ -43,11 +52,7 @@ defineExpose({ | |||
| 
 | ||||
| <template> | ||||
|   <DialogPortal> | ||||
|     <DialogOverlay | ||||
|       class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-overlay fixed inset-0 z-[1000]" | ||||
|       data-dismissable-modal="true" | ||||
|       @click="() => emits('close')" | ||||
|     /> | ||||
|     <DialogOverlay v-if="open && modal" @click="() => emits('close')" /> | ||||
|     <DialogContent | ||||
|       ref="contentRef" | ||||
|       v-bind="forwarded" | ||||
|  |  | |||
|  | @ -0,0 +1,11 @@ | |||
| <script setup lang="ts"> | ||||
| import { useScrollLock } from '@vben-core/composables'; | ||||
| 
 | ||||
| useScrollLock(); | ||||
| </script> | ||||
| <template> | ||||
|   <div | ||||
|     class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-overlay fixed inset-0 z-[1000]" | ||||
|     data-dismissable-modal="true" | ||||
|   ></div> | ||||
| </template> | ||||
|  | @ -7,15 +7,17 @@ import { | |||
|   DialogContent, | ||||
|   type DialogContentEmits, | ||||
|   type DialogContentProps, | ||||
|   DialogOverlay, | ||||
|   DialogPortal, | ||||
|   useForwardPropsEmits, | ||||
| } from 'radix-vue'; | ||||
| 
 | ||||
| import { type SheetVariants, sheetVariants } from './sheet'; | ||||
| import SheetOverlay from './SheetOverlay.vue'; | ||||
| 
 | ||||
| interface SheetContentProps extends DialogContentProps { | ||||
|   class?: any; | ||||
|   modal?: boolean; | ||||
|   open?: boolean; | ||||
|   side?: SheetVariants['side']; | ||||
| } | ||||
| 
 | ||||
|  | @ -28,7 +30,13 @@ const props = defineProps<SheetContentProps>(); | |||
| const emits = defineEmits<DialogContentEmits>(); | ||||
| 
 | ||||
| const delegatedProps = computed(() => { | ||||
|   const { class: _, side: _side, ...delegated } = props; | ||||
|   const { | ||||
|     class: _, | ||||
|     modal: _modal, | ||||
|     open: _open, | ||||
|     side: _side, | ||||
|     ...delegated | ||||
|   } = props; | ||||
| 
 | ||||
|   return delegated; | ||||
| }); | ||||
|  | @ -38,10 +46,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits); | |||
| 
 | ||||
| <template> | ||||
|   <DialogPortal> | ||||
|     <DialogOverlay | ||||
|       class="bg-overlay data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[1000]" | ||||
|       data-dismissable-modal="true" | ||||
|     /> | ||||
|     <SheetOverlay v-if="open && modal" /> | ||||
|     <DialogContent | ||||
|       :class="cn(sheetVariants({ side }), 'z-[1000]', props.class)" | ||||
|       v-bind="{ ...forwarded, ...$attrs }" | ||||
|  |  | |||
|  | @ -0,0 +1,11 @@ | |||
| <script setup lang="ts"> | ||||
| import { useScrollLock } from '@vben-core/composables'; | ||||
| 
 | ||||
| useScrollLock(); | ||||
| </script> | ||||
| <template> | ||||
|   <div | ||||
|     class="bg-overlay data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[1000]" | ||||
|     data-dismissable-modal="true" | ||||
|   ></div> | ||||
| </template> | ||||
|  | @ -22,7 +22,7 @@ defineProps<{ showIcon?: boolean; theme?: string }>(); | |||
| 
 | ||||
| const route = useRoute(); | ||||
| const tabbarStore = useTabbarStore(); | ||||
| const { toggleMaximize } = useContentMaximize(); | ||||
| const { contentIsMaximize, toggleMaximize } = useContentMaximize(); | ||||
| const { refreshTab, unpinTab } = useTabs(); | ||||
| 
 | ||||
| const { | ||||
|  | @ -73,7 +73,7 @@ if (!preferences.tabbar.persist) { | |||
|     <TabsToolMore v-if="preferences.tabbar.showMore" :menus="menus" /> | ||||
|     <TabsToolScreen | ||||
|       v-if="preferences.tabbar.showMaximize" | ||||
|       :screen="preferences.sidebar.hidden" | ||||
|       :screen="contentIsMaximize" | ||||
|       @change="toggleMaximize" | ||||
|       @update:screen="toggleMaximize" | ||||
|     /> | ||||
|  |  | |||
|  | @ -884,6 +884,9 @@ importers: | |||
|       '@radix-icons/vue': | ||||
|         specifier: ^1.0.0 | ||||
|         version: 1.0.0(vue@3.5.4(typescript@5.6.2)) | ||||
|       '@vben-core/composables': | ||||
|         specifier: workspace:* | ||||
|         version: link:../../composables | ||||
|       '@vben-core/icons': | ||||
|         specifier: workspace:* | ||||
|         version: link:../../base/icons | ||||
|  |  | |||
|  | @ -23,30 +23,32 @@ export async function run(options: RunOptions) { | |||
|     return (pkg?.packageJson as Record<string, any>)?.scripts?.[command]; | ||||
|   }); | ||||
| 
 | ||||
|   const selectPkg = await select<any, string>({ | ||||
|     message: `Select the app you need to run [${command}]:`, | ||||
|     options: selectPkgs.map((item) => ({ | ||||
|       label: item?.packageJson.name, | ||||
|       value: item?.packageJson.name, | ||||
|     })), | ||||
|   }); | ||||
|   let selectPkg: string | symbol; | ||||
|   if (selectPkgs.length > 1) { | ||||
|     selectPkg = await select<any, string>({ | ||||
|       message: `Select the app you need to run [${command}]:`, | ||||
|       options: selectPkgs.map((item) => ({ | ||||
|         label: item?.packageJson.name, | ||||
|         value: item?.packageJson.name, | ||||
|       })), | ||||
|     }); | ||||
| 
 | ||||
|   if (isCancel(selectPkg) || !selectPkg) { | ||||
|     cancel('👋 Has cancelled'); | ||||
|     process.exit(0); | ||||
|     if (isCancel(selectPkg) || !selectPkg) { | ||||
|       cancel('👋 Has cancelled'); | ||||
|       process.exit(0); | ||||
|     } | ||||
|   } else { | ||||
|     selectPkg = selectPkgs[0]?.packageJson?.name ?? ''; | ||||
|   } | ||||
| 
 | ||||
|   if (!selectPkg) { | ||||
|     console.error('No app found'); | ||||
|     process.exit(1); | ||||
|   } | ||||
| 
 | ||||
|   execaCommand(`pnpm --filter=${selectPkg} run ${command}`, { | ||||
|     stdio: 'inherit', | ||||
|   }); | ||||
|   // const filters = [];
 | ||||
|   // for (const app of selectApps) {
 | ||||
|   //   filters.push(`--filter=${app}`);
 | ||||
|   // }
 | ||||
|   // $.verbose = true;
 | ||||
|   // execaCommand(`turbo run ${command} ${filters}`, {
 | ||||
|   //   stdio: 'inherit',
 | ||||
|   // });
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Vben
						Vben