From 73f5642c57f6990e5f2e4a066161b240fefecd39 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Sun, 1 Mar 2026 22:52:15 -0600 Subject: [PATCH] feat: improve visual feedback for insertion points and folder expansion * add accessibility announcement for cancelled drags --- src/App.css | 26 ++++++++++++++++++- src/components/Sidebar/DocumentItem.tsx | 4 +-- .../Sidebar/SidebarLocationItem.tsx | 14 ++++++++-- .../Sidebar/useSidebarInternalDnD.ts | 13 +++++++++- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/App.css b/src/App.css index 003de71..d00dd29 100644 --- a/src/App.css +++ b/src/App.css @@ -187,6 +187,15 @@ body, } } +@keyframes sidebar-spring-folder-pulse { + 0% { + box-shadow: 0 0 0 0 rgba(51, 177, 255, 0.22); + } + 100% { + box-shadow: 0 0 0 6px rgba(51, 177, 255, 0); + } +} + .sidebar-drop-pulse { animation: sidebar-drop-pulse 0.58s ease-out 2 forwards; } @@ -195,6 +204,20 @@ body, animation: sidebar-drop-edge-pulse 0.36s ease-out 2 forwards; } +.sidebar-drop-insertion-line { + background: var(--color-border-interactive); +} + +.sidebar-drop-insertion-dot { + box-shadow: 0 0 0 1px color-mix(in oklab, var(--color-border-interactive) 55%, transparent); +} + +.sidebar-folder-spring-pending { + border: 1px dashed color-mix(in oklab, var(--color-border-interactive) 70%, transparent); + background: color-mix(in oklab, var(--color-layer-hover-01) 78%, var(--color-border-interactive) 22%); + animation: sidebar-spring-folder-pulse 0.8s ease-out infinite; +} + .sidebar-drag-ghost { position: fixed; top: -9999px; @@ -236,7 +259,8 @@ body, animation: none; } .sidebar-drop-pulse, - .sidebar-drop-edge-pulse { + .sidebar-drop-edge-pulse, + .sidebar-folder-spring-pending { animation: none; } } diff --git a/src/components/Sidebar/DocumentItem.tsx b/src/components/Sidebar/DocumentItem.tsx index e6ff459..a2ba6b0 100644 --- a/src/components/Sidebar/DocumentItem.tsx +++ b/src/components/Sidebar/DocumentItem.tsx @@ -150,11 +150,11 @@ export function DocumentItem( {closestEdge && (
-
+
)}
diff --git a/src/components/Sidebar/SidebarLocationItem.tsx b/src/components/Sidebar/SidebarLocationItem.tsx index 3bdd222..9cc1c83 100644 --- a/src/components/Sidebar/SidebarLocationItem.tsx +++ b/src/components/Sidebar/SidebarLocationItem.tsx @@ -87,6 +87,7 @@ type SidebarTreeContextValue = { activeDropFolderPath?: string; activeDropFolderEdge?: Edge | null; activeDropFolderIntent?: "into" | "between"; + pendingSpringFolderPath?: string | null; activeDropDocumentPath?: string; activeDropDocumentEdge?: Edge | null; activeDropDocumentIsReorder?: boolean; @@ -236,6 +237,7 @@ function NestedDirectoryItem({ node, level, expandedDirectories, onToggleDirecto const folderEdge = isActiveFolderTarget ? dropIndicators.activeDropFolderEdge ?? null : null; const showInsertionLine = isActiveFolderTarget && dropIndicators.activeDropFolderIntent === "between" && (folderEdge === "top" || folderEdge === "bottom"); + const isSpringPending = dropIndicators.pendingSpringFolderPath === node.path && !isExpanded; const edgeStyle = useMemo(() => { if (!folderEdge) { return {}; @@ -276,6 +278,7 @@ function NestedDirectoryItem({ node, level, expandedDirectories, onToggleDirecto className={cn( "pb-0.5", isDropIntoTarget ? "rounded border-2 border-border-interactive bg-layer-hover-01" : "", + isSpringPending ? "sidebar-folder-spring-pending rounded" : "", skipAnimation ? "" : "transition-[box-shadow,background-color] duration-150", )}> -
+
) : null} @@ -354,6 +357,7 @@ function SidebarLocationItemComponent( ) { const { filenameVisibility, documentActions, onToggleLocation, openDocumentOperation } = useSidebarLocationContext(); const [expandedDirectories, setExpandedDirectories] = useState>(new Set()); + const [pendingSpringFolderPath, setPendingSpringFolderPath] = useState(null); const hoverExpandRef = useRef<{ path: string; timer: ReturnType } | null>(null); const dragExpandedSnapshotRef = useRef | null>(null); const autoExpandedDuringDragRef = useRef>(new Set()); @@ -420,10 +424,12 @@ function SidebarLocationItemComponent( const clearHoverExpand = useCallback(() => { if (!hoverExpandRef.current) { + setPendingSpringFolderPath(null); return; } globalThis.clearTimeout(hoverExpandRef.current.timer); hoverExpandRef.current = null; + setPendingSpringFolderPath(null); }, []); useEffect(() => { @@ -441,6 +447,7 @@ function SidebarLocationItemComponent( clearHoverExpand(); const folderPath = activeDropFolderPath; + setPendingSpringFolderPath(folderPath); hoverExpandRef.current = { path: folderPath, timer: globalThis.setTimeout(() => { @@ -453,6 +460,7 @@ function SidebarLocationItemComponent( } return new Set([...previous, folderPath]); }); + setPendingSpringFolderPath((currentPath) => currentPath === folderPath ? null : currentPath); hoverExpandRef.current = null; }, 800), }; @@ -508,6 +516,7 @@ function SidebarLocationItemComponent( activeDropFolderPath, activeDropFolderEdge, activeDropFolderIntent, + pendingSpringFolderPath, activeDropDocumentPath, activeDropDocumentEdge, activeDropDocumentIsReorder, @@ -522,6 +531,7 @@ function SidebarLocationItemComponent( activeDropFolderEdge, activeDropFolderIntent, activeDropFolderPath, + pendingSpringFolderPath, activeDragDocumentPath, documentActions, filenameVisibility, diff --git a/src/components/Sidebar/useSidebarInternalDnD.ts b/src/components/Sidebar/useSidebarInternalDnD.ts index e1851b5..9802ac0 100644 --- a/src/components/Sidebar/useSidebarInternalDnD.ts +++ b/src/components/Sidebar/useSidebarInternalDnD.ts @@ -154,6 +154,7 @@ export function useSidebarInternalDnD( const dropZoneRef = useRef(null); const nativeDragPosRef = useRef<{ x: number; y: number } | null>(null); const resolvedDestinationRef = useRef(UNSET_DROP_DESTINATION); + const activeDragTitleRef = useRef(null); const edgeScrollRafRef = useRef(null); const edgeScrollDirectionRef = useRef<1 | -1 | 0>(0); const dragLeaveClearTimerRef = useRef | null>(null); @@ -275,13 +276,20 @@ export function useSidebarInternalDnD( }, [cancelDeferredDragLeaveClear, stopEdgeScroll]); useEffect(() => { - const handleGlobalDragEnd = () => { + const handleGlobalDragEnd = (event: Event) => { + const dropEffect = (event as globalThis.DragEvent).dataTransfer?.dropEffect; + const dragTitle = activeDragTitleRef.current; + if (dropEffect === "none" && dragTitle) { + announce(`Cancelled drag for ${dragTitle}`); + } + setActiveDropTarget(null); setIsDraggingInternal(false); setDragGhostLabel(null); setSuppressActiveDragSourceOpacity(false); setActiveDragDocumentPath(null); setActiveDragDocumentLocationId(null); + activeDragTitleRef.current = null; resolvedDestinationRef.current = UNSET_DROP_DESTINATION; cancelDeferredDragLeaveClear(); stopEdgeScroll(); @@ -325,6 +333,7 @@ export function useSidebarInternalDnD( } setIsDraggingInternal(true); setDragGhostLabel(source.data.title); + activeDragTitleRef.current = source.data.title; resolvedDestinationRef.current = UNSET_DROP_DESTINATION; setActiveDropTarget(null); setSuppressActiveDragSourceOpacity(false); @@ -390,6 +399,7 @@ export function useSidebarInternalDnD( setSuppressActiveDragSourceOpacity(false); setActiveDragDocumentPath(null); setActiveDragDocumentLocationId(null); + activeDragTitleRef.current = null; cancelDeferredDragLeaveClear(); stopEdgeScroll(); @@ -670,6 +680,7 @@ export function useSidebarInternalDnD( setSuppressActiveDragSourceOpacity(false); setActiveDragDocumentPath(null); setActiveDragDocumentLocationId(null); + activeDragTitleRef.current = null; resolvedDestinationRef.current = UNSET_DROP_DESTINATION; cancelDeferredDragLeaveClear(); stopEdgeScroll(); -- 2.51.2