Browse Source

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 code
Vben 6 months ago
parent
commit
d27e5eeef7

+ 2 - 2
packages/@core/base/design/src/css/global.css

@@ -37,10 +37,10 @@
   }
 
   body {
-    @apply !pointer-events-auto;
-
     min-height: 100vh;
 
+    /* pointer-events: auto !important; */
+
     /* overflow: overlay; */
 
     /* -webkit-font-smoothing: antialiased; */

+ 19 - 0
packages/@core/base/shared/src/utils/dom.ts

@@ -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;
+}

+ 1 - 0
packages/@core/composables/src/index.ts

@@ -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 {

+ 48 - 0
packages/@core/composables/src/use-scroll-lock.ts

@@ -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 = '';
+  });
+}

+ 7 - 3
packages/@core/ui-kit/layout-ui/src/vben-layout.vue

@@ -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"
       >

+ 1 - 0
packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts

@@ -39,6 +39,7 @@ export class DrawerApi {
       isOpen: false,
       loading: false,
       modal: true,
+      openAutoFocus: false,
       showCancelButton: true,
       showConfirmButton: true,
       title: '',

+ 4 - 0
packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts

@@ -52,6 +52,10 @@ export interface DrawerProps {
    * @default true
    */
   modal?: boolean;
+  /**
+   * 是否自动聚焦
+   */
+  openAutoFocus?: boolean;
   /**
    * 是否显示取消按钮
    * @default true

+ 18 - 1
packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue

@@ -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

+ 1 - 1
packages/@core/ui-kit/popup-ui/src/drawer/use-drawer.ts

@@ -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.`,

+ 12 - 1
packages/@core/ui-kit/popup-ui/src/modal/modal.vue

@@ -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"

+ 1 - 1
packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts

@@ -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.`,

+ 1 - 0
packages/@core/ui-kit/shadcn-ui/build.config.ts

@@ -7,6 +7,7 @@ export default defineBuildConfig({
     {
       builder: 'mkdist',
       input: './src',
+
       pattern: ['**/*'],
     },
     {

+ 1 - 0
packages/@core/ui-kit/shadcn-ui/package.json

@@ -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:*",

+ 12 - 7
packages/@core/ui-kit/shadcn-ui/src/components/ui/dialog/DialogContent.vue

@@ -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"

+ 11 - 0
packages/@core/ui-kit/shadcn-ui/src/components/ui/dialog/DialogOverlay.vue

@@ -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>

+ 11 - 6
packages/@core/ui-kit/shadcn-ui/src/components/ui/sheet/SheetContent.vue

@@ -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 }"

+ 11 - 0
packages/@core/ui-kit/shadcn-ui/src/components/ui/sheet/SheetOverlay.vue

@@ -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>

+ 2 - 2
packages/effects/layouts/src/basic/tabbar/tabbar.vue

@@ -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"
     />

+ 3 - 0
pnpm-lock.yaml

@@ -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

+ 20 - 18
scripts/turbo-run/src/run.ts

@@ -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);
+    }
+  } else {
+    selectPkg = selectPkgs[0]?.packageJson?.name ?? '';
+  }
 
-  if (isCancel(selectPkg) || !selectPkg) {
-    cancel('👋 Has cancelled');
-    process.exit(0);
+  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',
-  // });
 }
 
 /**