From 084c0b573277c14a5501f82a39a643e027145f72 Mon Sep 17 00:00:00 2001 From: timmy Date: Mon, 24 Aug 2026 13:49:48 +0000 Subject: [PATCH] Fix mobile overlay inert ownership (#1352) --- frontend/mobile-insights.js | 16 +++++++++++++++- tests/test_mobile_insights.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/frontend/mobile-insights.js b/frontend/mobile-insights.js index fe1ab13..0fb8e59 100644 --- a/frontend/mobile-insights.js +++ b/frontend/mobile-insights.js @@ -8,12 +8,25 @@ let active = false; let previous = '#/my-work'; let retries = 0; + let backgroundInert = null; function attr(element, name, value) { if (value === null) element.removeAttribute(name); else element.setAttribute(name, value); } + function containBackground() { + if (backgroundInert) return; + backgroundInert = new Map(o.backgrounds.map(element => [element, element.inert])); + backgroundInert.forEach((_wasInert, element) => { element.inert = true; }); + } + + function releaseBackground() { + if (!backgroundInert) return; + backgroundInert.forEach((wasInert, element) => { element.inert = wasInert; }); + backgroundInert = null; + } + function render(open, focus = false) { const wasActive = active; active = Boolean(open && o.mediaQuery.matches); @@ -28,7 +41,8 @@ attr(o.root, 'aria-modal', active ? 'true' : null); attr(o.root, 'aria-hidden', active || !o.mediaQuery.matches ? null : 'true'); o.root.inert = !active && o.mediaQuery.matches; - o.backgrounds.forEach(element => { element.inert = active; }); + if (active) containBackground(); + else releaseBackground(); o.dock.hidden = active; attr(o.hud, 'data-overlay-hidden', active ? 'true' : null); if (active) o.closeButton.focus(); diff --git a/tests/test_mobile_insights.py b/tests/test_mobile_insights.py index e3377f9..fcefe17 100644 --- a/tests/test_mobile_insights.py +++ b/tests/test_mobile_insights.py @@ -133,6 +133,37 @@ process.stdout.write(JSON.stringify({{current:controller.current(),hash:location } +def test_late_inactive_route_event_does_not_release_background_owned_by_next_overlay(): + script = f""" +const createInsights = require({json.dumps(str(CONTROLLER))}); +const listeners = {{}}; +const element = () => ({{ + hidden:false, inert:false, attributes:{{}}, focuses:0, listeners:{{}}, + setAttribute(name,value) {{ this.attributes[name] = String(value); }}, + removeAttribute(name) {{ delete this.attributes[name]; }}, + addEventListener(name,fn) {{ this.listeners[name] = fn; }}, + focus() {{ this.focuses += 1; }}, +}}); +const root=element(), launcher=element(), closeButton=element(), dock=element(), hud=element(), header=element(); +const location={{pathname:'/dashboard/', search:'', hash:'#/insights'}}; +const history={{state:null, pushState() {{}}, back() {{}}, replaceState(state,unused,url) {{ + this.state=state; location.hash=url.slice(url.indexOf('#')); +}}}}; +const controller=createInsights({{ + root,launcher,closeButton,dock,hud,backgrounds:[header],history,location, + eventTarget:{{addEventListener(name,fn) {{ listeners[name]=fn; }}}}, + mediaQuery:{{matches:true,addEventListener() {{}}}}, +}}); +controller.start(); +closeButton.listeners.click(); +header.inert = true; +listeners.hashchange(); +process.stdout.write(JSON.stringify({{current:controller.current(), headerInert:header.inert}})); +""" + + assert run_node(script) == {"current": False, "headerInert": True} + + def test_escape_requests_back_navigation_only_while_insights_is_open(): script = f""" const createInsights=require({json.dumps(str(CONTROLLER))});