| @@ -20,12 +20,13 @@ 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 dirRef = useRef<(-1) | 1> (1) | |||||
| const itemsRef = useRef<(HTMLAnchorElement | null)[]> ([]) | |||||
| const navRef = useRef<HTMLDivElement | null> (null) | const navRef = useRef<HTMLDivElement | null> (null) | ||||
| const measure = () => { | const measure = () => { | ||||
| const nav = navRef.current | const nav = navRef.current | ||||
| const el = itemRefs.current[activeIdx] | |||||
| const el = itemsRef.current[activeIdx] | |||||
| if (!(nav) || !(el) || activeIdx < 0) | if (!(nav) || !(el) || activeIdx < 0) | ||||
| return | return | ||||
| @@ -44,7 +45,6 @@ export default (({ user }: Props) => { | |||||
| 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) | ||||
| @@ -80,6 +80,14 @@ export default (({ user }: Props) => { | |||||
| const prevActiveIdxRef = useRef<number> (activeIdx) | const prevActiveIdxRef = useRef<number> (activeIdx) | ||||
| if (activeIdx !== prevActiveIdxRef.current) | |||||
| { | |||||
| dirRef.current = activeIdx > prevActiveIdxRef.current ? 1 : -1 | |||||
| prevActiveIdxRef.current = activeIdx | |||||
| } | |||||
| const dir = dirRef.current | |||||
| useLayoutEffect (() => { | useLayoutEffect (() => { | ||||
| if (activeIdx < 0) | if (activeIdx < 0) | ||||
| return | return | ||||
| @@ -99,15 +107,6 @@ export default (({ user }: Props) => { | |||||
| 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 => ( | ||||
| @@ -150,9 +149,9 @@ export default (({ user }: Props) => { | |||||
| <div ref={navRef} className="relative hidden md:flex h-full items-center"> | <div ref={navRef} className="relative hidden md:flex h-full items-center"> | ||||
| <div aria-hidden | <div aria-hidden | ||||
| className={cn ('absolute top-1/2 -translate-y-1/2 h-full rounded-md', | |||||
| className={cn ('absolute top-1/2 -translate-y-1/2 h-full', | |||||
| 'bg-yellow-200 dark:bg-red-950', | 'bg-yellow-200 dark:bg-red-950', | ||||
| 'transition-[transform,width,opacity] duration-200 ease-out')} | |||||
| 'transition-[transform,width] duration-200 ease-out')} | |||||
| style={{ width: hl.width, | style={{ width: hl.width, | ||||
| transform: `translate(${ hl.left }px, -50%)`, | transform: `translate(${ hl.left }px, -50%)`, | ||||
| opacity: hl.visible ? 1 : 0 }}/> | opacity: hl.visible ? 1 : 0 }}/> | ||||
| @@ -161,12 +160,10 @@ export default (({ user }: Props) => { | |||||
| <Link key={i} | <Link key={i} | ||||
| to={item.to} | to={item.to} | ||||
| ref={el => { | ref={el => { | ||||
| itemRefs.current[i] = el | |||||
| itemsRef.current[i] = el | |||||
| }} | }} | ||||
| className={cn ('relative z-10 flex h-full items-center px-5', | className={cn ('relative z-10 flex h-full items-center px-5', | ||||
| ((i === openItemIdx) | |||||
| ? 'font-bold' | |||||
| : 'opacity-90 hover:opacity-100'))}> | |||||
| (i === openItemIdx) && 'font-bold')}> | |||||
| {item.name} | {item.name} | ||||
| </Link>))} | </Link>))} | ||||
| </div> | </div> | ||||
| @@ -186,16 +183,20 @@ export default (({ user }: Props) => { | |||||
| </a> | </a> | ||||
| </nav> | </nav> | ||||
| <div className="hidden md:flex bg-yellow-200 dark:bg-red-950 | |||||
| items-center w-full min-h-[40px] px-3 overflow-hidden"> | |||||
| <AnimatePresence mode="wait" initial={false}> | |||||
| <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 | <motion.div | ||||
| key={activeIdx} | key={activeIdx} | ||||
| className="flex items-center" | |||||
| initial={{ y: subDir * 24, opacity: 0 }} | |||||
| animate={{ y: 0, opacity: 1 }} | |||||
| exit={{ y: subDir * 24, opacity: 0 }} | |||||
| transition={{ duration: .1, ease: 'easeOut' }}> | |||||
| 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 ?? []) | {(menu[activeIdx]?.subMenu ?? []) | ||||
| .filter (item => item.visible ?? true) | .filter (item => item.visible ?? true) | ||||
| .map ((item, i) => ( | .map ((item, i) => ( | ||||