fix: fake profile avatar position when resize

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2024-10-10 19:53:04 +08:00
parent a4fefd9926
commit 3af93e3776
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
5 changed files with 175 additions and 73 deletions

View File

@ -1,9 +1,26 @@
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import * as React from "react"
import { useHotkeyScope, useTypeScriptHappyCallback } from "~/hooks/common"
import { cn } from "~/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenu: typeof DropdownMenuPrimitive.Root = (props) => {
const [open, setOpen] = React.useState(!!props.open)
useHotkeyScope("DropdownMenu", open)
return (
<DropdownMenuPrimitive.Root
{...props}
onOpenChange={useTypeScriptHappyCallback(
(open) => {
setOpen(open)
props.onOpenChange?.(open)
},
[props.onOpenChange],
)}
/>
)
}
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
@ -54,20 +71,22 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"shadow-context-menu z-50 min-w-32 overflow-hidden rounded-md border bg-theme-background p-1 text-theme-foreground",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
>(({ className, sideOffset = 4, ...props }, ref) => {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"shadow-context-menu z-50 min-w-32 overflow-hidden rounded-md border bg-theme-background p-1 text-theme-foreground",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
})
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<

View File

@ -2,4 +2,5 @@ export const HotKeyScopeMap = {
Home: ["home"],
Menu: ["menu"],
Modal: ["modal"],
DropdownMenu: ["dropdown-menu"],
}

View File

@ -1,4 +1,4 @@
import { useCallback } from "react"
import { useCallback, useLayoutEffect, useRef } from "react"
import { useHotkeysContext } from "react-hotkeys-hook"
import { HotKeyScopeMap } from "~/constants"
@ -18,9 +18,41 @@ export const useSwitchHotKeyScope = () => {
for (const key of allScopes) {
disableScope(key)
}
enableScope(nextScope[0])
for (const key of nextScope) {
enableScope(key)
}
},
[disableScope, enableScope],
)
}
export const useHotkeyScopeFn = (scope: keyof typeof HotKeyScopeMap) => {
const { enableScope, disableScope, enabledScopes } = useHotkeysContext()
const currentScopeRef = useRef(enabledScopes)
return useCallback(() => {
const currentScope = currentScopeRef.current
for (const key of allScopes) {
disableScope(key)
}
for (const key of scope) {
enableScope(key)
}
return () => {
for (const key of scope) {
disableScope(key)
}
for (const scope of currentScope) {
enableScope(scope)
}
}
}, [enableScope, disableScope, scope])
}
export const useHotkeyScope = (scope: keyof typeof HotKeyScopeMap, when: boolean) => {
const fn = useHotkeyScopeFn(scope)
useLayoutEffect(() => {
if (when) {
return fn()
}
}, [fn, when])
}

View File

@ -1,6 +1,6 @@
import { repository } from "@pkg"
import type { FC } from "react"
import { memo, useState } from "react"
import { forwardRef, memo, useCallback, useLayoutEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
@ -37,7 +37,7 @@ export const ProfileButton: FC<LoginProps> = memo((props) => {
const presentUserProfile = usePresentUserProfileModal("dialog")
const presentAchievement = useAchievementModal()
const { t } = useTranslation()
const [ref, { x, y }] = useMeasure()
const [dropdown, setDropdown] = useState(false)
const navigate = useNavigate()
@ -50,9 +50,7 @@ export const ProfileButton: FC<LoginProps> = memo((props) => {
<>
<DropdownMenu onOpenChange={setDropdown}>
<DropdownMenuTrigger asChild className="!outline-none focus-visible:bg-theme-item-hover">
<ActionButton>
<UserAvatar ref={ref} className="h-6 p-0 [&_*]:border-0" hideName />
</ActionButton>
<TransitionAvatar stage={dropdown ? "zoom-in" : ""} />
</DropdownMenuTrigger>
<DropdownMenuContent
@ -134,26 +132,67 @@ export const ProfileButton: FC<LoginProps> = memo((props) => {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{x !== 0 && y !== 0 && (
</>
)
})
ProfileButton.displayName = "ProfileButton"
const TransitionAvatar = forwardRef<
HTMLButtonElement,
{
stage: "zoom-in" | ""
} & React.HTMLAttributes<HTMLButtonElement>
>(({ stage, ...props }, forwardRef) => {
const [ref, { x, y }, forceRefresh] = useMeasure()
const [avatarHovered, setAvatarHovered] = useState(false)
const zoomIn = stage === "zoom-in"
const [currentZoomIn, setCurrentZoomIn] = useState(false)
useLayoutEffect(() => {
if (zoomIn) {
setCurrentZoomIn(true)
}
}, [zoomIn])
return (
<>
<ActionButton
{...props}
ref={forwardRef}
onMouseEnter={useCallback(() => {
forceRefresh()
setAvatarHovered(true)
}, [forceRefresh])}
onMouseLeave={useCallback(() => {
setAvatarHovered(false)
}, [])}
>
<UserAvatar ref={ref} className="h-6 p-0 [&_*]:border-0" hideName />
</ActionButton>
{x !== 0 && y !== 0 && (avatarHovered || zoomIn || currentZoomIn) && (
<RootPortal>
<UserAvatar
style={{
left: x - (dropdown ? 16 : 0),
left: x - (zoomIn ? 16 : 0),
top: y,
}}
className={cn(
"pointer-events-none fixed -bottom-6 z-[999] p-0 duration-200 [&_*]:border-0",
"pointer-events-none fixed -bottom-6 p-0 duration-200 [&_*]:border-0",
"transform-gpu will-change-[left,top,height]",
dropdown ? "h-14 " : "h-6",
zoomIn ? "z-[999] h-14" : "z-[-1] h-6",
)}
hideName
onTransitionEnd={() => {
if (!zoomIn && currentZoomIn) {
setCurrentZoomIn(false)
}
}}
/>
</RootPortal>
)}
</>
)
})
ProfileButton.displayName = "ProfileButton"
function PowerButton() {
const wallet = useWallet()

View File

@ -20,56 +20,67 @@ export const UserAvatar = forwardRef<
hideName?: boolean
userId?: string
enableModal?: boolean
style?: React.CSSProperties
} & LoginProps
>(({ className, avatarClassName, hideName, userId, enableModal, style, ...props }, ref) => {
const { session, status } = useSession({
enabled: !userId,
})
const presentUserProfile = usePresentUserProfileModal("drawer")
} & LoginProps &
React.HTMLAttributes<HTMLDivElement>
>(
(
{ className, avatarClassName, hideName, userId, enableModal, style, onClick, ...props },
ref,
) => {
const { session, status } = useSession({
enabled: !userId,
})
const presentUserProfile = usePresentUserProfileModal("drawer")
const profile = useAuthQuery(
defineQuery(["profiles", userId], async () => {
const res = await apiClient.profiles.$get({
query: { id: userId! },
})
return res.data
}),
{
enabled: !!userId,
},
)
const profile = useAuthQuery(
defineQuery(["profiles", userId], async () => {
const res = await apiClient.profiles.$get({
query: { id: userId! },
})
return res.data
}),
{
enabled: !!userId,
},
)
if (!userId && status !== "authenticated") {
return <LoginButton {...props} />
}
if (!userId && status !== "authenticated") {
return <LoginButton {...props} />
}
const renderUserData = userId ? profile.data : session?.user
return (
<div
style={style}
ref={ref}
className={cn(
"flex h-20 items-center justify-center gap-2 px-5 py-2 font-medium text-zinc-600 dark:text-zinc-300",
className,
)}
onClick={enableModal ? () => presentUserProfile(userId) : undefined}
>
<Avatar
const renderUserData = userId ? profile.data : session?.user
return (
<div
style={style}
ref={ref}
onClick={(e) => {
if (enableModal) {
presentUserProfile(userId)
}
onClick?.(e)
}}
{...props}
className={cn(
"aspect-square h-full w-auto overflow-hidden rounded-full border bg-stone-300",
avatarClassName,
"flex h-20 items-center justify-center gap-2 px-5 py-2 font-medium text-zinc-600 dark:text-zinc-300",
className,
)}
>
<AvatarImage
className="duration-200 animate-in fade-in-0"
src={replaceImgUrlIfNeed(renderUserData?.image || undefined)}
/>
<AvatarFallback>{renderUserData?.name?.slice(0, 2)}</AvatarFallback>
</Avatar>
{!hideName && <div>{renderUserData?.name}</div>}
</div>
)
})
<Avatar
className={cn(
"aspect-square h-full w-auto overflow-hidden rounded-full border bg-stone-300",
avatarClassName,
)}
>
<AvatarImage
className="duration-200 animate-in fade-in-0"
src={replaceImgUrlIfNeed(renderUserData?.image || undefined)}
/>
<AvatarFallback>{renderUserData?.name?.slice(0, 2)}</AvatarFallback>
</Avatar>
{!hideName && <div>{renderUserData?.name}</div>}
</div>
)
},
)
UserAvatar.displayName = "UserAvatar"