画面上でのテストは完了. 移行にす早さが求められるので,準備ができたらマージする. Reviewed-on: #430 Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #430 でマージされました。
このコミットが含まれているのは:
@@ -20,6 +20,7 @@ import type { FC, MouseEvent } from 'react'
|
||||
import type { Material, Menu, MenuVisibleItem, Tag, User } from '@/types'
|
||||
|
||||
type Props = { user: User | null }
|
||||
type NavDensity = 'auto' | 'comfortable' | 'compact'
|
||||
|
||||
|
||||
export const menuOutline = (
|
||||
@@ -40,6 +41,9 @@ export const menuOutline = (
|
||||
|
||||
const materialFlg = /^\/materials\/\d+/.test (pathName)
|
||||
|
||||
const helpVsbl = /\/help\/((?!changes)[^\/?]+)/.test (pathName)
|
||||
const helpIdentifier = pathName.split ('/')[2] ?? ''
|
||||
|
||||
return [
|
||||
{ name: '広場', to: '/posts', subMenu: [
|
||||
{ name: '一覧', to: '/posts' },
|
||||
@@ -71,7 +75,7 @@ export const menuOutline = (
|
||||
visible: materialFlg && Boolean (material?.tag) },
|
||||
{ name: '履歴', to: `/materials/changes?material_id=${ material?.id }`,
|
||||
visible: materialFlg }] },
|
||||
{ name: 'Wiki', to: '/wiki/ヘルプ:ホーム', base: '/wiki', subMenu: [
|
||||
{ name: 'Wiki', to: '/wiki', subMenu: [
|
||||
{ name: '検索', to: '/wiki' },
|
||||
{ name: '新規', to: '/wiki/new', visible: editable },
|
||||
{ name: '全体履歴', to: '/wiki/changes' },
|
||||
@@ -82,6 +86,13 @@ export const menuOutline = (
|
||||
{ name: '履歴', to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg },
|
||||
{ name: '編輯', to: `/wiki/${ wikiId || wikiTitle }/edit`,
|
||||
visible: wikiPageFlg && editable }] },
|
||||
{ name: 'ヘルプ', to: '/help/home', base: '/help', subMenu: [
|
||||
{ name: '検索', to: '/help' },
|
||||
{ name: '全体履歴', to: '/help/changes' },
|
||||
{ component: <Separator/>, visible: helpVsbl },
|
||||
{ name: '履歴',
|
||||
to: `/help/changes?identifier=${ helpIdentifier }`,
|
||||
visible: helpVsbl }] },
|
||||
{ name: 'おたのしみ', visible: false, subMenu: [
|
||||
{ name: '上映会 (β)', to: '/theatres/1' },
|
||||
{ name: 'グカネータ (β)', to: '/gekanator' }] },
|
||||
@@ -104,6 +115,8 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
const dirRef = useRef<(-1) | 1> (1)
|
||||
const itemsRef = useRef<(HTMLAnchorElement | null)[]> ([])
|
||||
const navRef = useRef<HTMLDivElement | null> (null)
|
||||
const layoutRef = useRef<HTMLDivElement | null> (null)
|
||||
const barRef = useRef<HTMLElement | null> (null)
|
||||
|
||||
const measure = (idx: number) => {
|
||||
const nav = navRef.current
|
||||
@@ -128,6 +141,8 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
width: 0,
|
||||
visible: false })
|
||||
const [menuOpen, setMenuOpen] = useState (false)
|
||||
const [desktop, setDesktop] = useState (false)
|
||||
const [density, setDensity] = useState<NavDensity> ('auto')
|
||||
const [moreVsbl, setMoreVsbl] = useState (false)
|
||||
const [openItemIdx, setOpenItemIdx] = useState (-1)
|
||||
const [wikiId, setWikiId] = useState<number | null> (WikiIdBus.get ())
|
||||
@@ -194,15 +209,81 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
const dir = dirRef.current
|
||||
|
||||
useLayoutEffect (() => {
|
||||
const raf = requestAnimationFrame (() => measure (moreVsbl ? -1 : activeIdx))
|
||||
const onResize = () => requestAnimationFrame (() => measure (moreVsbl ? -1 : activeIdx))
|
||||
const layout = layoutRef.current
|
||||
const bar = barRef.current
|
||||
const primary = bar?.querySelector<HTMLElement> ('.top-nav-primary')
|
||||
if (layout == null || bar == null || primary == null)
|
||||
return
|
||||
|
||||
addEventListener ('resize', onResize)
|
||||
return () => {
|
||||
cancelAnimationFrame (raf)
|
||||
removeEventListener ('resize', onResize)
|
||||
const userLink = bar.querySelector<HTMLElement> ('.top-nav-user-desktop')
|
||||
let raf = 0
|
||||
let active = true
|
||||
|
||||
const update = () => {
|
||||
if (!(window.matchMedia ('(min-width: 768px)').matches))
|
||||
{
|
||||
setDesktop (false)
|
||||
setDensity ('auto')
|
||||
return
|
||||
}
|
||||
|
||||
// Measure the same desktop elements while the mobile layout is active.
|
||||
layout.setAttribute ('data-measuring', '')
|
||||
const previousDensity = layout.getAttribute ('data-density') ?? 'auto'
|
||||
const fittingDensity = (() => {
|
||||
try
|
||||
{
|
||||
for (const candidate of ['auto', 'comfortable', 'compact'] as const)
|
||||
{
|
||||
layout.setAttribute ('data-density', candidate)
|
||||
const style = getComputedStyle (bar)
|
||||
const required = primary.getBoundingClientRect ().width
|
||||
+ (userLink?.getBoundingClientRect ().width ?? 0)
|
||||
+ parseFloat (style.paddingLeft) + parseFloat (style.paddingRight)
|
||||
+ (userLink == null ? 0 : parseFloat (style.columnGap))
|
||||
if (required <= bar.clientWidth)
|
||||
return candidate
|
||||
}
|
||||
return null
|
||||
}
|
||||
finally
|
||||
{
|
||||
layout.setAttribute ('data-density', previousDensity)
|
||||
layout.removeAttribute ('data-measuring')
|
||||
}
|
||||
}) ()
|
||||
setDensity (fittingDensity ?? 'compact')
|
||||
setDesktop (fittingDensity != null)
|
||||
if (fittingDensity != null)
|
||||
measure (moreVsbl ? -1 : activeIdx)
|
||||
}
|
||||
})
|
||||
|
||||
const schedule = () => {
|
||||
if (!(active))
|
||||
return
|
||||
cancelAnimationFrame (raf)
|
||||
raf = requestAnimationFrame (update)
|
||||
}
|
||||
|
||||
const observer =
|
||||
typeof ResizeObserver === 'undefined' ? null : new ResizeObserver (schedule)
|
||||
observer?.observe (bar)
|
||||
observer?.observe (primary)
|
||||
if (userLink != null)
|
||||
observer?.observe (userLink)
|
||||
update ()
|
||||
addEventListener ('resize', schedule)
|
||||
document.fonts?.addEventListener ('loadingdone', schedule)
|
||||
void document.fonts?.ready.then (schedule)
|
||||
|
||||
return () => {
|
||||
active = false
|
||||
cancelAnimationFrame (raf)
|
||||
observer?.disconnect ()
|
||||
removeEventListener ('resize', schedule)
|
||||
document.fonts?.removeEventListener ('loadingdone', schedule)
|
||||
}
|
||||
}, [activeIdx, moreVsbl, desktop, density, user?.name])
|
||||
|
||||
useEffect (() => {
|
||||
const unsubscribe = WikiIdBus.subscribe (setWikiId)
|
||||
@@ -215,19 +296,22 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
}, [activeIdx, location])
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="top-nav-root px-3 flex justify-between items-center w-full">
|
||||
<div className="flex items-center gap-2 h-12">
|
||||
<div ref={layoutRef} className="top-nav-layout w-full min-w-0 shrink-0"
|
||||
data-desktop={desktop} data-density={density}>
|
||||
<nav ref={barRef}
|
||||
className="top-nav-root top-nav-bar flex justify-between items-center w-full">
|
||||
<div className="top-nav-primary flex shrink-0 items-center h-12">
|
||||
<PrefetchLink
|
||||
to="/posts"
|
||||
className="top-nav-brand-link mx-4 text-xl font-bold"
|
||||
className="top-nav-brand-link shrink-0 whitespace-nowrap font-bold"
|
||||
onClick={() => {
|
||||
scroll (0, 0)
|
||||
}}>
|
||||
ぼざクリ タグ広場
|
||||
</PrefetchLink>
|
||||
|
||||
<div ref={navRef} className="relative hidden md:flex h-12 items-center">
|
||||
<div ref={navRef}
|
||||
className="top-nav-desktop-menu relative shrink-0 h-12 items-center">
|
||||
<div aria-hidden
|
||||
className={cn ('absolute inset-y-0 h-12',
|
||||
'top-nav-highlight',
|
||||
@@ -239,6 +323,7 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
{visibleMenu.map ((item, i) => (
|
||||
<motion.div
|
||||
key={item.to}
|
||||
className="shrink-0 whitespace-nowrap"
|
||||
{...(animationsOff
|
||||
? { }
|
||||
: { layoutId: `menu-${ item.name }` })}
|
||||
@@ -252,7 +337,7 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
ref={(el: (HTMLAnchorElement | null)) => {
|
||||
itemsRef.current[i] = el
|
||||
}}
|
||||
className={cn ('top-nav-menu-link relative z-10 flex h-full items-center px-5',
|
||||
className={cn ('top-nav-menu-link relative z-10 flex h-full items-center',
|
||||
(i === openItemIdx) && 'font-bold')}>
|
||||
{item.name}
|
||||
</PrefetchLink>
|
||||
@@ -267,7 +352,8 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
setMoreVsbl (true)
|
||||
measure (-1)
|
||||
}}
|
||||
className={cn ('top-nav-menu-link relative z-10 flex h-full items-center px-5',
|
||||
className={cn ('top-nav-menu-link relative z-10 flex h-full items-center',
|
||||
'shrink-0 whitespace-nowrap',
|
||||
(openItemIdx < 0 || moreVsbl) && 'font-bold')}>
|
||||
その他 »
|
||||
</PrefetchLink>
|
||||
@@ -278,7 +364,8 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="top-nav-mobile-toggle md:hidden ml-auto border-0 bg-transparent pr-4"
|
||||
className="top-nav-mobile-toggle shrink-0 whitespace-nowrap
|
||||
ml-auto border-0 bg-transparent pr-4"
|
||||
onClick={() => {
|
||||
setMenuOpen (!(menuOpen))
|
||||
}}>
|
||||
@@ -291,7 +378,8 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
key="submenu-shell"
|
||||
{...(animationsOff ? { }
|
||||
: { layout: true })}
|
||||
className="relative z-20 hidden md:block overflow-hidden top-nav-submenu"
|
||||
className="top-nav-desktop-submenu relative z-20 shrink-0
|
||||
overflow-hidden top-nav-submenu"
|
||||
animate={{ height: submenuHeight }}
|
||||
onMouseLeave={() => {
|
||||
if (moreVsbl)
|
||||
@@ -305,7 +393,8 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
? (
|
||||
moreMenu.map ((item, i) => (
|
||||
<div key={i} className="relative h-[40px]">
|
||||
<div className="absolute inset-0 flex items-center px-3">
|
||||
<div className="absolute inset-0 flex items-center px-3
|
||||
overflow-x-auto whitespace-nowrap">
|
||||
<motion.div
|
||||
transition={topNavTransition}
|
||||
{...(animationsOff
|
||||
@@ -315,7 +404,7 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
: { initial: { x: 40, y: -40, opacity: 0 },
|
||||
animate: { x: 0, y: 0, opacity: 1 },
|
||||
exit: { x: 40, y: -40, opacity: 0 } }))}
|
||||
className="z-10 h-full flex items-center px-3 font-bold w-28">
|
||||
className="z-10 h-full flex shrink-0 items-center px-3 font-bold w-28">
|
||||
<h2>{item.name}</h2>
|
||||
</motion.div>
|
||||
{item.subMenu
|
||||
@@ -363,7 +452,8 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
<div className="relative h-[40px]">
|
||||
{animationsOff
|
||||
? (
|
||||
<div className="absolute inset-0 flex items-center px-3">
|
||||
<div className="absolute inset-0 flex items-center px-3
|
||||
overflow-x-auto whitespace-nowrap">
|
||||
{(visibleMenu[activeIdx]?.subMenu ?? [])
|
||||
.filter (item => item.visible ?? true)
|
||||
.map ((item, i) => (
|
||||
@@ -392,7 +482,8 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
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"
|
||||
className="absolute inset-0 flex items-center px-3
|
||||
overflow-x-auto whitespace-nowrap"
|
||||
initial="enter"
|
||||
animate="centre"
|
||||
exit="exit"
|
||||
@@ -432,7 +523,7 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
? (
|
||||
menuOpen && (
|
||||
<div
|
||||
className={cn ('top-nav-root top-nav-mobile-menu flex flex-col md:hidden',
|
||||
className={cn ('top-nav-root top-nav-mobile-menu flex flex-col',
|
||||
'items-start')}>
|
||||
<Separator/>
|
||||
{visibleMenu.map ((item, i) => (
|
||||
@@ -476,9 +567,6 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
</Fragment>))}
|
||||
<PrefetchLink
|
||||
to="/more"
|
||||
ref={(el: (HTMLAnchorElement | null)) => {
|
||||
itemsRef.current[visibleMenu.length] = el
|
||||
}}
|
||||
className={cn ('top-nav-mobile-row w-full min-h-[40px] flex items-center pl-8',
|
||||
((openItemIdx < 0)
|
||||
&& 'top-nav-mobile-active font-bold'))}>
|
||||
@@ -492,7 +580,7 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
{menuOpen && (
|
||||
<motion.div
|
||||
key="spmenu"
|
||||
className={cn ('top-nav-root top-nav-mobile-menu flex flex-col md:hidden',
|
||||
className={cn ('top-nav-root top-nav-mobile-menu flex flex-col',
|
||||
'items-start')}
|
||||
variants={{ closed: { clipPath: 'inset(0 0 100% 0)',
|
||||
height: 0 },
|
||||
@@ -558,9 +646,6 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
</Fragment>))}
|
||||
<PrefetchLink
|
||||
to="/more"
|
||||
ref={(el: (HTMLAnchorElement | null)) => {
|
||||
itemsRef.current[visibleMenu.length] = el
|
||||
}}
|
||||
className={cn ('top-nav-mobile-row w-full min-h-[40px] flex items-center pl-8',
|
||||
((openItemIdx < 0)
|
||||
&& 'top-nav-mobile-active font-bold'))}>
|
||||
@@ -570,7 +655,7 @@ const TopNav: FC<Props> = ({ user }) => {
|
||||
<Separator/>
|
||||
</motion.div>)}
|
||||
</AnimatePresence>)}
|
||||
</>)
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default TopNav
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする