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

マージ済み
みてるぞ が 2 個のコミットを feature/183 から main へマージ 2025-12-20 02:36:33 +09:00
2個のファイルの変更127行の追加31行の削除
コミット d5e35f0049 の変更だけを表示してゐます - すべてのコミットを表示
+1 -1
ファイルの表示
@@ -76,7 +76,7 @@ export default (({ post }: Props) => {
return ( return (
<SidebarComponent> <SidebarComponent>
<TagSearch/> <TagSearch/>
<motion.div layout> <motion.div key={post?.id ?? 0} layout>
{CATEGORIES.map ((cat: Category) => cat in tags && ( {CATEGORIES.map ((cat: Category) => cat in tags && (
<motion.div layout className="my-3" key={cat}> <motion.div layout className="my-3" key={cat}>
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle> <SubsectionTitle>{categoryNames[cat]}</SubsectionTitle>
+126 -30
ファイルの表示
@@ -1,7 +1,7 @@
import axios from 'axios' import axios from 'axios'
import toCamel from 'camelcase-keys' import toCamel from 'camelcase-keys'
import { AnimatePresence, motion } from 'framer-motion' 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 { Link, useLocation } from 'react-router-dom'
import Separator from '@/components/MenuSeparator' import Separator from '@/components/MenuSeparator'
@@ -20,9 +20,31 @@ type Props = { user: User | null }
export default (({ user }: Props) => { export default (({ user }: Props) => {
const location = useLocation () const location = useLocation ()
const itemRefs = useRef<(HTMLAnchorElement | null)[]> ([])
const navRef = useRef<HTMLDivElement | null> (null)
const measure = () => {
const nav = navRef.current
const el = itemRefs.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 [menuOpen, setMenuOpen] = useState (false)
const [openItemIdx, setOpenItemIdx] = useState (-1) const [openItemIdx, setOpenItemIdx] = useState (-1)
const [postCount, setPostCount] = useState<number | null> (null) const [postCount, setPostCount] = useState<number | null> (null)
const [subDir, setSubDir] = useState<(-1) | 1> (1)
const [wikiId, setWikiId] = useState<number | null> (WikiIdBus.get ()) const [wikiId, setWikiId] = useState<number | null> (WikiIdBus.get ())
const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId) const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId)
@@ -54,11 +76,38 @@ export default (({ user }: Props) => {
{ name: 'お前', to: `/users/${ user?.id }`, visible: false }, { name: 'お前', to: `/users/${ user?.id }`, visible: false },
{ name: '設定', to: '/users/settings', visible: Boolean (user) }] }] { name: '設定', to: '/users/settings', visible: Boolean (user) }] }]
const activeIdx = menu.findIndex (item => location.pathname.startsWith (item.base || item.to))
const prevActiveIdxRef = useRef<number> (activeIdx)
useLayoutEffect (() => {
if (activeIdx < 0)
return
const raf = requestAnimationFrame (measure)
const onResize = () => requestAnimationFrame (measure)
addEventListener ('resize', onResize)
return () => {
cancelAnimationFrame (raf)
removeEventListener ('resize', onResize)
}
}, [activeIdx])
useEffect (() => { useEffect (() => {
const unsubscribe = WikiIdBus.subscribe (setWikiId) const unsubscribe = WikiIdBus.subscribe (setWikiId)
return () => unsubscribe () return () => unsubscribe ()
}, []) }, [])
useEffect (() => {
const prev = prevActiveIdxRef.current
if (prev !== activeIdx)
{
setSubDir (activeIdx > prev ? 1 : -1)
prevActiveIdxRef.current = activeIdx
}
}, [activeIdx])
useEffect (() => { useEffect (() => {
setMenuOpen (false) setMenuOpen (false)
setOpenItemIdx (menu.findIndex (item => ( setOpenItemIdx (menu.findIndex (item => (
@@ -99,16 +148,28 @@ export default (({ user }: Props) => {
</Link> </Link>
{menu.map ((item, i) => ( <div ref={navRef} className="relative hidden md:flex h-full items-center">
<Link key={i} <div aria-hidden
to={item.to} className={cn ('absolute top-1/2 -translate-y-1/2 h-full rounded-md',
className={cn ('hidden md:flex h-full items-center', 'bg-yellow-200 dark:bg-red-950',
(location.pathname.startsWith (item.base || item.to) 'transition-[transform,width,opacity] duration-200 ease-out')}
? 'bg-yellow-200 dark:bg-red-950 px-4 font-bold' style={{ width: hl.width,
: 'px-2'))}> transform: `translate(${ hl.left }px, -50%)`,
{item.name} opacity: hl.visible ? 1 : 0 }}/>
</Link>
))} {menu.map ((item, i) => (
<Link key={i}
to={item.to}
ref={el => {
itemRefs.current[i] = el
}}
className={cn ('relative z-10 flex h-full items-center px-5',
((i === openItemIdx)
? 'font-bold'
: 'opacity-90 hover:opacity-100'))}>
{item.name}
</Link>))}
</div>
</div> </div>
<TopNavUser user={user}/> <TopNavUser user={user}/>
@@ -126,15 +187,28 @@ export default (({ user }: Props) => {
</nav> </nav>
<div className="hidden md:flex bg-yellow-200 dark:bg-red-950 <div className="hidden md:flex bg-yellow-200 dark:bg-red-950
items-center w-full min-h-[40px] px-3"> items-center w-full min-h-[40px] px-3 overflow-hidden">
{menu.find (item => location.pathname.startsWith (item.base || item.to))?.subMenu <AnimatePresence mode="wait" initial={false}>
.filter (item => item.visible ?? true) <motion.div
.map ((item, i) => 'component' in item ? item.component : ( key={activeIdx}
<Link key={i} className="flex items-center"
to={item.to} initial={{ y: subDir * 24, opacity: 0 }}
className="h-full flex items-center px-3"> animate={{ y: 0, opacity: 1 }}
{item.name} exit={{ y: subDir * 24, opacity: 0 }}
</Link>))} transition={{ duration: .1, 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> </div>
<AnimatePresence initial={false}> <AnimatePresence initial={false}>
@@ -167,16 +241,38 @@ export default (({ user }: Props) => {
}}> }}>
{item.name} {item.name}
</Link> </Link>
{i === openItemIdx && (
item.subMenu <AnimatePresence initial={false}>
.filter (subItem => subItem.visible ?? true) {i === openItemIdx && (
.map ((subItem, j) => 'component' in subItem ? subItem.component : ( <motion.div
<Link key={j} key={`sp-sub-${ i }`}
to={subItem.to} className="w-full bg-yellow-50 dark:bg-red-950"
className="w-full min-h-[36px] flex items-center pl-12 variants={{ closed: { clipPath: 'inset(0 0 100% 0)',
bg-yellow-50 dark:bg-red-950"> height: 0,
{subItem.name} opacity: 0 },
</Link>)))} 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>))} </Fragment>))}
<TopNavUser user={user} sp/> <TopNavUser user={user} sp/>
<Separator/> <Separator/>