소스 검색

Fix: filter dropdown close — grace timer for transit gap + close on Clear

- The mt-1 gap between the trigger button and the panel meant the
  cursor briefly exited the root while moving from button to panel,
  firing mouseleave and closing the dropdown before the user could
  reach it. Replaced the naive mouseleave-close with a 250 ms grace
  timer: mouseleave on root or panel schedules close; mouseenter on
  either cancels it. The cursor now has a forgiving window to cross
  the gap without losing the dropdown.
- Owner / Status Clear buttons now close the dropdown after wiping
  the filter set — matching the user's mental model that "Clear"
  finishes the interaction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chiappa 3 일 전
부모
커밋
1864835fd1
1개의 변경된 파일26개의 추가작업 그리고 9개의 파일을 삭제
  1. 26 9
      public/assets/js/sprint-planner.js

+ 26 - 9
public/assets/js/sprint-planner.js

@@ -734,6 +734,7 @@
         persistOwnerFilter();
         updateOwnerFilterUi();
         applyFilters();
+        setHidden(qs('[data-owner-filter-dropdown]'), true);
     });
 
     on(root, 'click', '[data-owner-filter-trigger]', function (ev) {
@@ -816,6 +817,7 @@
         persistStatusFilter();
         updateStatusFilterUi();
         applyFilters();
+        setHidden(qs('[data-status-filter-dropdown]'), true);
     });
 
     on(root, 'click', '[data-status-filter-trigger]', function (ev) {
@@ -979,20 +981,35 @@
         }
     });
 
-    // Close dropdowns when the cursor leaves the trigger+panel root. The
-    // panel sits inside the root (`<div class="relative">`), so a single
-    // mouseleave on the root fires only when the cursor exits both the
-    // button and the dropdown.
+    // Close dropdowns when the cursor leaves both the trigger and the
+    // panel. The panel is absolutely positioned with a small mt-1 gap,
+    // so a naive mouseleave on the root fires while the cursor is in
+    // transit between button and panel — we use a 250 ms grace timer
+    // that is cancelled if the cursor enters the panel (or re-enters
+    // the root) within the window.
     [
         ['[data-owner-filter-root]',  '[data-owner-filter-dropdown]'],
         ['[data-status-filter-root]', '[data-status-filter-dropdown]'],
         ['[data-columns-root]',       '[data-columns-dropdown]'],
     ].forEach(function (pair) {
-        const r = qs(pair[0]);
-        if (!r) { return; }
-        r.addEventListener('mouseleave', function () {
-            setHidden(qs(pair[1]), true);
-        });
+        const r  = qs(pair[0]);
+        const dd = qs(pair[1]);
+        if (!r || !dd) { return; }
+        let timer = null;
+        const cancel = function () {
+            if (timer) { clearTimeout(timer); timer = null; }
+        };
+        const schedule = function () {
+            cancel();
+            timer = setTimeout(function () {
+                setHidden(dd, true);
+                timer = null;
+            }, 250);
+        };
+        r.addEventListener('mouseenter',  cancel);
+        r.addEventListener('mouseleave',  schedule);
+        dd.addEventListener('mouseenter', cancel);
+        dd.addEventListener('mouseleave', schedule);
     });
 
     // --- Reset filters ---------------------------------------------------