Prompt and context
Design a draggable mobile sidebar. Desktop users expect Escape to close it, Android users expect the back gesture or button, and an unsaved form must require confirmation. Use CloseWatcher for one close flow and explain cancel, close, requestClose(), destroy(), focus, multiple watchers, unsupported browsers, and history boundaries.
MDN describes CloseWatcher as the interface for making custom components respond to device-specific close actions. The HTML Standard also defines close-watcher grouping and protections around abusing history actions. This article synthesizes public material and does not claim to be a company-specific interview question.
What the interviewer is testing
The interviewer wants to see whether you distinguish a close request from immediate closure, prevent closure during the cancel phase, and keep one close handler responsible for UI cleanup. A strong answer mentions user activation, multiple-watcher grouping, AbortSignal lifetime, focus return, and an explicit-button fallback; a weak answer only listens for keydown.
Questions to clarify first
- Is the sidebar modal, non-modal, or tied to a history navigation state?
- Should unsaved content block every close request or only when particular fields are dirty?
- Should a back gesture close the component or navigate to the previous history entry?
- Do target browsers support CloseWatcher, and may the feature degrade to an explicit close button?
A 30-second answer
“I would normalize every close entry point as a close request. When the sidebar opens, create a CloseWatcher with an AbortSignal. In cancel, check whether the form is dirty; prevent the request and show confirmation when it is, otherwise allow it. close hides the component, restores focus, and cleans up resources, and the explicit close button follows the same path. Unsupported browsers keep the button and a limited Escape fallback; the browser or router owns back navigation when there is no closable component.”
Step-by-step solution
Separate the two actions. requestClose() simulates a device close request and fires cancel; if it is not prevented, close follows. close() fires close immediately without cancel, while destroy() only deactivates the watcher. After saving, unmounting, or leaving a route, explicitly choose a close request or forced cleanup rather than mixing the meanings.
function openDrawer() {
const controller = new AbortController();
const watcher = new CloseWatcher({ signal: controller.signal });
watcher.addEventListener("cancel", (event) => {
if (!formIsDirty()) return;
event.preventDefault();
showDiscardConfirmation(() => watcher.close());
});
watcher.addEventListener("close", () => {
hideDrawer();
restoreFocusToTrigger();
controller.abort();
});
return { watcher, controller };
}The confirmation dialog should not recursively create an unclosable watcher. After an explicit discard confirmation, call the current watcher’s close(); canceling the confirmation leaves the sidebar open. After saving, clear the dirty state and call requestClose() so the same cleanup path runs.
Focus behavior depends on the component. A modal sidebar should move focus to an understandable heading or first control and return it to the trigger on close. A non-modal panel should not steal focus, but it still needs an reachable close button and visible state. A close request must update accessible names, the scrim, scroll locking, and keyboard order, not just toggle CSS.
Multiple watchers have a special boundary. Without user activation, the specification allows watchers to be grouped, so one close request may close several of them. Do not create an unconditional watcher for every small panel. Prefer one top-level closable component owning the watcher; children request closure through events, and unmounting calls destroy() or aborts the associated signal.
A back gesture is not simply a click. The platform may treat it as history traversal or a close request, and the browser’s close-watcher manager selects the target. The application should confirm only when it can intercept and the component is open; with no closable component, normal history back must continue. Do not globally block popstate or back gestures to protect one local form.
Capability detection protects the core path. Check window.CloseWatcher before constructing one. When unsupported, keep the explicit close button, add a limited Escape listener if needed, and let the existing router own back behavior. Do not claim that custom listeners fully reproduce Android back semantics. Measure support, blocked requests, post-confirmation discards, and focus-restoration failures.
Example of a strong answer
I would normalize every sidebar close entry as a close request. On open, create a CloseWatcher with an AbortSignal. cancel only checks unsaved state: when dirty, call preventDefault() and show confirmation; when clean, allow the request. After discard confirmation, call close(); after saving, clear dirty state and call requestClose(). close is the one UI cleanup point: hide the panel, restore focus to the trigger, remove scroll locking, and terminate the watcher.
I would limit watcher count so unactivated instances are not accidentally grouped; unmounting or route changes destroy them. Modal and non-modal components get different focus and scrim rules, and back gestures reach browser history when no component is open. Browsers without CloseWatcher keep an explicit button and limited keyboard fallback without blocking core navigation. Metrics validate close, confirmation, and accessibility behavior.
Common mistakes
- Symptom → Use
close()for every entry point; why it fails → It skips the unsaved confirmation phase; fix → User and platform intent usesrequestClose(), while forced cleanup usesclose(). - Symptom → Listen only for Escape; why it fails → Android back and other device close actions are missed; fix → Use CloseWatcher and keep an explicit button.
- Symptom → Create a watcher for every child panel; why it fails → Unactivated watchers may be grouped; fix → Let the top-level closable component own one instance.
- Symptom → Leave focus on a hidden node; why it fails → Keyboard and assistive-technology users lose their position; fix → Store the trigger and restore focus in
close. - Symptom → Block back events globally; why it fails → History navigation breaks when no component is open; fix → Block only an open component’s close request when confirmation is required.
Follow-up questions and answers
When should you use requestClose() versus close()?
Use requestClose() for user or platform intent because it gives cancel a chance to prevent closure. Use close() after explicit discard confirmation, during unmount cleanup, or whenever closure must be immediate. Both should converge on the same close cleanup handler.
Should the unsaved-confirmation dialog have its own CloseWatcher?
Not necessarily. Give the confirmation dialog an explicit close button and an accessible focus path to avoid recursion or grouped closure with the parent watcher. The parent calls close() after confirmation; closing the child only changes confirmation state.
How do you handle several open panels?
Define a stack or top-level ownership: one request closes only the topmost component that is actually closable, while the others remain open. Do not rely on implicit grouping of unactivated watchers as your business stack; record order and return focus in application state.
What is the fallback when CloseWatcher is unsupported?
Keep an explicit close button and basic Escape handling, reusing the same dirty-state confirmation, focus restoration, and cleanup functions. Do not globally intercept back gestures or history. Measure the degraded cohort before deciding whether to expand enhancement.