소스 검색

Fix: stepper popover now closes when the bound input loses focus

Follow-up to the slider-only rewrite (15b2d24). The previous close
triggers (pointer-leave-popup, outside-pointerdown, Escape, Tab) all
required the user to go through the popup or press a key. A user
who clicked the input, then moved focus elsewhere via a method that
didn't land on any of those paths — e.g. clicking a non-focusable
element nearby whose pointerdown didn't bubble, or the browser stealing
focus via the devtools — found the popup left hanging.

Replaced the Tab-specific keydown handler with a generic `focusout`
listener on `document`. When focus leaves either the bound input or a
focusable element inside the popover (like the range slider thumb),
the popover closes — unless focus is moving into the popover itself
(`pop.contains(next)`) or onto another eligible `input[type=number]`
(which the click/focusin handlers will rebind us to seamlessly, no
flicker). Blur-to-body, blur-to-a-non-eligible-control, and Tab-out
all now close us.

Escape still closes + returns focus. Outside pointerdown is kept as
a belt-and-braces for touch devices where focus semantics are
inconsistent.

phpunit: 88 / 208 unchanged. node --check clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
achiappa 2 주 전
부모
커밋
f189ef70a1
1개의 변경된 파일22개의 추가작업 그리고 11개의 파일을 삭제
  1. 22 11
      public/assets/js/number-stepper.js

+ 22 - 11
public/assets/js/number-stepper.js

@@ -211,22 +211,33 @@
         close(false);
     });
 
-    // Escape closes and returns focus; Tab out of the input + popover
-    // closes without focus return.
+    // Blur close. Fires when focus leaves either the bound input or a
+    // focusable element inside the popover (e.g. the slider thumb).
+    // If focus is moving into the popover itself, or onto another
+    // eligible input (which the click/focusin handlers will rebind us
+    // to), we stay open. Any other destination — another cell, the
+    // page body, a different page element — closes us. This plugs
+    // the gap where a click elsewhere on the page blurred the input
+    // but never landed on a registered "outside" target.
+    function isOurs(el) {
+        return !!el && (el === boundInput || (pop && pop.contains && pop.contains(el)));
+    }
+    document.addEventListener('focusout', function (ev) {
+        if (!pop || pop.hidden) { return; }
+        if (!isOurs(ev.target)) { return; }
+        const next = ev.relatedTarget;
+        if (isOurs(next))      { return; }  // staying inside our realm
+        if (isEligible(next))  { return; }  // another number input — will rebind
+        close(false);
+    });
+
+    // Escape closes and returns focus. Tab is now handled via the
+    // generic focusout listener above.
     document.addEventListener('keydown', function (ev) {
         if (!pop || pop.hidden) { return; }
         if (ev.key === 'Escape') {
             ev.preventDefault();
             close(true);
-            return;
-        }
-        if (ev.key === 'Tab') {
-            setTimeout(function () {
-                const active = document.activeElement;
-                if (active === boundInput) { return; }
-                if (pop.contains(active)) { return; }
-                close(false);
-            }, 0);
         }
     });