サイド・バーの幅を可変に (#385) #396
@@ -488,25 +488,23 @@ const MaterialSidebar: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden md:block">
|
||||
<SidebarComponent sidebarKey="materials">
|
||||
<div className="space-y-4">
|
||||
<MaterialFilterButtons
|
||||
materialFilter={materialFilter}
|
||||
onChange={handleFilterChange}/>
|
||||
{isLoading && (
|
||||
<p className="text-sm text-neutral-500 dark:text-stone-400">読込中……</p>)}
|
||||
{isError && (
|
||||
<p className="text-sm text-red-600 dark:text-red-300">
|
||||
タグ一覧の取得に失敗しました.
|
||||
</p>)}
|
||||
{(!isLoading && !isError) && (
|
||||
<ul>
|
||||
{renderDesktopTree (visibleRootTags)}
|
||||
</ul>)}
|
||||
</div>
|
||||
</SidebarComponent>
|
||||
</div>
|
||||
<SidebarComponent sidebarKey="materials" className="hidden md:block">
|
||||
<div className="space-y-4">
|
||||
<MaterialFilterButtons
|
||||
materialFilter={materialFilter}
|
||||
onChange={handleFilterChange}/>
|
||||
{isLoading && (
|
||||
<p className="text-sm text-neutral-500 dark:text-stone-400">読込中……</p>)}
|
||||
{isError && (
|
||||
<p className="text-sm text-red-600 dark:text-red-300">
|
||||
タグ一覧の取得に失敗しました.
|
||||
</p>)}
|
||||
{(!isLoading && !isError) && (
|
||||
<ul>
|
||||
{renderDesktopTree (visibleRootTags)}
|
||||
</ul>)}
|
||||
</div>
|
||||
</SidebarComponent>
|
||||
</>)
|
||||
}
|
||||
|
||||
|
||||
@@ -146,10 +146,13 @@ const DropSlot = ({ cat }: { cat: Category }) => {
|
||||
}
|
||||
|
||||
|
||||
type Props = { post: Post; sp?: boolean }
|
||||
type Props = {
|
||||
className?: string
|
||||
post: Post
|
||||
sp?: boolean }
|
||||
|
||||
|
||||
const TagDetailSidebar: FC<Props> = ({ post, sp }) => {
|
||||
const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
|
||||
sp = Boolean (sp)
|
||||
|
||||
const qc = useQueryClient ()
|
||||
@@ -278,7 +281,7 @@ const TagDetailSidebar: FC<Props> = ({ post, sp }) => {
|
||||
}, [baseTags])
|
||||
|
||||
return (
|
||||
<SidebarComponent sidebarKey="post-detail-tags">
|
||||
<SidebarComponent sidebarKey="post-detail-tags" className={className}>
|
||||
<TagSearch/>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
|
||||
@@ -13,6 +13,16 @@ const sidebarRoot = (container: HTMLElement): Element =>
|
||||
container.querySelector ('[style*="--sidebar-width"]')!
|
||||
|
||||
|
||||
const sidebarTree = (props: Partial<Parameters<typeof SidebarComponent>[0]>) => (
|
||||
<HelmetProvider>
|
||||
<div data-sidebar-container>
|
||||
<SidebarComponent sidebarKey="test-sidebar" {...props}>
|
||||
Sidebar body
|
||||
</SidebarComponent>
|
||||
</div>
|
||||
</HelmetProvider>)
|
||||
|
||||
|
||||
const renderSidebar = (
|
||||
props: Partial<Parameters<typeof SidebarComponent>[0]> = {},
|
||||
containerWidth = 800,
|
||||
@@ -24,14 +34,7 @@ const renderSidebar = (
|
||||
},
|
||||
})
|
||||
|
||||
const rendered = render (
|
||||
<HelmetProvider>
|
||||
<div data-sidebar-container>
|
||||
<SidebarComponent sidebarKey="test-sidebar" {...props}>
|
||||
Sidebar body
|
||||
</SidebarComponent>
|
||||
</div>
|
||||
</HelmetProvider>)
|
||||
const rendered = render (sidebarTree (props))
|
||||
|
||||
return rendered
|
||||
}
|
||||
@@ -96,4 +99,26 @@ describe ('SidebarComponent', () => {
|
||||
expect (localStorage.getItem ('sidebar:test-sidebar:right:width')).toBeNull ()
|
||||
expect (localStorage.getItem ('sidebar:other-sidebar:right:width')).toBe ('480')
|
||||
})
|
||||
|
||||
it ('clamps current width on maxWidth changes without re-reading storage', async () => {
|
||||
localStorage.setItem ('sidebar:test-sidebar:left:width', '420')
|
||||
|
||||
const { container, rerender } = renderSidebar ({ maxWidth: 420 }, 900)
|
||||
|
||||
await waitFor (() => {
|
||||
expect (sidebarWidth (sidebarRoot (container))).toBe ('420px')
|
||||
})
|
||||
|
||||
rerender (sidebarTree ({ maxWidth: 300 }))
|
||||
|
||||
await waitFor (() => {
|
||||
expect (sidebarWidth (sidebarRoot (container))).toBe ('300px')
|
||||
})
|
||||
|
||||
rerender (sidebarTree ({ maxWidth: 500 }))
|
||||
|
||||
await waitFor (() => {
|
||||
expect (sidebarWidth (sidebarRoot (container))).toBe ('300px')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -69,6 +69,7 @@ const SidebarComponent: FC<Props> = ({
|
||||
side = 'left',
|
||||
}) => {
|
||||
const rootRef = useRef<HTMLDivElement | null> (null)
|
||||
const maxWidthRef = useRef (maxWidth)
|
||||
const getContainerWidth = useCallback (() => {
|
||||
const containerWidth = rootRef.current
|
||||
?.closest ('[data-sidebar-container]')
|
||||
@@ -83,12 +84,14 @@ const SidebarComponent: FC<Props> = ({
|
||||
typeof window === 'undefined' ? containerWidth : window.innerWidth
|
||||
const viewportMaxWidth = Math.floor (viewportWidth * MAX_VIEWPORT_WIDTH_RATIO)
|
||||
const maxAllowedWidth =
|
||||
maxWidth == null
|
||||
maxWidthRef.current == null
|
||||
? getSidebarMaxWidth (containerWidth)
|
||||
: Math.max (MIN_SIDEBAR_WIDTH, Math.min (viewportMaxWidth, maxWidth))
|
||||
: Math.max (
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
Math.min (viewportMaxWidth, maxWidthRef.current))
|
||||
|
||||
return Math.min (maxAllowedWidth, Math.max (MIN_SIDEBAR_WIDTH, nextWidth))
|
||||
}, [maxWidth])
|
||||
}, [])
|
||||
|
||||
const [width, setWidth] = useState (() => (
|
||||
typeof window === 'undefined'
|
||||
@@ -153,6 +156,21 @@ const SidebarComponent: FC<Props> = ({
|
||||
restoreBodyStyle ()
|
||||
}, [clearLongPressTimer, restoreBodyStyle, sidebarKey])
|
||||
|
||||
const applyWidth = useCallback ((nextWidth: number) => {
|
||||
if (nextWidth === widthRef.current)
|
||||
return
|
||||
|
||||
widthRef.current = nextWidth
|
||||
setWidth (nextWidth)
|
||||
}, [])
|
||||
|
||||
useEffect (() => {
|
||||
maxWidthRef.current = maxWidth
|
||||
const containerWidth = getContainerWidth ()
|
||||
const nextWidth = getClampedWidth (widthRef.current, containerWidth)
|
||||
applyWidth (nextWidth)
|
||||
}, [applyWidth, getClampedWidth, getContainerWidth, maxWidth])
|
||||
|
||||
const updateWidth = useCallback ((clientX: number) => {
|
||||
const state = resizeRef.current
|
||||
if (state == null)
|
||||
@@ -163,9 +181,8 @@ const SidebarComponent: FC<Props> = ({
|
||||
? clientX - state.startX
|
||||
: state.startX - clientX
|
||||
const nextWidth = getClampedWidth (state.startWidth + delta, containerWidth)
|
||||
widthRef.current = nextWidth
|
||||
setWidth (nextWidth)
|
||||
}, [getClampedWidth, getContainerWidth])
|
||||
applyWidth (nextWidth)
|
||||
}, [applyWidth, getClampedWidth, getContainerWidth])
|
||||
|
||||
const handlePointerDown = (ev: PointerEvent<HTMLDivElement>) => {
|
||||
if (ev.pointerType !== 'mouse' && ev.pointerType !== 'touch')
|
||||
@@ -235,10 +252,9 @@ const SidebarComponent: FC<Props> = ({
|
||||
const resetWidth = useCallback (() => {
|
||||
const containerWidth = getContainerWidth ()
|
||||
const nextWidth = getClampedWidth (DEFAULT_SIDEBAR_WIDTH, containerWidth)
|
||||
widthRef.current = nextWidth
|
||||
setWidth (nextWidth)
|
||||
applyWidth (nextWidth)
|
||||
window.localStorage.removeItem (storageKeyForSidebar (sidebarKey, side))
|
||||
}, [getClampedWidth, getContainerWidth, sidebarKey, side])
|
||||
}, [applyWidth, getClampedWidth, getContainerWidth, sidebarKey, side])
|
||||
|
||||
const handleDoubleClick = (ev: MouseEvent<HTMLDivElement>) => {
|
||||
ev.preventDefault ()
|
||||
@@ -251,9 +267,14 @@ const SidebarComponent: FC<Props> = ({
|
||||
const nextWidth = getClampedWidth (
|
||||
readStoredSidebarWidth (sidebarKey, side),
|
||||
containerWidth)
|
||||
widthRef.current = nextWidth
|
||||
setWidth (nextWidth)
|
||||
}, [getClampedWidth, getContainerWidth, sidebarKey, side])
|
||||
applyWidth (nextWidth)
|
||||
}, [applyWidth, getClampedWidth, getContainerWidth, sidebarKey, side])
|
||||
|
||||
useEffect (() => {
|
||||
const containerWidth = getContainerWidth ()
|
||||
const nextWidth = getClampedWidth (widthRef.current, containerWidth)
|
||||
applyWidth (nextWidth)
|
||||
}, [applyWidth, getClampedWidth, getContainerWidth])
|
||||
|
||||
useEffect (() => {
|
||||
if (typeof window === 'undefined')
|
||||
@@ -262,16 +283,12 @@ const SidebarComponent: FC<Props> = ({
|
||||
const handleResize = () => {
|
||||
const containerWidth = getContainerWidth ()
|
||||
const nextWidth = getClampedWidth (widthRef.current, containerWidth)
|
||||
if (nextWidth === widthRef.current)
|
||||
return
|
||||
|
||||
widthRef.current = nextWidth
|
||||
setWidth (nextWidth)
|
||||
applyWidth (nextWidth)
|
||||
}
|
||||
|
||||
window.addEventListener ('resize', handleResize)
|
||||
return () => window.removeEventListener ('resize', handleResize)
|
||||
}, [getClampedWidth, getContainerWidth])
|
||||
}, [applyWidth, getClampedWidth, getContainerWidth])
|
||||
|
||||
useEffect (() => {
|
||||
onWidthChange?.(width)
|
||||
|
||||
@@ -6,9 +6,11 @@ import type { FC } from 'react'
|
||||
|
||||
|
||||
const MaterialBasePage: FC = () => (
|
||||
<div className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden">
|
||||
<div
|
||||
data-sidebar-container
|
||||
className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden">
|
||||
<MaterialSidebar/>
|
||||
<Outlet/>
|
||||
</div>)
|
||||
|
||||
export default MaterialBasePage
|
||||
export default MaterialBasePage
|
||||
|
||||
@@ -92,16 +92,16 @@ const PostDetailPage: FC<Props> = ({ user }) => {
|
||||
: 'bg-gray-500 hover:bg-gray-600')
|
||||
|
||||
return (
|
||||
<div className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden">
|
||||
<div
|
||||
data-sidebar-container
|
||||
className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden">
|
||||
<Helmet>
|
||||
{(post?.thumbnail || post?.thumbnailBase) && (
|
||||
<meta name="thumbnail" content={post.thumbnail! || post.thumbnailBase!}/>)}
|
||||
{post && <title>{`${ post.title || post.url } | ${ SITE_TITLE }`}</title>}
|
||||
</Helmet>
|
||||
|
||||
<div className="hidden md:block">
|
||||
{post && <TagDetailSidebar post={post}/>}
|
||||
</div>
|
||||
{post && <TagDetailSidebar post={post} className="hidden md:block"/>}
|
||||
|
||||
<MainArea className="relative">
|
||||
{post
|
||||
@@ -179,9 +179,7 @@ const PostDetailPage: FC<Props> = ({ user }) => {
|
||||
: 'Loading...'}
|
||||
</MainArea>
|
||||
|
||||
<div className="md:hidden">
|
||||
{post && <TagDetailSidebar post={post} sp/>}
|
||||
</div>
|
||||
{post && <TagDetailSidebar post={post} sp className="md:hidden"/>}
|
||||
</div>)
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ const PostListPage: FC = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
data-sidebar-container
|
||||
className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden"
|
||||
ref={containerRef}>
|
||||
<Helmet>
|
||||
|
||||
新しい課題から参照
ユーザをブロックする