Browse Source

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 ngày trước cách đây
mục cha
commit
1864835fd1
1 tập tin đã thay đổi với 26 bổ sung9 xóa
  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 ---------------------------------------------------