diff --git a/static/style.css b/static/style.css index 2fbd9ae78..ffe7ba248 100644 --- a/static/style.css +++ b/static/style.css @@ -3100,7 +3100,7 @@ .sidebar-nav .nav-tab svg{width:20px;height:20px;} .sidebar-nav .nav-tab.active{background:var(--accent-bg);} .sidebar-nav .nav-tab.active::before{left:-4px;top:50%;bottom:auto;transform:translateY(-50%);width:3px;height:16px;border-radius:0 2px 2px 0;} - .sidebar-nav .dashboard-link:not(.nav-action-visible){display:none!important;} + .sidebar-nav .dashboard-link:not(.nav-action-visible),.sidebar-nav [data-nav-action-mirror]:not(.nav-action-visible){display:none!important;} .sidebar .panel-view{height:100%;margin-left:52px;} .sidebar .panel-head{padding-right:52px;} .mobile-sidebar-close{display:inline-flex!important;position:absolute;right:4px;top:calc(4px + var(--app-titlebar-safe-top));width:44px;height:44px;z-index:4;background:var(--surface);border:1px solid var(--border);} diff --git a/static/ui.js b/static/ui.js index 84e066909..aa811da8e 100644 --- a/static/ui.js +++ b/static/ui.js @@ -1312,16 +1312,29 @@ function _syncNavActionMirrors(){ if(!sourceIds.has(mirror.getAttribute('data-nav-action-mirror')))mirror.remove(); }); sources.forEach(source=>{ - source.classList.add('nav-action-visible'); + const sourceVisible=(()=>{ + if(source.hidden||source.getAttribute('aria-hidden')==='true')return false; + if(source.classList.contains('nav-tab-hidden'))return false; + if(source.style&&(source.style.display==='none'||source.style.visibility==='hidden'))return false; + if(typeof window!=='undefined'&&typeof window.getComputedStyle==='function'){ + const computed=window.getComputedStyle(source); + if(computed&&(computed.display==='none'||computed.visibility==='hidden'))return false; + } + return true; + })(); let mirror=mirrors.find(el=>el.getAttribute('data-nav-action-mirror')===source.id); if(!mirror){ mirror=source.cloneNode(true); _stripInlineEventHandlers(mirror); mirror.id=source.id+'Mobile'; mirror.classList.remove('rail-btn'); - mirror.classList.add('nav-action-visible','has-tooltip--bottom'); + mirror.classList.add('has-tooltip--bottom'); mirror.setAttribute('data-nav-action-mirror',source.id); - mirror.addEventListener('click',e=>{e.preventDefault();if(mirror._navActionSource)mirror._navActionSource.click();}); + mirror.addEventListener('click',e=>{ + e.preventDefault(); + if(mirror._navActionSource)mirror._navActionSource.click(); + if(typeof closeMobileSidebar==='function')closeMobileSidebar(); + }); const anchor=sidebar.querySelector('.dashboard-link,[data-dashboard-link]')||sidebar.querySelector('[data-panel="logs"]'); sidebar.insertBefore(mirror,anchor||null); }else{ @@ -1329,6 +1342,7 @@ function _syncNavActionMirrors(){ _stripInlineEventHandlers(mirror); } mirror._navActionSource=source; + mirror.classList.toggle('nav-action-visible',sourceVisible); const label=source.getAttribute('data-tooltip')||source.getAttribute('aria-label')||''; if(label)mirror.setAttribute('data-label',label); }); @@ -1336,7 +1350,12 @@ function _syncNavActionMirrors(){ function _initNavActionMirrors(){ _syncNavActionMirrors(); const rail=document.querySelector('.rail'); - if(rail&&window.MutationObserver)new MutationObserver(_syncNavActionMirrors).observe(rail,{childList:true}); + if(rail&&window.MutationObserver)new MutationObserver(_syncNavActionMirrors).observe(rail,{ + childList:true, + subtree:true, + attributes:true, + attributeFilter:['class','style','hidden','aria-hidden','data-tooltip','aria-label'], + }); } if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',_initNavActionMirrors,{once:true}); else _initNavActionMirrors(); diff --git a/tests/test_dashboard_link_ui.py b/tests/test_dashboard_link_ui.py index e89bb81e7..8fc4c91c7 100644 --- a/tests/test_dashboard_link_ui.py +++ b/tests/test_dashboard_link_ui.py @@ -402,7 +402,8 @@ def test_mobile_dashboard_link_uses_shared_visible_action_class(): assert match is not None mobile_css = match.group(1) assert re.search(r"\.dashboard-link-visible,\s*\.nav-action-visible\{display:flex!important;\}", STYLE_CSS) - assert ".sidebar-nav .dashboard-link:not(.nav-action-visible){display:none!important;}" in mobile_css + assert ".sidebar-nav .dashboard-link:not(.nav-action-visible)" in mobile_css + assert ".sidebar-nav [data-nav-action-mirror]:not(.nav-action-visible){display:none!important;}" in mobile_css assert ".sidebar-nav .dashboard-link.dashboard-link-visible{display:none!important;}" not in mobile_css assert ".sidebar-nav .dashboard-link:not(.dashboard-link-visible){display:none!important;}" not in mobile_css @@ -445,6 +446,19 @@ def test_extension_rail_actions_are_mirrored_to_mobile_nav(): add: (...classes) => classes.forEach(c => this.classList._set.add(c)), remove: (...classes) => classes.forEach(c => this.classList._set.delete(c)), contains: c => this.classList._set.has(c), + toggle: (cls, force) => { + if (force === undefined) { + if (this.classList._set.has(cls)) { + this.classList._set.delete(cls); + return false; + } + this.classList._set.add(cls); + return true; + } + if (force) this.classList._set.add(cls); + else this.classList._set.delete(cls); + return !!force; + }, }; } appendChild(child) { child.parentNode = this; this.children.push(child); return child; } @@ -508,6 +522,7 @@ def test_extension_rail_actions_are_mirrored_to_mobile_nav(): const source = new El('button'); const dashboard = new El('button'); let clicked = 0; + let sidebarClosed = 0; source.id = 'hwxThemeCreatorRailBtn'; source.classList.add('rail-btn', 'nav-tab', 'has-tooltip'); source.setAttribute('data-tooltip', 'Theme Creator'); @@ -519,6 +534,7 @@ def test_extension_rail_actions_are_mirrored_to_mobile_nav(): sidebar.appendChild(dashboard); let observerCallback = null; let observedRail = null; + let observedOptions = null; global.document = { readyState: 'complete', @@ -528,10 +544,18 @@ def test_extension_rail_actions_are_mirrored_to_mobile_nav(): return null; }, }; - global.window = {}; + global.closeMobileSidebar = () => { sidebarClosed += 1; }; + global.window = { + getComputedStyle(el) { + return { + display: el.hidden || (el.style && el.style.display === 'none') ? 'none' : '', + visibility: (el.style && el.style.visibility) || '', + }; + }, + }; global.MutationObserver = window.MutationObserver = class { constructor(callback) { observerCallback = callback; } - observe(target) { observedRail = target; } + observe(target, options) { observedRail = target; observedOptions = options; } }; const helpers = Function(extractFn('_stripInlineEventHandlers') + '\\n' + extractFn('_syncNavActionMirrors') + '\\n' + extractFn('_initNavActionMirrors') + '; return { _initNavActionMirrors };')(); @@ -539,14 +563,22 @@ def test_extension_rail_actions_are_mirrored_to_mobile_nav(): rail.appendChild(source); observerCallback(); const mirror = sidebar.children.find(el => el.getAttribute('data-nav-action-mirror') === 'hwxThemeCreatorRailBtn'); + source.hidden = true; + observerCallback(); + const mirrorHiddenWhenSourceHidden = !mirror.classList.contains('nav-action-visible'); + source.hidden = false; + observerCallback(); mirror.click(); const mirrorBeforeDashboard = sidebar.children[0] === mirror; source.remove(); observerCallback(); console.log(JSON.stringify({ observerArmed: observedRail === rail, + observerAttributes: !!(observedOptions && observedOptions.attributes), + observerSubtree: !!(observedOptions && observedOptions.subtree), sourceVisible: source.classList.contains('nav-action-visible'), mirrorVisible: mirror.classList.contains('nav-action-visible'), + mirrorHiddenWhenSourceHidden, mirrorRailClass: mirror.classList.contains('rail-btn'), mirrorLabel: mirror.getAttribute('data-label'), mirrorOnclickAttribute: mirror.getAttribute('onclick'), @@ -554,6 +586,7 @@ def test_extension_rail_actions_are_mirrored_to_mobile_nav(): mirrorBeforeDashboard, mirrorRemoved: !sidebar.children.some(el => el.getAttribute('data-nav-action-mirror') === 'hwxThemeCreatorRailBtn'), clicked, + sidebarClosed, })); """ ) @@ -567,8 +600,11 @@ def test_extension_rail_actions_are_mirrored_to_mobile_nav(): pathlib.Path(path).unlink(missing_ok=True) assert out == { "observerArmed": True, - "sourceVisible": True, + "observerAttributes": True, + "observerSubtree": True, + "sourceVisible": False, "mirrorVisible": True, + "mirrorHiddenWhenSourceHidden": True, "mirrorRailClass": False, "mirrorLabel": "Theme Creator", "mirrorOnclickAttribute": None, @@ -576,6 +612,7 @@ def test_extension_rail_actions_are_mirrored_to_mobile_nav(): "mirrorBeforeDashboard": True, "mirrorRemoved": True, "clicked": 1, + "sidebarClosed": 1, }