Browse Source

feat: アニメーション修正(#183) (#185)

#183

#183

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/185
pull/187/head
みてるぞ 2 weeks ago
parent
commit
34c227f326
2 changed files with 129 additions and 32 deletions
  1. +1
    -1
      frontend/src/components/TagDetailSidebar.tsx
  2. +128
    -31
      frontend/src/components/TopNav.tsx

+ 1
- 1
frontend/src/components/TagDetailSidebar.tsx View File

@@ -76,7 +76,7 @@ export default (({ post }: Props) => {
return (
<SidebarComponent>
<TagSearch/>
<motion.div layout>
<motion.div key={post?.id ?? 0} layout>
{CATEGORIES.map ((cat: Category) => cat in tags && (
<motion.div layout className="my-3" key={cat}>
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle>


+ 128
- 31
frontend/src/components/TopNav.tsx View File

@@ -1,7 +1,7 @@
import axios from 'axios'
import toCamel from 'camelcase-keys'
import { AnimatePresence, motion } from 'framer-motion'
import { Fragment, useState, useEffect } from 'react'
import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { Link, useLocation } from 'react-router-dom'

import Separator from '@/components/MenuSeparator'
@@ -20,6 +20,28 @@ type Props = { user: User | null }
export default (({ user }: Props) => {
const location = useLocation ()

const dirRef = useRef<(-1) | 1> (1)
const itemsRef = useRef<(HTMLAnchorElement | null)[]> ([])
const navRef = useRef<HTMLDivElement | null> (null)

const measure = () => {
const nav = navRef.current
const el = itemsRef.current[activeIdx]
if (!(nav) || !(el) || activeIdx < 0)
return

const navRect = nav.getBoundingClientRect ()
const elRect = el.getBoundingClientRect ()

setHl ({ left: elRect.left - navRect.left,
width: elRect.width,
visible: true })
}

const [hl, setHl] = useState<{ left: number; width: number; visible: boolean }> ({
left: 0,
width: 0,
visible: false })
const [menuOpen, setMenuOpen] = useState (false)
const [openItemIdx, setOpenItemIdx] = useState (-1)
const [postCount, setPostCount] = useState<number | null> (null)
@@ -54,6 +76,32 @@ export default (({ user }: Props) => {
{ name: 'お前', to: `/users/${ user?.id }`, visible: false },
{ name: '設定', to: '/users/settings', visible: Boolean (user) }] }]

const activeIdx = menu.findIndex (item => location.pathname.startsWith (item.base || item.to))

const prevActiveIdxRef = useRef<number> (activeIdx)

if (activeIdx !== prevActiveIdxRef.current)
{
dirRef.current = activeIdx > prevActiveIdxRef.current ? 1 : -1
prevActiveIdxRef.current = activeIdx
}

const dir = dirRef.current

useLayoutEffect (() => {
if (activeIdx < 0)
return

const raf = requestAnimationFrame (measure)
const onResize = () => requestAnimationFrame (measure)

addEventListener ('resize', onResize)
return () => {
cancelAnimationFrame (raf)
removeEventListener ('resize', onResize)
}
}, [activeIdx])

useEffect (() => {
const unsubscribe = WikiIdBus.subscribe (setWikiId)
return () => unsubscribe ()
@@ -99,16 +147,26 @@ export default (({ user }: Props) => {
ぼざクリ タグ広場
</Link>

{menu.map ((item, i) => (
<Link key={i}
to={item.to}
className={cn ('hidden md:flex h-full items-center',
(location.pathname.startsWith (item.base || item.to)
? 'bg-yellow-200 dark:bg-red-950 px-4 font-bold'
: 'px-2'))}>
{item.name}
</Link>
))}
<div ref={navRef} className="relative hidden md:flex h-full items-center">
<div aria-hidden
className={cn ('absolute top-1/2 -translate-y-1/2 h-full',
'bg-yellow-200 dark:bg-red-950',
'transition-[transform,width] duration-200 ease-out')}
style={{ width: hl.width,
transform: `translate(${ hl.left }px, -50%)`,
opacity: hl.visible ? 1 : 0 }}/>

{menu.map ((item, i) => (
<Link key={i}
to={item.to}
ref={el => {
itemsRef.current[i] = el
}}
className={cn ('relative z-10 flex h-full items-center px-5',
(i === openItemIdx) && 'font-bold')}>
{item.name}
</Link>))}
</div>
</div>

<TopNavUser user={user}/>
@@ -125,16 +183,33 @@ export default (({ user }: Props) => {
</a>
</nav>

<div className="hidden md:flex bg-yellow-200 dark:bg-red-950
items-center w-full min-h-[40px] px-3">
{menu.find (item => location.pathname.startsWith (item.base || item.to))?.subMenu
.filter (item => item.visible ?? true)
.map ((item, i) => 'component' in item ? item.component : (
<Link key={i}
to={item.to}
className="h-full flex items-center px-3">
{item.name}
</Link>))}
<div className="relative hidden md:flex bg-yellow-200 dark:bg-red-950
items-center w-full min-h-[40px] overflow-hidden">
<AnimatePresence initial={false} custom={dir}>
<motion.div
key={activeIdx}
custom={dir}
variants={{ enter: (d: -1 | 1) => ({ y: d * 24, opacity: 0 }),
centre: { y: 0, opacity: 1 },
exit: (d: -1 | 1) => ({ y: (-d) * 24, opacity: 0 }) }}
className="absolute inset-0 flex items-center px-3"
initial="enter"
animate="centre"
exit="exit"
transition={{ duration: .2, ease: 'easeOut' }}>
{(menu[activeIdx]?.subMenu ?? [])
.filter (item => item.visible ?? true)
.map ((item, i) => (
'component' in item
? <Fragment key={`c-${ i }`}>{item.component}</Fragment>
: (
<Link key={`l-${ i }`}
to={item.to}
className="h-full flex items-center px-3">
{item.name}
</Link>)))}
</motion.div>
</AnimatePresence>
</div>

<AnimatePresence initial={false}>
@@ -167,16 +242,38 @@ export default (({ user }: Props) => {
}}>
{item.name}
</Link>
{i === openItemIdx && (
item.subMenu
.filter (subItem => subItem.visible ?? true)
.map ((subItem, j) => 'component' in subItem ? subItem.component : (
<Link key={j}
to={subItem.to}
className="w-full min-h-[36px] flex items-center pl-12
bg-yellow-50 dark:bg-red-950">
{subItem.name}
</Link>)))}

<AnimatePresence initial={false}>
{i === openItemIdx && (
<motion.div
key={`sp-sub-${ i }`}
className="w-full bg-yellow-50 dark:bg-red-950"
variants={{ closed: { clipPath: 'inset(0 0 100% 0)',
height: 0,
opacity: 0 },
open: { clipPath: 'inset(0 0 0% 0)',
height: 'auto',
opacity: 1 } }}
initial="closed"
animate="open"
exit="closed"
transition={{ duration: .2, ease: 'easeOut' }}>
{item.subMenu
.filter (subItem => subItem.visible ?? true)
.map ((subItem, j) => (
'component' in subItem
? (
<Fragment key={`sp-c-${ i }-${ j }`}>
{subItem.component}
</Fragment>)
: (
<Link key={`sp-l-${ i }-${ j }`}
to={subItem.to}
className="w-full min-h-[36px] flex items-center pl-12">
{subItem.name}
</Link>)))}
</motion.div>)}
</AnimatePresence>
</Fragment>))}
<TopNavUser user={user} sp/>
<Separator/>


Loading…
Cancel
Save