Keep closed workspace dropdown out of SR navigation

This commit is contained in:
sheldon-im authored and t committed 2026-07-09 02:40:36 +00:00
1 parent 06e0df4656
commit 67f8414656
3 files changed
+85 -6

No files matched your search

+1 -1
View File
@@ -751,7 +751,7 @@
<button class="ctx-compress-btn composer-mobile-context-compress" id="composerMobileCtxCompressBtn" type="button" style="display:none"></button>
</div>
</div>
<div class="ws-dropdown ws-dropdown-footer" id="composerWsDropdown"></div>
<div class="ws-dropdown ws-dropdown-footer" id="composerWsDropdown" hidden aria-hidden="true" inert></div>
<div class="composer-reasoning-dropdown" id="composerReasoningDropdown">
<div class="reasoning-option" data-effort="none">None</div>
<div class="reasoning-option" data-effort="minimal">Minimal</div>
+50 -5
View File
@@ -5648,7 +5648,7 @@ function syncWorkspaceDisplays(){
const mobileAction=$('composerMobileWorkspaceAction');
const mobileLabel=$('composerMobileWorkspaceLabel');
const composerDropdown=$('composerWsDropdown');
if(!hasWorkspace && composerDropdown) composerDropdown.classList.remove('open');
if(!hasWorkspace && composerDropdown) _setWorkspaceDropdownOpenState(composerDropdown,false);
// Only show workspace label once boot has finished to prevent
// flash of "No workspace" before the saved session finishes loading.
if(composerLabel) composerLabel.textContent=S._bootReady?label:'';
@@ -5680,6 +5680,41 @@ async function loadWorkspaceList(){
}catch(e){ return {workspaces:[], last:''}; }
}
function _setWorkspaceDropdownOpenState(dd,open){
if(!dd)return;
dd.classList.toggle('open',!!open);
dd.hidden=!open;
dd.setAttribute('aria-hidden',open?'false':'true');
if(open){
try{dd.inert=false;}catch(_){}
dd.removeAttribute('inert');
}else{
try{dd.inert=true;}catch(_){}
dd.setAttribute('inert','');
}
}
function _getComposerWorkspaceFocusTarget(){
const panel=(typeof $==='function')?$('composerMobileConfigPanel'):null;
const mobileAction=(typeof $==='function')?$('composerMobileWorkspaceAction'):null;
if(panel&&panel.classList.contains('open')&&mobileAction&&!mobileAction.disabled) return mobileAction;
return (typeof $==='function')?$('composerWorkspaceChip'):null;
}
function _focusComposerWorkspaceTarget(target){
if(target&&!target.disabled&&typeof target.focus==='function'){
try{target.focus({preventScroll:true});}
catch(_){target.focus();}
}
}
function _shouldRestoreComposerWorkspaceFocus(dd){
if(typeof document==='undefined') return true;
const active=document.activeElement;
if(!active||active===document.body) return true;
return !!(dd&&dd.contains(active));
}
function _renderWorkspaceAction(label, meta, iconSvg, onClick){
const opt=document.createElement('div');
opt.className='ws-opt ws-opt-action';
@@ -5833,7 +5868,7 @@ function toggleWsDropdown(){
closeProfileDropdown(); // close profile dropdown if open
loadWorkspaceList().then(data=>{
renderWorkspaceDropdownInto(dd, data.workspaces, S.session?.workspace||S._profileDefaultWorkspace||data.last||'');
dd.classList.add('open');
_setWorkspaceDropdownOpenState(dd,true);
});
}
}
@@ -5853,7 +5888,7 @@ function toggleComposerWsDropdown(){
if(typeof closeReasoningDropdown==='function') closeReasoningDropdown();
loadWorkspaceList().then(data=>{
renderWorkspaceDropdownInto(dd, data.workspaces, S.session?.workspace||S._profileDefaultWorkspace||data.last||'');
dd.classList.add('open');
_setWorkspaceDropdownOpenState(dd,true);
_positionComposerWsDropdown();
if(chip){
chip.classList.add('active');
@@ -5872,8 +5907,8 @@ function closeWsDropdown(){
const composerDd=$('composerWsDropdown');
const composerChip=$('composerWorkspaceChip');
const mobileAction=$('composerMobileWorkspaceAction');
if(dd)dd.classList.remove('open');
if(composerDd)composerDd.classList.remove('open');
if(dd)_setWorkspaceDropdownOpenState(dd,false);
if(composerDd)_setWorkspaceDropdownOpenState(composerDd,false);
if(composerChip){
composerChip.classList.remove('active');
composerChip.setAttribute('aria-expanded','false');
@@ -6325,6 +6360,10 @@ async function switchToWorkspace(path,name){
if(typeof cancelEditMode==='function')cancelEditMode();
if(typeof clearPreview==='function')clearPreview();
}
const composerDd=(typeof $==='function')?$('composerWsDropdown'):null;
const restoreComposerFocusTarget=(composerDd&&composerDd.classList.contains('open')&&typeof _getComposerWorkspaceFocusTarget==='function')
? _getComposerWorkspaceFocusTarget()
: null;
try{
closeWsDropdown();
await api('/api/session/update',{method:'POST',body:JSON.stringify({
@@ -6336,6 +6375,12 @@ async function switchToWorkspace(path,name){
S._profileSwitchWorkspace=null;
S._pendingSessionToolsets=null;
syncTopbar();
if(
restoreComposerFocusTarget&&
typeof _shouldRestoreComposerWorkspaceFocus==='function'&&
_shouldRestoreComposerWorkspaceFocus(composerDd)&&
typeof _focusComposerWorkspaceTarget==='function'
) _focusComposerWorkspaceTarget(restoreComposerFocusTarget);
await loadDir('.');
if (_currentPanel === 'memory') await loadMemory(true);
showToast(t('workspace_switched_to',name||getWorkspaceFriendlyName(path)));
@@ -47,6 +47,21 @@ def test_composer_workspace_dropdown_keeps_aria_expanded_in_sync():
assert "mobileAction.setAttribute('aria-expanded','false')" in close
def test_closed_composer_workspace_dropdown_is_removed_from_screen_reader_navigation():
assert 'id="composerWsDropdown" hidden aria-hidden="true" inert' in INDEX_HTML
helper = _block(PANELS_JS, "function _setWorkspaceDropdownOpenState", "function _getComposerWorkspaceFocusTarget")
assert "dd.hidden=!open" in helper
assert "dd.setAttribute('aria-hidden',open?'false':'true')" in helper
assert "dd.removeAttribute('inert')" in helper
assert "dd.setAttribute('inert','')" in helper
toggle = _block(PANELS_JS, "function toggleComposerWsDropdown", "function closeWsDropdown")
close = _block(PANELS_JS, "function closeWsDropdown", "document.addEventListener('click'")
assert "_setWorkspaceDropdownOpenState(dd,true)" in toggle
assert "_setWorkspaceDropdownOpenState(composerDd,false)" in close
def test_composer_workspace_cue_is_transient_not_a_persistent_editor_description():
assert 'id="msg"' in INDEX_HTML
assert 'aria-describedby="composerWorkspaceContext"' not in INDEX_HTML
@@ -59,6 +74,25 @@ def test_composer_workspace_cue_is_transient_not_a_persistent_editor_description
assert "workspace_context_none" not in sync
def test_workspace_selection_from_composer_dropdown_restores_focus_to_opening_trigger_once():
focus_helpers = _block(PANELS_JS, "function _getComposerWorkspaceFocusTarget", "function _renderWorkspaceAction")
switch = _block(PANELS_JS, "async function switchToWorkspace", "// ── Profile panel")
assert "const mobileAction=(typeof $==='function')?$('composerMobileWorkspaceAction'):null" in focus_helpers
assert "if(panel&&panel.classList.contains('open')&&mobileAction&&!mobileAction.disabled) return mobileAction" in focus_helpers
assert "return (typeof $==='function')?$('composerWorkspaceChip'):null" in focus_helpers
assert "function _shouldRestoreComposerWorkspaceFocus(dd)" in focus_helpers
assert "return !!(dd&&dd.contains(active))" in focus_helpers
assert "const restoreComposerFocusTarget=(composerDd&&composerDd.classList.contains('open')&&typeof _getComposerWorkspaceFocusTarget==='function')" in switch
assert "_shouldRestoreComposerWorkspaceFocus(composerDd)" in switch
assert "_focusComposerWorkspaceTarget(restoreComposerFocusTarget)" in switch
assert switch.count("_focusComposerWorkspaceTarget(restoreComposerFocusTarget)") == 1
assert switch.index("syncTopbar();") < switch.index("_focusComposerWorkspaceTarget(restoreComposerFocusTarget)") < switch.index("await loadDir('.')")
assert "workspace_switched_to" in switch
assert switch.index("_focusComposerWorkspaceTarget(restoreComposerFocusTarget)") < switch.index("workspace_switched_to")
def test_new_chat_has_screen_reader_only_workspace_announcer():
assert 'id="a11yAnnouncer"' in INDEX_HTML
assert 'class="sr-only"' in INDEX_HTML