From c0879ac117e6b1d16cc41edac81c152fcbcdc1ea Mon Sep 17 00:00:00 2001 From: miteruzo Date: Tue, 7 Jul 2026 20:57:12 +0900 Subject: [PATCH 1/8] #268 --- .../components/DraggableDroppableTagRow.tsx | 21 +++++++----- frontend/src/components/TagDetailSidebar.tsx | 33 +++++++++++++++---- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/DraggableDroppableTagRow.tsx b/frontend/src/components/DraggableDroppableTagRow.tsx index f465bea..0e24398 100644 --- a/frontend/src/components/DraggableDroppableTagRow.tsx +++ b/frontend/src/components/DraggableDroppableTagRow.tsx @@ -57,7 +57,8 @@ const DraggableDroppableTagRow: FC = ({ tag, nestLevel, pathKey, parentTa isDragging: dragging } = useDraggable ({ id: dndId, data: { kind: 'tag', tagId: tag.id, - parentTagId } }) + parentTagId, + nestLevel } }) const { setNodeRef: setDropRef, isOver: over } = useDroppable ({ id: dndId, @@ -90,18 +91,20 @@ const DraggableDroppableTagRow: FC = ({ tag, nestLevel, pathKey, parentTa e.stopPropagation () } }} - ref={node => { - setDragRef (node) - setDropRef (node) - }} - style={style} + ref={setDropRef} className={cn ( 'min-w-0 max-w-full overflow-hidden rounded select-none', + sp && 'touch-pan-y', over && 'ring-2 ring-offset-2')} - {...attributes} - {...listeners}> + > = ({ className, post, sp }) => { sp = Boolean (sp) @@ -212,7 +218,7 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { return tagsTmp }, [post]) - const [activeTagId, setActiveTagId] = useState (null) + const [activeTagDrag, setActiveTagDrag] = useState (null) const [dragging, setDragging] = useState (false) const [saving, setSaving] = useState (false) const [tags, setTags] = useState (baseTags) @@ -328,9 +334,20 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { { + const byPointer = pointerWithin (args) + return byPointer.length > 0 ? byPointer : rectIntersection (args) + }} onDragStart={e => { if (e.active.data.current?.kind === 'tag') - setActiveTagId (e.active.data.current?.tagId ?? null) + { + const tagId = e.active.data.current?.tagId + const nestLevel = e.active.data.current?.nestLevel + setActiveTagDrag ( + tagId == null || nestLevel == null + ? null + : { tagId, nestLevel }) + } setDragging (true) suppressClickRef.current = true document.body.style.userSelect = 'none' @@ -341,13 +358,13 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { suppressClickRef.current = false}, { capture: true, once: true }) }} onDragCancel={() => { - setActiveTagId (null) + setActiveTagDrag (null) setDragging (false) document.body.style.userSelect = '' suppressClickRef.current = false }} onDragEnd={async e => { - setActiveTagId (null) + setActiveTagDrag (null) setDragging (false) await onDragEnd (e) document.body.style.userSelect = '' @@ -443,9 +460,11 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => {
- {activeTagId != null && (() => { - const tag = findTag (tags, activeTagId) - return tag && + {activeTagDrag != null && (() => { + const tag = findTag (tags, activeTagDrag.tagId) + return ( + tag + && ) }) ()}
-- 2.34.1 From 2d6af0aa5b67fe674e434aa52a982ce091b22218 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Tue, 7 Jul 2026 21:01:57 +0900 Subject: [PATCH 2/8] #268 --- .../components/DraggableDroppableTagRow.tsx | 4 +- frontend/src/components/TagDetailSidebar.tsx | 72 +++++++++++++++++-- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/DraggableDroppableTagRow.tsx b/frontend/src/components/DraggableDroppableTagRow.tsx index 0e24398..f1ecf20 100644 --- a/frontend/src/components/DraggableDroppableTagRow.tsx +++ b/frontend/src/components/DraggableDroppableTagRow.tsx @@ -58,7 +58,8 @@ const DraggableDroppableTagRow: FC = ({ tag, nestLevel, pathKey, parentTa data: { kind: 'tag', tagId: tag.id, parentTagId, - nestLevel } }) + nestLevel, + dndId } }) const { setNodeRef: setDropRef, isOver: over } = useDroppable ({ id: dndId, @@ -99,6 +100,7 @@ const DraggableDroppableTagRow: FC = ({ tag, nestLevel, pathKey, parentTa > = ({ className, post, sp }) => { @@ -222,16 +228,59 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { const [dragging, setDragging] = useState (false) const [saving, setSaving] = useState (false) const [tags, setTags] = useState (baseTags) + const [dragOverlayOffset, setDragOverlayOffset] = + useState ({ x: 0, y: 0 }) const flatTagsByCategory = useMemo ( () => buildFlatTagByCategory (tags), [tags], ) const suppressClickRef = useRef (false) + const dragStartRectRef = useRef (null) + const offsetFrameRef = useRef (null) const sensors = useSensors ( useSensor (MouseSensor, { activationConstraint: { distance: 6 } }), useSensor (TouchSensor, { activationConstraint: { delay: 250, tolerance: 8 } })) + const dragOverlayModifiers = useMemo ( + () => ( + sp + ? [({ transform }) => ({ + ...transform, + x: transform.x + dragOverlayOffset.x, + y: transform.y + dragOverlayOffset.y })] + : []), + [dragOverlayOffset.x, dragOverlayOffset.y, sp], + ) + + useLayoutEffect (() => { + if (!(sp && dragging && activeTagDrag && dragStartRectRef.current)) + { + setDragOverlayOffset ({ x: 0, y: 0 }) + return + } + + offsetFrameRef.current = requestAnimationFrame (() => { + const node = document.querySelector ( + `[data-tag-dnd-id="${ activeTagDrag.dndId }"]`, + ) + + if (!(node && dragStartRectRef.current)) + return + + const currentRect = node.getBoundingClientRect () + const initialRect = dragStartRectRef.current + + setDragOverlayOffset ({ + x: currentRect.left - initialRect.left, + y: currentRect.top - initialRect.top }) + }) + + return () => { + if (offsetFrameRef.current != null) + cancelAnimationFrame (offsetFrameRef.current) + } + }, [activeTagDrag, dragging, sp]) const reloadTags = async (): Promise => { setTags (buildTagByCategory (await apiGet (`/posts/${ post.id }`))) @@ -343,10 +392,19 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { { const tagId = e.active.data.current?.tagId const nestLevel = e.active.data.current?.nestLevel + const dndId = e.active.data.current?.dndId + dragStartRectRef.current = + e.active.rect.current.initial + ? new DOMRect ( + e.active.rect.current.initial.left, + e.active.rect.current.initial.top, + e.active.rect.current.initial.width, + e.active.rect.current.initial.height) + : null setActiveTagDrag ( - tagId == null || nestLevel == null + tagId == null || nestLevel == null || !(dndId) ? null - : { tagId, nestLevel }) + : { tagId, nestLevel, dndId }) } setDragging (true) suppressClickRef.current = true @@ -360,12 +418,16 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { onDragCancel={() => { setActiveTagDrag (null) setDragging (false) + setDragOverlayOffset ({ x: 0, y: 0 }) + dragStartRectRef.current = null document.body.style.userSelect = '' suppressClickRef.current = false }} onDragEnd={async e => { setActiveTagDrag (null) setDragging (false) + setDragOverlayOffset ({ x: 0, y: 0 }) + dragStartRectRef.current = null await onDragEnd (e) document.body.style.userSelect = '' }} @@ -458,7 +520,7 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { )} - +
{activeTagDrag != null && (() => { const tag = findTag (tags, activeTagDrag.tagId) -- 2.34.1 From 33f9c6602f00f4265870b6e847cc9e789e77bc5c Mon Sep 17 00:00:00 2001 From: miteruzo Date: Tue, 7 Jul 2026 22:34:58 +0900 Subject: [PATCH 3/8] #268 --- .../components/DraggableDroppableTagRow.tsx | 10 +- frontend/src/components/TagDetailSidebar.tsx | 152 +++++++----------- 2 files changed, 67 insertions(+), 95 deletions(-) diff --git a/frontend/src/components/DraggableDroppableTagRow.tsx b/frontend/src/components/DraggableDroppableTagRow.tsx index f1ecf20..2f1451a 100644 --- a/frontend/src/components/DraggableDroppableTagRow.tsx +++ b/frontend/src/components/DraggableDroppableTagRow.tsx @@ -58,15 +58,14 @@ const DraggableDroppableTagRow: FC = ({ tag, nestLevel, pathKey, parentTa data: { kind: 'tag', tagId: tag.id, parentTagId, - nestLevel, - dndId } }) + nestLevel } }) const { setNodeRef: setDropRef, isOver: over } = useDroppable ({ id: dndId, data: { kind: 'tag', tagId: tag.id } }) const style: CSSProperties = { transform: CSS.Translate.toString (transform), - visibility: dragging ? 'hidden' : 'visible' } + visibility: dragging && !(sp) ? 'hidden' : 'visible' } return (
= ({ tag, nestLevel, pathKey, parentTa return const dx = e.clientX - p.x const dy = e.clientY - p.y + if (dx * dx + dy * dy >= 9) armEatNextClick () }} onPointerUpCapture={() => { downPosRef.current = null }} + onPointerCancelCapture={() => { + downPosRef.current = null + }} onClickCapture={e => { if (suppressClickRef.current) { @@ -100,7 +103,6 @@ const DraggableDroppableTagRow: FC = ({ tag, nestLevel, pathKey, parentTa > { + const byPointer = pointerWithin (args) + return byPointer.length > 0 ? byPointer : rectIntersection (args) +} + +const alwaysMeasureDroppables = { + droppable: { strategy: MeasuringStrategy.Always } } as const + const renderTagTree = ( tag: TagWithSections, @@ -180,6 +188,24 @@ const DropSlot = ({ cat }: { cat: Category }) => { ) } +const EmptyCategoryDropSection = ( + { cat, children }: { cat: Category + children: ReactNode }) => { + const { setNodeRef, isOver: over } = useDroppable ({ + id: `slot:${ cat }`, + data: { kind: 'slot', cat } }) + + return ( +
+ {children} +
    +
  • + {over &&
    } +
  • +
+
) +} + type Props = { className?: string @@ -188,12 +214,7 @@ type Props = { type ActiveTagDrag = { tagId: number - nestLevel: number - dndId: string } - -type DragOverlayOffset = { - x: number - y: number } + nestLevel: number } const TagDetailSidebar: FC = ({ className, post, sp }) => { @@ -228,59 +249,16 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { const [dragging, setDragging] = useState (false) const [saving, setSaving] = useState (false) const [tags, setTags] = useState (baseTags) - const [dragOverlayOffset, setDragOverlayOffset] = - useState ({ x: 0, y: 0 }) const flatTagsByCategory = useMemo ( () => buildFlatTagByCategory (tags), [tags], ) const suppressClickRef = useRef (false) - const dragStartRectRef = useRef (null) - const offsetFrameRef = useRef (null) const sensors = useSensors ( useSensor (MouseSensor, { activationConstraint: { distance: 6 } }), useSensor (TouchSensor, { activationConstraint: { delay: 250, tolerance: 8 } })) - const dragOverlayModifiers = useMemo ( - () => ( - sp - ? [({ transform }) => ({ - ...transform, - x: transform.x + dragOverlayOffset.x, - y: transform.y + dragOverlayOffset.y })] - : []), - [dragOverlayOffset.x, dragOverlayOffset.y, sp], - ) - - useLayoutEffect (() => { - if (!(sp && dragging && activeTagDrag && dragStartRectRef.current)) - { - setDragOverlayOffset ({ x: 0, y: 0 }) - return - } - - offsetFrameRef.current = requestAnimationFrame (() => { - const node = document.querySelector ( - `[data-tag-dnd-id="${ activeTagDrag.dndId }"]`, - ) - - if (!(node && dragStartRectRef.current)) - return - - const currentRect = node.getBoundingClientRect () - const initialRect = dragStartRectRef.current - - setDragOverlayOffset ({ - x: currentRect.left - initialRect.left, - y: currentRect.top - initialRect.top }) - }) - - return () => { - if (offsetFrameRef.current != null) - cancelAnimationFrame (offsetFrameRef.current) - } - }, [activeTagDrag, dragging, sp]) const reloadTags = async (): Promise => { setTags (buildTagByCategory (await apiGet (`/posts/${ post.id }`))) @@ -383,28 +361,17 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { { - const byPointer = pointerWithin (args) - return byPointer.length > 0 ? byPointer : rectIntersection (args) - }} + collisionDetection={sp ? tagCollisionDetection : undefined} + measuring={sp ? alwaysMeasureDroppables : undefined} onDragStart={e => { if (e.active.data.current?.kind === 'tag') { const tagId = e.active.data.current?.tagId const nestLevel = e.active.data.current?.nestLevel - const dndId = e.active.data.current?.dndId - dragStartRectRef.current = - e.active.rect.current.initial - ? new DOMRect ( - e.active.rect.current.initial.left, - e.active.rect.current.initial.top, - e.active.rect.current.initial.width, - e.active.rect.current.initial.height) - : null setActiveTagDrag ( - tagId == null || nestLevel == null || !(dndId) + tagId == null || nestLevel == null ? null - : { tagId, nestLevel, dndId }) + : { tagId, nestLevel }) } setDragging (true) suppressClickRef.current = true @@ -418,16 +385,12 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { onDragCancel={() => { setActiveTagDrag (null) setDragging (false) - setDragOverlayOffset ({ x: 0, y: 0 }) - dragStartRectRef.current = null document.body.style.userSelect = '' suppressClickRef.current = false }} onDragEnd={async e => { setActiveTagDrag (null) setDragging (false) - setDragOverlayOffset ({ x: 0, y: 0 }) - dragStartRectRef.current = null await onDragEnd (e) document.body.style.userSelect = '' }} @@ -441,18 +404,26 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { if (!(categoryTags.length > 0 || dragging)) return null + const sectionTitle = ( + + + {CATEGORY_NAMES[cat]} + + ) + + if (!(categoryTags.length > 0)) + return ( + + {sectionTitle} + ) + return (
- - - {CATEGORY_NAMES[cat]} - - - + {sectionTitle}
    {(tagRelationDisplay === 'grouped' ? (tags[cat] ?? []).flatMap (tag => @@ -520,16 +491,15 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => {
)} - -
- {activeTagDrag != null && (() => { - const tag = findTag (tags, activeTagDrag.tagId) - return ( - tag - && ) - }) ()} -
-
+ {!(sp) && ( + +
+ {activeTagDrag != null && (() => { + const tag = findTag (tags, activeTagDrag.tagId) + return tag && + }) ()} +
+
)} ) } -- 2.34.1 From f8e8d8fbb0142adb96500e2464f9691e8f9f7cb2 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Tue, 7 Jul 2026 22:35:09 +0900 Subject: [PATCH 4/8] #268 --- .../components/DraggableDroppableTagRow.tsx | 2 +- frontend/src/components/TagDetailSidebar.tsx | 21 ++++++++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/DraggableDroppableTagRow.tsx b/frontend/src/components/DraggableDroppableTagRow.tsx index 2f1451a..ea1eda4 100644 --- a/frontend/src/components/DraggableDroppableTagRow.tsx +++ b/frontend/src/components/DraggableDroppableTagRow.tsx @@ -65,7 +65,7 @@ const DraggableDroppableTagRow: FC = ({ tag, nestLevel, pathKey, parentTa data: { kind: 'tag', tagId: tag.id } }) const style: CSSProperties = { transform: CSS.Translate.toString (transform), - visibility: dragging && !(sp) ? 'hidden' : 'visible' } + visibility: dragging ? 'hidden' : 'visible' } return (
{ - const byPointer = pointerWithin (args) - return byPointer.length > 0 ? byPointer : rectIntersection (args) + return pointerWithin (args) } const alwaysMeasureDroppables = { @@ -491,15 +489,14 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { )} - {!(sp) && ( - -
- {activeTagDrag != null && (() => { - const tag = findTag (tags, activeTagDrag.tagId) - return tag && - }) ()} -
-
)} + +
+ {activeTagDrag != null && (() => { + const tag = findTag (tags, activeTagDrag.tagId) + return tag && + }) ()} +
+
) } -- 2.34.1 From 347a0ebbbaf2dc4f1edf0112d78a5a4794bb027c Mon Sep 17 00:00:00 2001 From: miteruzo Date: Tue, 7 Jul 2026 23:04:19 +0900 Subject: [PATCH 5/8] #268 --- frontend/src/components/TagDetailSidebar.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/TagDetailSidebar.tsx b/frontend/src/components/TagDetailSidebar.tsx index 1168e30..965cebc 100644 --- a/frontend/src/components/TagDetailSidebar.tsx +++ b/frontend/src/components/TagDetailSidebar.tsx @@ -7,7 +7,7 @@ import { DndContext, useDroppable, useSensor, useSensors } from '@dnd-kit/core' -import { restrictToWindowEdges } from '@dnd-kit/modifiers' +import { restrictToWindowEdges, snapCenterToCursor } from '@dnd-kit/modifiers' import { useQueryClient } from '@tanstack/react-query' import { motion } from 'framer-motion' import { useEffect, useMemo, useRef, useState } from 'react' @@ -359,8 +359,8 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { { if (e.active.data.current?.kind === 'tag') { @@ -489,7 +489,7 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { )} - +
{activeTagDrag != null && (() => { const tag = findTag (tags, activeTagDrag.tagId) -- 2.34.1 From abeee76ddda55d1d7e1d59d408a6056e2ea1d977 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Tue, 7 Jul 2026 23:22:20 +0900 Subject: [PATCH 6/8] #268 --- .../components/DraggableDroppableTagRow.tsx | 66 ++++++++++++------- frontend/src/components/TagDetailSidebar.tsx | 20 +++++- 2 files changed, 61 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/DraggableDroppableTagRow.tsx b/frontend/src/components/DraggableDroppableTagRow.tsx index ea1eda4..49f9aed 100644 --- a/frontend/src/components/DraggableDroppableTagRow.tsx +++ b/frontend/src/components/DraggableDroppableTagRow.tsx @@ -13,6 +13,7 @@ import type { CSSProperties, FC, MutableRefObject } from 'react' import type { Tag } from '@/types' type Props = { + activeDndId?: string tag: Tag nestLevel: number pathKey: string @@ -21,7 +22,15 @@ type Props = { sp?: boolean } -const DraggableDroppableTagRow: FC = ({ tag, nestLevel, pathKey, parentTagId, suppressClickRef, sp }) => { +const DraggableDroppableTagRow: FC = ({ + activeDndId, + tag, + nestLevel, + pathKey, + parentTagId, + suppressClickRef, + sp, +}) => { const behaviourSettings = useClientBehaviourSettings () const animationMode = behaviourSettings.animation ?? 'normal' const layoutTransition = clientAnimationTransition ( @@ -53,19 +62,23 @@ const DraggableDroppableTagRow: FC = ({ tag, nestLevel, pathKey, parentTa const { attributes, listeners, setNodeRef: setDragRef, - transform, - isDragging: dragging } = useDraggable ({ id: dndId, - data: { kind: 'tag', - tagId: tag.id, - parentTagId, - nestLevel } }) + transform } = useDraggable ({ id: dndId, + data: { kind: 'tag', + dndId, + tagId: tag.id, + parentTagId, + nestLevel } }) const { setNodeRef: setDropRef, isOver: over } = useDroppable ({ id: dndId, data: { kind: 'tag', tagId: tag.id } }) + const activeDragging = activeDndId === dndId const style: CSSProperties = { transform: CSS.Translate.toString (transform), - visibility: dragging ? 'hidden' : 'visible' } + visibility: activeDragging ? 'hidden' : 'visible' } + const innerClassName = cn ( + 'inline-flex min-w-0 max-w-full items-baseline overflow-hidden', + sp && 'touch-pan-y') return (
= ({ tag, nestLevel, pathKey, parentTa sp && 'touch-pan-y', over && 'ring-2 ring-offset-2')} > - - - + {activeDragging + ? ( +
+ +
) + : ( + + + )}
) } diff --git a/frontend/src/components/TagDetailSidebar.tsx b/frontend/src/components/TagDetailSidebar.tsx index 965cebc..45464dd 100644 --- a/frontend/src/components/TagDetailSidebar.tsx +++ b/frontend/src/components/TagDetailSidebar.tsx @@ -43,6 +43,7 @@ const alwaysMeasureDroppables = { const renderTagTree = ( + activeDndId: string | undefined, tag: TagWithSections, nestLevel: number, path: string, @@ -54,6 +55,7 @@ const renderTagTree = ( const self = (
  • a.name < b.name ? -1 : 1) .flatMap (child => - renderTagTree (child, nestLevel + 1, key, suppressClickRef, tag.id, sp))) + renderTagTree ( + activeDndId, + child, + nestLevel + 1, + key, + suppressClickRef, + tag.id, + sp))) ?? [])] } @@ -211,6 +220,7 @@ type Props = { sp?: boolean } type ActiveTagDrag = { + dndId: string tagId: number nestLevel: number } @@ -251,6 +261,7 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { () => buildFlatTagByCategory (tags), [tags], ) + const activeDndId = activeTagDrag?.dndId const suppressClickRef = useRef (false) @@ -366,10 +377,11 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { { const tagId = e.active.data.current?.tagId const nestLevel = e.active.data.current?.nestLevel + const dndId = e.active.data.current?.dndId setActiveTagDrag ( - tagId == null || nestLevel == null + tagId == null || nestLevel == null || dndId == null ? null - : { tagId, nestLevel }) + : { dndId, tagId, nestLevel }) } setDragging (true) suppressClickRef.current = true @@ -426,6 +438,7 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { {(tagRelationDisplay === 'grouped' ? (tags[cat] ?? []).flatMap (tag => renderTagTree ( + activeDndId, tag, 0, `cat-${ cat }`, @@ -437,6 +450,7 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { : (flatTagsByCategory[cat] ?? []).map (tag => (
  • Date: Tue, 7 Jul 2026 23:41:21 +0900 Subject: [PATCH 7/8] #268 --- frontend/src/components/TagDetailSidebar.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/TagDetailSidebar.tsx b/frontend/src/components/TagDetailSidebar.tsx index 45464dd..63fbfd2 100644 --- a/frontend/src/components/TagDetailSidebar.tsx +++ b/frontend/src/components/TagDetailSidebar.tsx @@ -503,7 +503,10 @@ const TagDetailSidebar: FC = ({ className, post, sp }) => { )} - +
    {activeTagDrag != null && (() => { const tag = findTag (tags, activeTagDrag.tagId) -- 2.34.1 From 5cf46406a9dac3f8136f6fd09637d568ea3d8d19 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Wed, 8 Jul 2026 00:00:03 +0900 Subject: [PATCH 8/8] #268 --- .../DraggableDroppableTagRow.test.tsx | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 frontend/src/components/DraggableDroppableTagRow.test.tsx diff --git a/frontend/src/components/DraggableDroppableTagRow.test.tsx b/frontend/src/components/DraggableDroppableTagRow.test.tsx new file mode 100644 index 0000000..8469831 --- /dev/null +++ b/frontend/src/components/DraggableDroppableTagRow.test.tsx @@ -0,0 +1,75 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import DraggableDroppableTagRow from '@/components/DraggableDroppableTagRow' +import { buildTag } from '@/test/factories' +import { renderWithProviders } from '@/test/render' + +const dndKit = vi.hoisted (() => ({ + useDraggable: vi.fn (), + useDroppable: vi.fn (), +})) + +vi.mock ('@dnd-kit/core', () => dndKit) + +const tag = buildTag ({ id: 7, name: 'ドラッグ元', postCount: 3 }) + +const renderRow = (activeDndId?: string) => { + renderWithProviders ( + , + ) +} + +const tagBody = (): HTMLElement => { + const link = document.querySelector ('a[title="ドラッグ元"]') + + if (link == null) + throw new Error ('tag link not found') + + const body = link.closest ('div') + + if (body == null) + throw new Error ('tag body not found') + + return body +} + +describe ('DraggableDroppableTagRow', () => { + beforeEach (() => { + vi.clearAllMocks () + dndKit.useDraggable.mockReturnValue ({ + attributes: { 'aria-describedby': 'drag-source' }, + listeners: { onPointerDown: vi.fn () }, + setNodeRef: vi.fn (), + transform: null }) + dndKit.useDroppable.mockReturnValue ({ + isOver: false, + setNodeRef: vi.fn () }) + }) + + it ('passes dndId through draggable data for active-row tracking', () => { + renderRow () + + expect (dndKit.useDraggable).toHaveBeenCalledWith ( + expect.objectContaining ({ + id: 'tag-node:cat-general-7', + data: expect.objectContaining ({ + dndId: 'tag-node:cat-general-7', + nestLevel: 2, + tagId: 7 }) })) + }) + + it ('hides only the active drag source from the explicit active dnd id', () => { + renderRow ('tag-node:cat-general-7') + expect (tagBody ()).toHaveStyle ({ visibility: 'hidden' }) + }) + + it ('keeps inactive tag rows visible while another tag is dragged', () => { + renderRow ('tag-node:other') + expect (tagBody ()).toHaveStyle ({ visibility: 'visible' }) + }) +}) -- 2.34.1