From 091f97968b010e08545b7a4997cdd87885eed266 Mon Sep 17 00:00:00 2001 From: wypark Date: Sat, 6 Jun 2026 23:22:46 +0900 Subject: [PATCH] . --- LOG.md | 7 +++ src/components/layout/DesktopDock.tsx | 62 ++++++++++++++++++++------- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/LOG.md b/LOG.md index e21c5b3..a1f955d 100644 --- a/LOG.md +++ b/LOG.md @@ -1,5 +1,12 @@ # Work Log +## 2026-06-06 + +- Fixed the desktop Dock so route changes from Dock menu links no longer leave focus-held open state behind when the Dock is not pinned. +- Kept the saved `dock-pinned-v2` preference unchanged during navigation and only released transient Dock focus/hover state after menu clicks and pathname changes. +- Validation: `npm run lint` passed and `npm run build` passed. In-app browser verification on `http://localhost:3111/` confirmed that after unpinning the Dock, clicking the Dock archive menu navigates to `/archive`, the pin button remains `aria-pressed=false`, and the Dock collapses after the pointer leaves. +- Recommended next task: do one visual pass on the Dock at narrow desktop widths to confirm the collapse animation still feels smooth around the sidebar offset. + ## 2026-06-02 - Refined the public layout after desktop/mobile review: the shell now guards horizontal overflow, uses route-aware bottom padding, hides the top menubar on mobile, and opens the mobile sidebar/search panel from the Dock. diff --git a/src/components/layout/DesktopDock.tsx b/src/components/layout/DesktopDock.tsx index 71c6135..2fdd194 100644 --- a/src/components/layout/DesktopDock.tsx +++ b/src/components/layout/DesktopDock.tsx @@ -1,6 +1,6 @@ 'use client'; -import { type CSSProperties, type FocusEvent, useEffect, useRef, useState } from 'react'; +import { type CSSProperties, type FocusEvent, useCallback, useEffect, useRef, useState } from 'react'; import Link from 'next/link'; import { usePathname, useRouter } from 'next/navigation'; import { clsx } from 'clsx'; @@ -124,6 +124,7 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De const [isDockHovered, setIsDockHovered] = useState(false); const [isDockFocused, setIsDockFocused] = useState(false); const [isDockClosing, setIsDockClosing] = useState(false); + const isPinnedRef = useRef(isPinned); const dockZoneRef = useRef(null); const dockLeaveTimerRef = useRef(null); const dockCollapseTimerRef = useRef(null); @@ -132,7 +133,7 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De const isDockOpen = isPinned || isDockHovered || isDockFocused; const isDockRangeExpanded = isDockOpen || isDockClosing; - const clearDockTimers = () => { + const clearDockTimers = useCallback(() => { if (dockLeaveTimerRef.current !== null) { window.clearTimeout(dockLeaveTimerRef.current); dockLeaveTimerRef.current = null; @@ -142,7 +143,7 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De window.clearTimeout(dockCollapseTimerRef.current); dockCollapseTimerRef.current = null; } - }; + }, []); const expandDock = () => { clearDockTimers(); @@ -150,7 +151,7 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De setIsDockHovered(true); }; - const startDockCollapse = () => { + const startDockCollapse = useCallback(() => { clearDockTimers(); setIsDockHovered(false); setIsDockClosing(true); @@ -158,7 +159,7 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De setIsDockClosing(false); dockCollapseTimerRef.current = null; }, DOCK_COLLAPSE_MS); - }; + }, [clearDockTimers]); const scheduleDockCollapse = () => { if (isPinned || isDockFocused) return; @@ -169,10 +170,44 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De }, DOCK_LEAVE_DELAY_MS); }; + const blurFocusedDockItem = useCallback(() => { + if ( + document.activeElement instanceof HTMLElement && + dockZoneRef.current?.contains(document.activeElement) + ) { + document.activeElement.blur(); + } + }, []); + + const releaseUnpinnedDockAfterNavigation = useCallback(() => { + if (isPinnedRef.current) return; + + const isPointerInsideDock = dockZoneRef.current?.matches(':hover') ?? false; + + blurFocusedDockItem(); + setIsDockFocused(false); + + if (isPointerInsideDock) { + clearDockTimers(); + setIsDockClosing(false); + setIsDockHovered(true); + return; + } + + startDockCollapse(); + }, [blurFocusedDockItem, clearDockTimers, startDockCollapse]); + + useEffect(() => { + isPinnedRef.current = isPinned; + }, [isPinned]); + useEffect(() => { const frameId = window.requestAnimationFrame(() => { const savedValue = window.localStorage.getItem(DOCK_PINNED_STORAGE_KEY); - setIsPinned(savedValue === null ? true : savedValue === 'true'); + const nextValue = savedValue === null ? true : savedValue === 'true'; + + isPinnedRef.current = nextValue; + setIsPinned(nextValue); }); return () => window.cancelAnimationFrame(frameId); @@ -181,14 +216,15 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De useEffect(() => { const frameId = window.requestAnimationFrame(() => { setIsMoreOpen(false); + releaseUnpinnedDockAfterNavigation(); }); return () => window.cancelAnimationFrame(frameId); - }, [pathname]); + }, [pathname, releaseUnpinnedDockAfterNavigation]); useEffect(() => { return () => clearDockTimers(); - }, []); + }, [clearDockTimers]); const handlePinnedChange = () => { const nextValue = !isPinned; @@ -198,6 +234,7 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De pinToggleStartedByPointerRef.current = false; window.localStorage.setItem(DOCK_PINNED_STORAGE_KEY, String(nextValue)); clearDockTimers(); + isPinnedRef.current = nextValue; setIsPinned(nextValue); if (nextValue) { @@ -208,13 +245,7 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De if (startedByPointer) { setIsDockFocused(false); - - if ( - document.activeElement instanceof HTMLElement && - dockZoneRef.current?.contains(document.activeElement) - ) { - document.activeElement.blur(); - } + blurFocusedDockItem(); } if (isPointerInsideDock) { @@ -387,6 +418,7 @@ export default function DesktopDock({ isSidebarCollapsed, onOpenMobileMenu }: De aria-current={active ? 'page' : undefined} className={itemClass} style={toneStyle} + onClick={releaseUnpinnedDockAfterNavigation} > {content}