## Implementation Plan: Back Navigation Closes Overlays **Goal:** When any overlay (modal, lightbox, dialog, or context menu) is open, pressing the browser back button closes the overlay without navigating away from the page. ### Changes Required (4 files) #### Common pattern to add in each file: 1. **On overlay open** (`openModal`, `open()`, etc.) – after making the overlay visible, call: ```js window.history.pushState({ __overlay: true }, ''); ``` 2. **On overlay close** (any user-initiated close – click outside, close button, Escape) – before removing visibility, call: ```js window.history.back(); ``` Then remove the `visible` class. 3. **Add a `popstate` event listener** in the component’s constructor (or at the end of the module’s init) that checks: - Overlay is currently visible - `event.state` exists and has `__overlay: true` If both true: remove overlay visibility class, then call `window.history.back()` to undo the push. --- #### File-by-file details **`devplacepy/static/js/ModalManager.js`** - In `openModal` (around line 28): add `history.pushState({__overlay: true}, '')` after `modal.classList.add('visible')`. - In `closeModal` (line 37–44): before removing class, add `history.back()`. (If called from popstate, class is already removed – that’s safe.) - At the end of `initModals()`, add: ```js window.addEventListener('popstate', (event) => { const modal = document.querySelector('.modal-overlay.visible'); if (modal && event.state && event.state.__overlay) { modal.classList.remove('visible'); history.back(); } }); ``` **`devplacepy/static/js/components/AppLightbox.js`** - In `open()` (line 71–81): after `this.overlay.classList.add("visible")`, add `history.pushState({__overlay: true}, '')`. - In `close()` (line 83–88): before `this.overlay.classList.remove("visible")`, add `history.back()`. - In the constructor, bind and add the `popstate` listener: ```js this._onPopstate = (event) => { if (this.overlay.classList.contains("visible") && event.state?.__overlay) { this.overlay.classList.remove("visible"); history.back(); } }; window.addEventListener('popstate', this._onPopstate); ``` - Optionally: in `close()`, remove the listener if the component is destroyed (not required for initial fix). **`devplacepy/static/js/components/AppDialog.js`** - In `open(mode, options)` (line 87–117): after `this.overlay.classList.add("visible")`, add `history.pushState({__overlay: true}, '')`. - In `close(value)` (line 128–134): before `this.overlay.classList.remove("visible")`, add `history.back()`. - In constructor, add the same `popstate` listener pattern as AppLightbox, using `this.overlay`. **`devplacepy/static/js/components/AppContextMenu.js`** - In `open(x, y, items)` (line 92–110): after `this.menu.classList.add("visible")`, add `history.pushState({__overlay: true}, '')`. - In `close()` (line 112–114): before `this.menu.classList.remove("visible")`, add `history.back()`. - In constructor, add popstate listener using `this.menu`. --- ### Verification After implementation, run these steps manually and also the project’s automated checks: **Manual test on any page with overlays:** 1. Open a modal, lightbox, dialog, and context menu (one at a time). 2. Press browser back button → each overlay closes, page stays on the current page. 3. Press back again (if no other history entries) → normal browser back behavior. 4. Close each overlay via its regular mechanism (Escape, click outside) → no stray history entries; subsequent back works as expected. **Automated verification:** ```bash pip install -e '.[dev]' -q && make test-unit ruff check . ``` These commands must pass. --- ### Definition of Done - [ ] All four overlay components (`ModalManager.js`, `AppLightbox.js`, `AppDialog.js`, `AppContextMenu.js`) implement the history push/pop/popstate pattern as described above. - [ ] After each overlay opens, a new history state is pushed (`__overlay: true`). - [ ] Any user-initiated close (Escape, click outside, close button) calls `window.history.back()` before removing the visible class. - [ ] A `popstate` event listener is registered on `window` for each component; it detects the pushed state, closes the overlay, and calls `history.back()` – preventing navigation. - [ ] No infinite loops or double-back calls occur (interaction between popstate handler and close handlers is leak-proof). - [ ] `pip install -e '.[dev]' -q && make test-unit` passes with exit code 0. - [ ] `ruff check .` passes with exit code 0. - [ ] Manual tests (open/back/close via UI) succeed on current desktop browser.