Improve workspace switcher screen reader state

This commit is contained in:
sheldon-im authored and t committed 2026-07-09 02:40:35 +00:00
1 parent d6e6e91de4
commit 23de53c004
4 files changed
+91 -8

No files matched your search

+15
View File
@@ -214,6 +214,7 @@ const LOCALES = {
model_group_configured: 'Configured',
ws_search_placeholder: 'Search workspaces…',
ws_no_results: 'No workspaces found',
workspace_switcher_aria: 'Switch workspace. Current workspace: {0}.',
workspace_new_worktree_conversation: 'New conversation in worktree',
workspace_new_worktree_conversation_meta: 'Create an isolated git worktree for this workspace.',
workspace_worktree_created: 'Worktree conversation created',
@@ -1913,6 +1914,7 @@ const LOCALES = {
model_group_configured: 'Configurato',
ws_search_placeholder: 'Cerca workspace…',
ws_no_results: 'Nessun workspace trovato',
workspace_switcher_aria: 'Cambia workspace. Workspace corrente: {0}.',
workspace_new_worktree_conversation: 'Nuova conversazione in worktree',
workspace_new_worktree_conversation_meta: 'Crea un worktree git isolato per questo workspace.',
workspace_worktree_created: 'Conversazione worktree creata',
@@ -3603,6 +3605,7 @@ const LOCALES = {
model_group_configured: '設定済み',
ws_search_placeholder: 'ワークスペースを検索…',
ws_no_results: 'ワークスペースが見つかりません',
workspace_switcher_aria: 'ワークスペースを切り替えます。現在のワークスペース: {0}。',
workspace_new_worktree_conversation: 'worktree で新しい会話',
workspace_new_worktree_conversation_meta: 'このワークスペース用に隔離された git worktree を作成します。',
workspace_worktree_created: 'worktree 会話を作成しました',
@@ -5387,6 +5390,7 @@ const LOCALES = {
model_group_configured: 'Настроенные',
ws_search_placeholder: 'Поиск рабочих пространств…',
ws_no_results: 'Рабочие пространства не найдены',
workspace_switcher_aria: 'Переключить рабочее пространство. Текущее рабочее пространство: {0}.',
workspace_new_worktree_conversation: 'Новый разговор в worktree',
workspace_new_worktree_conversation_meta: 'Создать изолированный git worktree для этого рабочего пространства.',
workspace_worktree_created: 'Разговор в worktree создан',
@@ -6904,6 +6908,7 @@ const LOCALES = {
model_group_configured: 'Configurados',
ws_search_placeholder: 'Buscar espacios de trabajo…',
ws_no_results: 'No se encontraron espacios de trabajo',
workspace_switcher_aria: 'Cambiar espacio de trabajo. Espacio de trabajo actual: {0}.',
workspace_new_worktree_conversation: 'Nueva conversación en worktree',
workspace_new_worktree_conversation_meta: 'Crear un git worktree aislado para este espacio de trabajo.',
workspace_worktree_created: 'Conversación en worktree creada',
@@ -9525,6 +9530,7 @@ const LOCALES = {
model_group_configured: 'Konfiguriert',
ws_search_placeholder: 'Arbeitsbereiche suchen…',
ws_no_results: 'Keine Arbeitsbereiche gefunden',
workspace_switcher_aria: 'Workspace wechseln. Aktueller Workspace: {0}.',
workspace_new_worktree_conversation: 'Neue Unterhaltung in Worktree',
workspace_new_worktree_conversation_meta: 'Erstellt einen isolierten git worktree für diesen Arbeitsbereich.',
workspace_worktree_created: 'Worktree-Unterhaltung erstellt',
@@ -10160,6 +10166,7 @@ const LOCALES = {
model_group_configured: '已配置',
ws_search_placeholder: '搜索工作区…',
ws_no_results: '未找到工作区',
workspace_switcher_aria: '切换工作区。当前工作区:{0}。',
workspace_new_worktree_conversation: '在 worktree 中新建对话',
workspace_new_worktree_conversation_meta: '为此工作区创建隔离的 git worktree。',
workspace_worktree_created: '已创建 worktree 对话',
@@ -11810,6 +11817,7 @@ const LOCALES = {
model_group_configured: '已設定',
ws_search_placeholder: '搜尋工作區…',
ws_no_results: '找不到工作區',
workspace_switcher_aria: '切換工作區。目前工作區:{0}。',
workspace_new_worktree_conversation: '在 worktree 中新增對話',
workspace_new_worktree_conversation_meta: '為此工作區建立隔離的 git worktree。',
workspace_worktree_created: '已建立 worktree 對話',
@@ -13436,6 +13444,7 @@ const LOCALES = {
model_group_configured: 'Configurados',
ws_search_placeholder: 'Buscar espaços de trabalho…',
ws_no_results: 'Nenhum espaço de trabalho encontrado',
workspace_switcher_aria: 'Alternar espaço de trabalho. Espaço de trabalho atual: {0}.',
workspace_new_worktree_conversation: 'Nova conversa em worktree',
workspace_new_worktree_conversation_meta: 'Cria um git worktree isolado para este espaço de trabalho.',
workspace_worktree_created: 'Conversa em worktree criada',
@@ -15002,6 +15011,7 @@ const LOCALES = {
model_group_configured: '구성됨',
ws_search_placeholder: '워크스페이스 검색…',
ws_no_results: '워크스페이스를 찾을 수 없습니다',
workspace_switcher_aria: '워크스페이스 전환. 현재 워크스페이스: {0}.',
workspace_new_worktree_conversation: 'worktree에서 새 대화',
workspace_new_worktree_conversation_meta: '이 워크스페이스용 격리된 git worktree를 만듭니다.',
workspace_worktree_created: 'worktree 대화가 생성되었습니다',
@@ -16685,6 +16695,7 @@ const LOCALES = {
model_group_configured: 'Configuré',
ws_search_placeholder: 'Rechercher des espaces de travail\u2026',
ws_no_results: 'Aucun espace de travail trouvé',
workspace_switcher_aria: 'Changer d’espace de travail. Espace de travail actuel : {0}.',
workspace_new_worktree_conversation: 'Nouvelle conversation dans un worktree',
workspace_new_worktree_conversation_meta: 'Créer un worktree git isolé pour cet espace de travail.',
workspace_worktree_created: 'Conversation Worktree créée',
@@ -19787,6 +19798,7 @@ const LOCALES = {
workspace_worktree_created: 'Worktree konverzace vytvořena',
workspace_worktree_failed: 'Vytvoření worktree selhalo: ',
ws_no_results: 'Nenalezeny žádné pracovní prostory',
workspace_switcher_aria: 'Přepnout pracovní prostor. Aktuální pracovní prostor: {0}.',
ws_search_placeholder: 'Hledat pracovní prostory…',
you: 'Vy',
// tool/worklog references
@@ -20013,6 +20025,7 @@ const LOCALES = {
model_group_configured: 'Yapılandırılmış',
ws_search_placeholder: 'Çalışma alanlarını arayın\u2026',
ws_no_results: 'Çalışma alanı bulunamadı',
workspace_switcher_aria: 'Çalışma alanını değiştir. Geçerli çalışma alanı: {0}.',
workspace_new_worktree_conversation: 'Worktree\'de yeni konuşma',
workspace_new_worktree_conversation_meta: 'Bu çalışma alanı için yalıtılmış bir git çalışma ağacı oluşturun.',
workspace_worktree_created: 'Worktree görüşmesi oluşturuldu',
@@ -21689,6 +21702,7 @@ const LOCALES = {
model_group_configured: 'Skonfigurowane',
ws_search_placeholder: 'Szukaj obszarów roboczych…',
ws_no_results: 'Nie znaleziono żadnych obszarów roboczych',
workspace_switcher_aria: 'Przełącz obszar roboczy. Bieżący obszar roboczy: {0}.',
workspace_new_worktree_conversation: 'Nowa konwersacja w worktree',
workspace_new_worktree_conversation_meta: 'Utwórz izolowane git worktree dla tego obszaru roboczego.',
workspace_worktree_created: 'Utworzono konwersację w worktree',
@@ -23351,6 +23365,7 @@ const LOCALES = {
model_group_configured: 'Đã cấu hình',
ws_search_placeholder: 'Tìm workspace…',
ws_no_results: 'Không tìm thấy workspace nào',
workspace_switcher_aria: 'Chuyển workspace. Workspace hiện tại: {0}.',
workspace_new_worktree_conversation: 'Cuộc trò chuyện mới trong worktree',
workspace_new_worktree_conversation_meta: 'Tạo git worktree độc lập cho workspace này.',
workspace_worktree_created: 'Đã tạo cuộc trò chuyện worktree',
+2 -2
View File
@@ -636,7 +636,7 @@
<button class="composer-workspace-files-btn" id="btnWorkspacePanelToggle" type="button" onclick="toggleWorkspacePanel()" title="Show workspace panel" aria-pressed="false" aria-label="Toggle workspace files panel">
<span class="composer-workspace-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></span>
</button>
<button class="composer-workspace-chip" id="composerWorkspaceChip" type="button" onclick="toggleComposerWsDropdown()" title="Switch workspace" disabled>
<button class="composer-workspace-chip" id="composerWorkspaceChip" type="button" onclick="toggleComposerWsDropdown()" title="Switch workspace" aria-label="Switch workspace" aria-haspopup="true" aria-expanded="false" aria-controls="composerWsDropdown" disabled>
<span class="composer-workspace-label" id="composerWorkspaceLabel"></span>
<span class="composer-workspace-chevron" aria-hidden="true"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
@@ -722,7 +722,7 @@
</button>
</div>
<div class="composer-mobile-config-panel" id="composerMobileConfigPanel" aria-label="Workspace, model, quota, reasoning, and context settings">
<button class="composer-mobile-config-action" id="composerMobileWorkspaceAction" type="button" onclick="toggleComposerWsDropdown()" title="Switch workspace">
<button class="composer-mobile-config-action" id="composerMobileWorkspaceAction" type="button" onclick="toggleComposerWsDropdown()" title="Switch workspace" aria-label="Switch workspace" aria-haspopup="true" aria-expanded="false" aria-controls="composerWsDropdown">
<span class="composer-workspace-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></span>
<span class="composer-mobile-config-copy"><span class="composer-mobile-config-kicker" data-i18n="composer_mobile_workspace">Workspace</span><span class="composer-mobile-config-value" id="composerMobileWorkspaceLabel"></span></span>
</button>
+23 -6
View File
@@ -5653,14 +5653,19 @@ function syncWorkspaceDisplays(){
// flash of "No workspace" before the saved session finishes loading.
if(composerLabel) composerLabel.textContent=S._bootReady?label:'';
if(mobileLabel) mobileLabel.textContent=S._bootReady?label:'';
const composerExpanded=!!(composerDropdown&&composerDropdown.classList.contains('open'));
if(composerChip){
composerChip.disabled=!hasWorkspace;
composerChip.title=hasWorkspace?ws:t('no_workspace');
composerChip.classList.toggle('active',!!(composerDropdown&&composerDropdown.classList.contains('open')));
composerChip.setAttribute('aria-label',hasWorkspace?t('workspace_switcher_aria',label):t('no_workspace'));
composerChip.setAttribute('aria-expanded',composerExpanded?'true':'false');
composerChip.classList.toggle('active',composerExpanded);
}
if(mobileAction){
mobileAction.title=hasWorkspace?ws:t('no_workspace');
mobileAction.classList.toggle('active',!!(composerDropdown&&composerDropdown.classList.contains('open')));
mobileAction.setAttribute('aria-label',hasWorkspace?t('workspace_switcher_aria',label):t('no_workspace'));
mobileAction.setAttribute('aria-expanded',composerExpanded?'true':'false');
mobileAction.classList.toggle('active',composerExpanded);
}
}
@@ -5850,8 +5855,14 @@ function toggleComposerWsDropdown(){
renderWorkspaceDropdownInto(dd, data.workspaces, S.session?.workspace||S._profileDefaultWorkspace||data.last||'');
dd.classList.add('open');
_positionComposerWsDropdown();
if(chip) chip.classList.add('active');
if(mobileAction) mobileAction.classList.add('active');
if(chip){
chip.classList.add('active');
chip.setAttribute('aria-expanded','true');
}
if(mobileAction){
mobileAction.classList.add('active');
mobileAction.setAttribute('aria-expanded','true');
}
});
}
}
@@ -5863,8 +5874,14 @@ function closeWsDropdown(){
const mobileAction=$('composerMobileWorkspaceAction');
if(dd)dd.classList.remove('open');
if(composerDd)composerDd.classList.remove('open');
if(composerChip)composerChip.classList.remove('active');
if(mobileAction)mobileAction.classList.remove('active');
if(composerChip){
composerChip.classList.remove('active');
composerChip.setAttribute('aria-expanded','false');
}
if(mobileAction){
mobileAction.classList.remove('active');
mobileAction.setAttribute('aria-expanded','false');
}
}
document.addEventListener('click',e=>{
if(
@@ -0,0 +1,51 @@
"""Issue #5671: workspace switcher and New Chat workspace announcements."""
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
INDEX_HTML = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
STYLE_CSS = (ROOT / "static" / "style.css").read_text(encoding="utf-8")
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
def _block(source: str, start_marker: str, end_marker: str) -> str:
start = source.find(start_marker)
assert start != -1, f"{start_marker!r} not found"
end = source.find(end_marker, start)
assert end != -1, f"{end_marker!r} not found after {start_marker!r}"
return source[start:end]
def test_composer_workspace_switchers_expose_action_and_popup_state():
assert 'id="composerWorkspaceChip"' in INDEX_HTML
assert 'id="composerMobileWorkspaceAction"' in INDEX_HTML
assert 'aria-label="Switch workspace"' in INDEX_HTML
assert 'aria-haspopup="true"' in INDEX_HTML
assert 'aria-expanded="false"' in INDEX_HTML
assert 'aria-controls="composerWsDropdown"' in INDEX_HTML
assert INDEX_HTML.count('aria-controls="composerWsDropdown"') == 2
sync = _block(PANELS_JS, "function syncWorkspaceDisplays", "async function loadWorkspaceList")
assert "const composerExpanded=!!(composerDropdown&&composerDropdown.classList.contains('open'))" in sync
assert "composerChip.setAttribute('aria-label',hasWorkspace?t('workspace_switcher_aria',label):t('no_workspace'))" in sync
assert "composerChip.setAttribute('aria-expanded',composerExpanded?'true':'false')" in sync
assert "composerChip.classList.toggle('active',composerExpanded)" in sync
assert "mobileAction.setAttribute('aria-label',hasWorkspace?t('workspace_switcher_aria',label):t('no_workspace'))" in sync
assert "mobileAction.setAttribute('aria-expanded',composerExpanded?'true':'false')" in sync
assert "mobileAction.classList.toggle('active',composerExpanded)" in sync
def test_composer_workspace_dropdown_keeps_aria_expanded_in_sync():
toggle = _block(PANELS_JS, "function toggleComposerWsDropdown", "function closeWsDropdown")
close = _block(PANELS_JS, "function closeWsDropdown", "document.addEventListener('click'")
assert "chip.setAttribute('aria-expanded','true')" in toggle
assert "mobileAction.setAttribute('aria-expanded','true')" in toggle
assert "composerChip.setAttribute('aria-expanded','false')" in close
assert "mobileAction.setAttribute('aria-expanded','false')" in close
def test_workspace_switcher_i18n_key_exists_in_english_locale():
assert "workspace_switcher_aria: 'Switch workspace. Current workspace: {0}.'" in I18N_JS