-
Worklog — SZ-59
Title: Fix: Modal dismissed on focus loss (Lose Focus, Lose Modal)
Problem Statement
When a modal dialog is open, losing focus causes the modal to dismiss automatically. This occurs when:
- Clicking outside the browser window
- Switching tabs or applications
- Interacting with background UI unintentionally
The dismissal is not user-initiated (Cancel / Close), leading to disrupted workflows and potential loss of unsaved input.
Reproduction Steps
- Open a modal dialog containing user input
- Shift focus away from the modal (click outside window, switch tab, or blur event)
- Observe that the modal closes automatically
Expected: Modal remains open until explicit user action
Actual: Modal dismisses on focus loss
Investigation
- Reproduced the issue consistently across multiple modal entry points
- Traced modal lifecycle and event handlers responsible for dismissal
- Observed that modal close logic is triggered by:
onBlur/ focus loss events- Backdrop click handling without intent differentiation
- Verified that no explicit user dismissal action is performed
This confirms the issue is event-handling related, not routing or state corruption.
Root Cause
Modal dismissal logic is bound too broadly to focus or blur events.
Specifically:
- Loss of focus is treated as equivalent to an explicit dismiss action
- Event propagation from backdrop or document-level listeners is not constrained
- The modal cannot distinguish between:
- Intentional dismissal (Close / Cancel / Esc)
- Incidental focus changes (tab switch, window blur)
Design Decision
Modal dismissal must occur only on explicit user intent:
- Close button
- Cancel action
- Explicit backdrop click (where applicable)
- Escape key (if supported)
Loss of focus alone must not dismiss the modal.
Fix Strategy
- Decouple modal visibility from
onBlur/ focus loss events - Guard backdrop click handlers to ensure dismissal only occurs when:
- The click target is the backdrop itself
- The click originates within the same window context
- Prevent event bubbling from triggering unintended dismissals
- Ensure modal state remains stable across focus changes
Implementation Notes
- Removed or constrained focus/blur listeners tied to modal close logic
- Tightened backdrop click detection to avoid false positives
- Verified keyboard navigation and accessibility remain intact
- Ensured no regression in explicit close / cancel flows
Verification
- Confirmed modal remains open when:
- Switching browser tabs
- Switching applications
- Clicking outside the browser window
- Confirmed modal closes only when:
- Close / Cancel is clicked
- Escape key is pressed
- Backdrop is intentionally clicked
- Manually tested across affected modal variants
Impact
- Prevents accidental modal dismissal
- Protects unsaved user input
- Aligns modal behavior with user expectations and standard UX patterns
- No breaking changes to public API or modal consumers
Status
Fix implemented and verified locally.
Ready for review. -
Root cause:
In AuthContext.tsx, there is a window focus listener:
useEffect(() => { if (!user) return; const handleFocus = () => { loadCurrentUser(); // Called when browser window regains focus }; window.addEventListener("focus", handleFocus); return () => window.removeEventListener("focus", handleFocus); }, [user, loadCurrentUser]);When I switch windows and come back, the browser fires a focus event, which triggers loadCurrentUser() loadCurrentUser() sets loading: true:
const loadCurrentUser = useCallback(async () => { setLoading(true); // **This is the problem** // ... fetch user data ... setLoading(false); }, []);In App.tsx, the ProtectedLayout checks the loading state:
function ProtectedLayout() { const { user, loading } = useAuth(); if (loading) { return <div>Loading...</div>; // Renders loading screen } // ... renders sidebar, navbar, and pages }When loading: true, React unmounts the entire protected layout (including UserListPage and all its state like openCreate: true), then remounts it when loading finishes. This reset all component state, closing the modal.
The Fix I replaced the focus effect in AuthContext.tsx with this:
// Check session when tab regains focus useEffect(() => { if (!user) return; const handleFocus = () => { // Validate session WITHOUT setting loading state API.get("/users/me") .then(res => setUser(res.data)) .catch(err => { if (err?.response?.status === 401 || err?.response?.status === 403) { // Only reload if session expired loadCurrentUser(); } }); }; window.addEventListener("focus", handleFocus); return () => window.removeEventListener("focus", handleFocus); }, [user, loadCurrentUser]);This fix works because
- The session is validated silently in the background
- loading state is NOT set, so the UI doesn't unmount
- Component state (like modal open/closed) is preserved
- Only if the session actually expired (401/403) does it call loadCurrentUser() which would properly redirect to login
This fix applies globally to all modals in Sztab UI because it fixes the root cause in the authentication context.
-
Work log: Spent ~3h debugging modal auto-dismiss issue, including tracing focus events, auth lifecycle, and component unmount behavior. Identified root cause in AuthContext window focus handler triggering global loading state and unmounting protected layout. Spent ~1h implementing fix, refactoring modal handling, and testing across window focus changes.
| Type |
Bug
|
| Priority |
Major
|
| Assignee | |
| Version |
1.8
|
| Sprints |
n/a
|
| Customer |
n/a
|
Description
If the browser window or tab loses focus while a modal dialog is open, the modal may close automatically, causing loss of unsaved input.
Impact
User data entry may be lost unintentionally.
Workaround
Avoid switching tabs or applications while editing modal content.
Planned Fix
Focus management and modal lifecycle handling to be corrected in a future release.