Skip to content

Commit

Permalink
feat: user-dropdown support hover trigger (#5143)
Browse files Browse the repository at this point in the history
* feat: user-dropdown support `hover` trigger

* fix: modified type declaration
  • Loading branch information
mynetfan authored Dec 15, 2024
1 parent 7581fb3 commit f446cbf
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 62 deletions.
1 change: 1 addition & 0 deletions packages/effects/hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@vben/stores": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "catalog:",
"vue": "catalog:",
"vue-router": "catalog:",
"watermark-js-plus": "catalog:"
Expand Down
1 change: 1 addition & 0 deletions packages/effects/hooks/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './use-app-config';
export * from './use-content-maximize';
export * from './use-design-tokens';
export * from './use-hover-toggle';
export * from './use-pagination';
export * from './use-refresh';
export * from './use-tabs';
Expand Down
63 changes: 63 additions & 0 deletions packages/effects/hooks/src/use-hover-toggle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { Arrayable, MaybeElementRef } from '@vueuse/core';

import { computed, onUnmounted, ref, watch } from 'vue';
import type { Ref } from 'vue';

import { isFunction } from '@vben/utils';

import { useMouseInElement } from '@vueuse/core';

/**
* 监测鼠标是否在元素内部,如果在元素内部则返回 true,否则返回 false
* @param refElement 所有需要检测的元素。如果提供了一个数组,那么鼠标在任何一个元素内部都会返回 true
* @param delay 延迟更新状态的时间
* @returns 返回一个数组,第一个元素是一个 ref,表示鼠标是否在元素内部,第二个元素是一个控制器,可以通过 enable 和 disable 方法来控制监听器的启用和禁用
*/
export function useHoverToggle(
refElement: Arrayable<MaybeElementRef>,
delay: (() => number) | number = 500,
) {
const isOutsides: Array<Ref<boolean>> = [];
const value = ref(false);
const timer = ref<ReturnType<typeof setTimeout> | undefined>();
const refs = Array.isArray(refElement) ? refElement : [refElement];
refs.forEach((refEle) => {
const listener = useMouseInElement(refEle, { handleOutside: true });
isOutsides.push(listener.isOutside);
});
const isOutsideAll = computed(() => isOutsides.every((v) => v.value));

function setValueDelay(val: boolean) {
timer.value && clearTimeout(timer.value);
timer.value = setTimeout(
() => {
value.value = val;
timer.value = undefined;
},
isFunction(delay) ? delay() : delay,
);
}

const watcher = watch(
isOutsideAll,
(val) => {
setValueDelay(!val);
},
{ immediate: true },
);

const controller = {
enable() {
watcher.resume();
},
disable() {
watcher.pause();
},
};

onUnmounted(() => {
timer.value && clearTimeout(timer.value);
});

return [value, controller] as [typeof value, typeof controller];
}
151 changes: 90 additions & 61 deletions packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
import type { AnyFunction } from '@vben/types';
import type { Component } from 'vue';
import { computed, ref } from 'vue';
import { computed, useTemplateRef, watch } from 'vue';
import { useHoverToggle } from '@vben/hooks';
import { LockKeyhole, LogOut } from '@vben/icons';
import { $t } from '@vben/locales';
import { preferences, usePreferences } from '@vben/preferences';
Expand Down Expand Up @@ -53,6 +54,10 @@ interface Props {
* 文本
*/
text?: string;
/** 触发方式 */
trigger?: 'both' | 'click' | 'hover';
/** hover触发时,延迟响应的时间 */
hoverDelay?: number;
}
defineOptions({
Expand All @@ -67,10 +72,11 @@ const props = withDefaults(defineProps<Props>(), {
showShortcutKey: true,
tagText: '',
text: '',
trigger: 'click',
hoverDelay: 500,
});
const emit = defineEmits<{ logout: [] }>();
const openPopover = ref(false);
const { globalLockScreenShortcutKey, globalLogoutShortcutKey } =
usePreferences();
Expand All @@ -84,6 +90,27 @@ const [LogoutModal, logoutModalApi] = useVbenModal({
},
});
const refTrigger = useTemplateRef('refTrigger');
const refContent = useTemplateRef('refContent');
const [openPopover, hoverWatcher] = useHoverToggle(
[refTrigger, refContent],
() => props.hoverDelay,
);
watch(
() => props.trigger === 'hover' || props.trigger === 'both',
(val) => {
if (val) {
hoverWatcher.enable();
} else {
hoverWatcher.disable();
}
},
{
immediate: true,
},
);
const altView = computed(() => (isWindowsOs() ? 'Alt' : ''));
const enableLogoutShortcutKey = computed(() => {
Expand Down Expand Up @@ -155,73 +182,75 @@ if (enableShortcutKey.value) {
{{ $t('ui.widgets.logoutTip') }}
</LogoutModal>

<DropdownMenu>
<DropdownMenuTrigger>
<DropdownMenu v-model:open="openPopover">
<DropdownMenuTrigger ref="refTrigger" :disabled="props.trigger === 'hover'">
<div class="hover:bg-accent ml-1 mr-2 cursor-pointer rounded-full p-1.5">
<div class="hover:text-accent-foreground flex-center">
<VbenAvatar :alt="text" :src="avatar" class="size-8" dot />
</div>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent class="mr-2 min-w-[240px] p-0 pb-1">
<DropdownMenuLabel class="flex items-center p-3">
<VbenAvatar
:alt="text"
:src="avatar"
class="size-12"
dot
dot-class="bottom-0 right-1 border-2 size-4 bg-green-500"
/>
<div class="ml-2 w-full">
<div
v-if="tagText || text || $slots.tagText"
class="text-foreground mb-1 flex items-center text-sm font-medium"
>
{{ text }}
<slot name="tagText">
<Badge v-if="tagText" class="ml-2 text-green-400">
{{ tagText }}
</Badge>
</slot>
</div>
<div class="text-muted-foreground text-xs font-normal">
{{ description }}
<div ref="refContent">
<DropdownMenuLabel class="flex items-center p-3">
<VbenAvatar
:alt="text"
:src="avatar"
class="size-12"
dot
dot-class="bottom-0 right-1 border-2 size-4 bg-green-500"
/>
<div class="ml-2 w-full">
<div
v-if="tagText || text || $slots.tagText"
class="text-foreground mb-1 flex items-center text-sm font-medium"
>
{{ text }}
<slot name="tagText">
<Badge v-if="tagText" class="ml-2 text-green-400">
{{ tagText }}
</Badge>
</slot>
</div>
<div class="text-muted-foreground text-xs font-normal">
{{ description }}
</div>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator v-if="menus?.length" />
<DropdownMenuItem
v-for="menu in menus"
:key="menu.text"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="menu.handler"
>
<VbenIcon :icon="menu.icon" class="mr-2 size-4" />
{{ menu.text }}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
v-if="preferences.widget.lockScreen"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="handleOpenLock"
>
<LockKeyhole class="mr-2 size-4" />
{{ $t('ui.widgets.lockScreen.title') }}
<DropdownMenuShortcut v-if="enableLockScreenShortcutKey">
{{ altView }} L
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator v-if="preferences.widget.lockScreen" />
<DropdownMenuItem
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="handleLogout"
>
<LogOut class="mr-2 size-4" />
{{ $t('common.logout') }}
<DropdownMenuShortcut v-if="enableLogoutShortcutKey">
{{ altView }} Q
</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuLabel>
<DropdownMenuSeparator v-if="menus?.length" />
<DropdownMenuItem
v-for="menu in menus"
:key="menu.text"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="menu.handler"
>
<VbenIcon :icon="menu.icon" class="mr-2 size-4" />
{{ menu.text }}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
v-if="preferences.widget.lockScreen"
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="handleOpenLock"
>
<LockKeyhole class="mr-2 size-4" />
{{ $t('ui.widgets.lockScreen.title') }}
<DropdownMenuShortcut v-if="enableLockScreenShortcutKey">
{{ altView }} L
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator v-if="preferences.widget.lockScreen" />
<DropdownMenuItem
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
@click="handleLogout"
>
<LogOut class="mr-2 size-4" />
{{ $t('common.logout') }}
<DropdownMenuShortcut v-if="enableLogoutShortcutKey">
{{ altView }} Q
</DropdownMenuShortcut>
</DropdownMenuItem>
</div>
</DropdownMenuContent>
</DropdownMenu>
</template>
1 change: 1 addition & 0 deletions playground/src/layouts/basic.vue
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ watch(
:text="userStore.userInfo?.realName"
description="ann.vben@gmail.com"
tag-text="Pro"
trigger="both"
@logout="handleLogout"
/>
</template>
Expand Down
5 changes: 4 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit f446cbf

Please sign in to comment.