Skip to content

Commit 436f9bd

Browse files
committed
frontend/aria/hotkey: toggle side chat with 0
1 parent 41a841e commit 436f9bd

File tree

22 files changed

+135
-40
lines changed

22 files changed

+135
-40
lines changed

src/.claude/settings.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,13 @@
6161
"mcp__github__get_pull_request_files",
6262
"mcp__github__get_pull_request_status",
6363
"mcp__github__list_workflow_runs",
64-
"mcp__github__list_workflows"
65-
"mcp__chrome-devtools__*"
64+
"mcp__github__list_workflows",
65+
"mcp__chrome-devtools__new_page",
66+
"mcp__chrome-devtools__wait_for",
67+
"mcp__chrome-devtools__navigate_page",
68+
"mcp__chrome-devtools__list_pages",
69+
"mcp__chrome-devtools__click",
70+
"mcp__chrome-devtools__press_key"
6671
],
6772
"deny": []
6873
}

src/dev/ARIA.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ These phases outline areas that need further accessibility work. Explore in deta
248248

249249
**Phase 24 ✅**: Editor content landmark navigation. Full keyboard navigation (Tab/Shift+Tab) between split editor frames. Content landmark focusable via Alt+Shift+M, Return key enters editor. Design system color feedback.
250250

251-
**Phase 25 ✅**: Hotkey quick navigation dialog. Global hotkey (Shift+Shift, Alt+Shift+H, Alt+Shift+Space) with hierarchical tree showing frames, files, pages, account. Multi-term search with visual highlighting. Full keyboard navigation support.
251+
**Phase 25 ✅**: Hotkey quick navigation dialog. Global hotkey (Shift+Shift, Alt+Shift+H, Alt+Shift+Space) with hierarchical tree showing frames, files, pages, account. Multi-term search with visual highlighting. Full keyboard navigation support. Numeric shortcuts: Key 0 toggles side chat, Keys 1–9 focus frames.
252252

253253
---
254254

@@ -257,13 +257,15 @@ These phases outline areas that need further accessibility work. Explore in deta
257257
### Application Entry Points
258258

259259
**Flow:**
260+
260261
1. **`packages/static/src/app.html`** - Minimal HTML template with empty `<head>` and container divs
261262
2. **`packages/static/src/webapp-cocalc.ts`** - Entry point that calls `init()`
262263
3. **`packages/frontend/entry-point.ts`** - Initializes Redux, stores, and all app subsystems
263264
4. **`packages/frontend/app/render.tsx`** - Mounts React app to `#cocalc-webapp-container`
264265
5. **`packages/frontend/app/page.tsx`** - Main App component with navigation, content layout
265266

266267
### Current Structure
268+
267269
- **static** package: Builds static assets (webpack) for the SPA
268270
- **frontend** package: React components, Redux state, app logic
269271
- **app.html**: Base template (extremely minimal - needs enhancement)
@@ -272,6 +274,7 @@ These phases outline areas that need further accessibility work. Explore in deta
272274
### WCAG AA Improvements Needed
273275

274276
#### 1. HTML Root & Head Elements (`app.html` & `meta.tsx`)
277+
275278
- [x] ✅ Add `lang` attribute to `<html>` for screen reader language detection - **Fixed in `packages/frontend/app/localize.tsx`** (dynamically set from i18n locale)
276279
- [x] ✅ Remove `user-scalable=no` from viewport meta tag (WCAG AA: low vision users must be able to zoom) - **Fixed in `packages/static/src/meta.tsx`**
277280
- [x] ✅ Add `<title>` tag (can be updated dynamically via React) - **Already implemented in `packages/frontend/browser.ts`** with `set_window_title()` function called throughout app navigation
@@ -280,41 +283,48 @@ These phases outline areas that need further accessibility work. Explore in deta
280283
- [ ] Add **skip links** for keyboard navigation (skip to main content, skip nav)
281284

282285
#### 2. Document Structure
286+
283287
- [ ] Ensure React app renders proper semantic HTML structure
284288
- [ ] Root `<main>` landmark for primary content (✅ partially done in page.tsx)
285289
- [ ] `<nav>` for top navigation (✅ done in page.tsx)
286290
- [ ] `<aside>` for sidebars (need to verify)
287291
- [ ] Dynamic page `<title>` based on context (projects, files, pages)
288292

289293
#### 3. Focus Management & Keyboard
294+
290295
- [ ] Skip to main content link (functional, keyboard-accessible)
291296
- [ ] Focus visible styles for keyboard users (`:focus-visible`)
292297
- [ ] Focus trap for modals (ensure focus doesn't escape)
293298
- [ ] Tab order validation (logical flow through page)
294299
- [ ] Return key handling for interactive elements
295300

296301
#### 4. Color & Contrast
302+
297303
- [ ] Verify WCAG AA contrast ratios (4.5:1 for normal text, 3:1 for large text)
298304
- [ ] Test with color blindness simulators
299305
- [ ] Ensure no information conveyed by color alone
300306

301307
#### 5. Images & Icons
308+
302309
- [ ] All decorative images: `aria-hidden="true"` or empty `alt=""`
303310
- [ ] Functional images: meaningful `alt` text
304311
- [ ] Icon-only buttons: `aria-label` (✅ mostly done)
305312

306313
#### 6. Forms & Inputs
314+
307315
- [ ] All `<input>` elements have associated `<label>` or `aria-label`
308316
- [ ] Required fields marked with `aria-required="true"`
309317
- [ ] Error messages linked via `aria-describedby`
310318
- [ ] Form validation messages announced to screen readers
311319

312320
#### 7. Headings & Structure
321+
313322
- [ ] Proper heading hierarchy (h1 → h2 → h3, no skips)
314323
- [ ] Meaningful heading text (not "Click here", "More")
315324
- [ ] One h1 per page (main topic/title)
316325

317326
#### 8. Alerts & Notifications
327+
318328
- [ ] Success/error messages: `role="alert"` with `aria-live="assertive"`
319329
- [ ] Info messages: `aria-live="polite"`
320330
- [ ] Notification timeout announcements
@@ -340,19 +350,22 @@ These phases outline areas that need further accessibility work. Explore in deta
340350
### Implementation Priority
341351

342352
**High Priority** (impacts many users):
353+
343354
- HTML lang attribute and meta tags
344355
- Skip links
345356
- Color contrast fixes
346357
- Form label associations
347358
- Heading hierarchy
348359

349360
**Medium Priority** (improves usability):
361+
350362
- Focus visible styles
351363
- Modal focus traps
352364
- Dynamic page titles
353365
- Confirmation dialogs
354366

355367
**Low Priority** (nice to have):
368+
356369
- Advanced ARIA patterns
357370
- Internationalization meta tags
358371
- Schema.org microdata
@@ -507,17 +520,20 @@ jq '.audits["aria-required-parent"].details.failed[].node.selector' dev/localhos
507520
**Final Fixes Applied:**
508521

509522
**label-content-name-mismatch** (3 items) ✅ **FIXED**
523+
510524
- Removed duplicate aria-labels from Switch components (kept only visible text)
511525
- Removed mismatched aria-label from Create button (visible text is sufficient)
512526
- Switches now use checkedChildren/unCheckedChildren for visible text only
513527
- Create button uses visible text without aria-label override
514528

515529
**aria-required-children** (2 items) ✅ **FIXED**
530+
516531
- Added `role="tab"` to SortableTab wrapper in `packages/frontend/components/sortable-tabs.tsx` (line 154)
517532
- This overrides the `role="button"` that dnd-kit attributes add via spread operator
518533
- Now tablist only has role="tab" children (both wrapper and inner Ant Design tab)
519534

520535
**aria-required-parent** (4 items) 🔄 **PENDING VERIFICATION**
536+
521537
- May be auto-fixed by the SortableTab role="tab" change
522538
- Need to run Lighthouse again to verify
523539

@@ -526,6 +542,7 @@ jq '.audits["aria-required-parent"].details.failed[].node.selector' dev/localhos
526542
**Report**: localhost_5000-20251113T152932.json
527543

528544
After all fixes:
545+
529546
- ✅ aria-command-name: PASS
530547
- ✅ image-alt: PASS
531548
- ✅ link-name: PASS
@@ -541,6 +558,7 @@ After all fixes:
541558
**Root Cause**: The actual Ant Design tab elements (`div role="tab"`) are nested too deeply inside the tablist wrapper due to how renderTabBar wraps each tab in SortableTab.
542559

543560
**Structure Problem**:
561+
544562
```
545563
SortableTabs role="tablist"
546564
└─ Ant Design Tabs component
@@ -562,7 +580,7 @@ SortableTabs role="tablist"
562580
### Remaining Work
563581

564582
**Immediate**:
583+
565584
1. Decide on approach to fix aria-required-parent/children (requires deeper restructuring)
566585

567-
**Future**:
568-
2. **DEFER**: color-contrast (8+ items) - plan custom antd theme with accessibility option
586+
**Future**: 2. **DEFER**: color-contrast (8+ items) - plan custom antd theme with accessibility option

src/packages/frontend/app/hotkey/dialog.tsx

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import { Input, Modal, Tree } from "antd";
1010
import React, { useEffect, useMemo, useRef, useState } from "react";
1111
import { FormattedMessage, useIntl } from "react-intl";
1212

13-
import { redux } from "@cocalc/frontend/app-framework";
13+
import { useTypedRedux } from "@cocalc/frontend/app-framework";
1414
import { Icon, Paragraph } from "@cocalc/frontend/components";
15+
import { toggleChat } from "@cocalc/frontend/chat/chat-indicator";
1516
import { getSideChatActions } from "@cocalc/frontend/frame-editors/generic/chat";
1617
import {
1718
get_local_storage,
@@ -266,6 +267,11 @@ export const QuickNavigationDialog: React.FC<QuickNavigationDialogProps> = ({
266267
const treeData = useEnhancedNavigationTreeData(!visible);
267268
const { frameTreeStructure, activeFrames, activeFileName, activeProjectId } =
268269
useActiveFrameData(!visible);
270+
// Get open_files state to check chat state
271+
const open_files = useTypedRedux(
272+
{ project_id: activeProjectId ?? "" },
273+
"open_files",
274+
);
269275
const searchInputRef = useRef<any>(null);
270276
const treeContainerRef = useRef<HTMLDivElement>(null);
271277
const [searchValue, setSearchValue] = useState("");
@@ -585,12 +591,27 @@ export const QuickNavigationDialog: React.FC<QuickNavigationDialogProps> = ({
585591
// Number shortcuts (0-9) - always available to jump to frames
586592
const num = parseInt(e.key, 10);
587593
if (!isNaN(num) && num >= 0 && num <= 9) {
588-
// Special handling for chat frame (key 0)
594+
// Special handling for chat frame (key 0) - toggle chat
589595
if (num === 0) {
590596
if (activeFileName && activeProjectId) {
591-
const projectActions = redux.getProjectActions(activeProjectId);
592-
projectActions?.open_chat({ path: activeFileName });
593-
focusChatInputWithRetry(activeProjectId, activeFileName, 0, true);
597+
// Check if chat is already open for this file
598+
const fileInfo = open_files?.get(activeFileName);
599+
const chatState = fileInfo?.get("chatState");
600+
const isChatOpen = !!chatState; // truthy chatState means chat is open
601+
602+
if (DEBUG) {
603+
console.log("Chat toggle key 0 pressed:", {
604+
activeFileName,
605+
activeProjectId,
606+
fileInfo: fileInfo?.toJS?.() || fileInfo,
607+
chatState,
608+
isChatOpen,
609+
});
610+
}
611+
612+
// Toggle chat using shared function
613+
toggleChat(activeProjectId, activeFileName, chatState, "hotkey-0");
614+
594615
onClose();
595616
e.preventDefault();
596617
return;
@@ -779,7 +800,6 @@ export const QuickNavigationDialog: React.FC<QuickNavigationDialogProps> = ({
779800
onChange={handleSearch}
780801
value={searchValue}
781802
autoFocus
782-
onKeyDown={handleKeyDown}
783803
allowClear
784804
style={{ marginBottom: 16 }}
785805
/>
@@ -867,7 +887,7 @@ export const QuickNavigationDialog: React.FC<QuickNavigationDialogProps> = ({
867887
<Paragraph type="secondary" style={{ marginTop: 16, marginBottom: 0 }}>
868888
<FormattedMessage
869889
id="app.hotkey.dialog.help_text"
870-
defaultMessage="Click frames above • Key 0 shows/focuses chat • Keys 1–9 focus frames • Type to search • ↑↓ navigate • Return to open • ESC to close"
890+
defaultMessage="Click frames above • Key 0 toggles chat • Keys 1–9 focus frames • Type to search • ↑↓ navigate • Return to open • ESC to close"
871891
description="Help text showing keyboard shortcuts in the quick navigation dialog"
872892
/>
873893
</Paragraph>

src/packages/frontend/chat/chat-indicator.tsx

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,29 @@ export type ChatState =
2727
| "external" // chat is open and managed externally (e.g., legacy sage worksheet)
2828
| "pending"; // chat should be opened when the file itself is actually initialized.
2929

30+
/**
31+
* Toggle chat for a file - close if open, open if closed
32+
* @param projectId - Project ID
33+
* @param path - File path
34+
* @param chatState - Current chat state
35+
* @param how - How the toggle was triggered (for tracking)
36+
*/
37+
export function toggleChat(
38+
projectId: string,
39+
path: string,
40+
chatState: ChatState,
41+
how: string = "chat-button",
42+
): void {
43+
const actions = redux.getProjectActions(projectId);
44+
if (chatState) {
45+
track("close-chat", { project_id: projectId, path, how });
46+
actions.close_chat({ path });
47+
} else {
48+
track("open-chat", { project_id: projectId, path, how });
49+
actions.open_chat({ path });
50+
}
51+
}
52+
3053
const CHAT_INDICATOR_STYLE: React.CSSProperties = {
3154
fontSize: "15pt",
3255
paddingTop: "2px",
@@ -67,16 +90,9 @@ export function ChatIndicator({ project_id, path, chatState }: Props) {
6790
function ChatButton({ project_id, path, chatState }) {
6891
const intl = useIntl();
6992

70-
const toggleChat = debounce(
93+
const handleToggleChat = debounce(
7194
() => {
72-
const actions = redux.getProjectActions(project_id);
73-
if (chatState) {
74-
track("close-chat", { project_id, path, how: "chat-button" });
75-
actions.close_chat({ path });
76-
} else {
77-
track("open-chat", { project_id, path, how: "chat-button" });
78-
actions.open_chat({ path });
79-
}
95+
toggleChat(project_id, path, chatState, "chat-button");
8096
},
8197
1000,
8298
{ leading: true },
@@ -112,7 +128,7 @@ function ChatButton({ project_id, path, chatState }) {
112128
type="text"
113129
danger={isNewChat}
114130
className={isNewChat ? "smc-chat-notification" : undefined}
115-
onClick={toggleChat}
131+
onClick={handleToggleChat}
116132
style={{ background: chatState ? "white" : undefined }}
117133
>
118134
<Icon name="comment" />

src/packages/frontend/i18n/trans/ar_EG.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@
156156
"ai-generate-document.modal.title": "إنشاء مستند {docName} باستخدام الذكاء الاصطناعي",
157157
"ai-generator.select_llm": "اختر نموذج اللغة",
158158
"app.fullscreen-button.tooltip": "وضع الشاشة الكاملة، يركز على المستند أو الصفحة الحالية.",
159-
"app.hotkey.dialog.help_text": "انقر فوق الإطارات أعلاه • المفتاح 0 يُظهر/يركز على الدردشة • المفاتيح 19 تركز على الإطارات • اكتب للبحث • ↑↓ للتنقل • Enter للفتح • ESC للإغلاق",
159+
"app.hotkey.dialog.help_text": "انقر على الإطارات أعلاه • المفتاح 0 يبدل الدردشة • المفاتيح 1-9 تركز على الإطارات • اكتب للبحث • ↑↓ للتنقل • عودة لفتح • ESC للإغلاق",
160160
"app.hotkey.dialog.search_placeholder": "البحث في الملفات والصفحات...",
161161
"app.hotkey.dialog.title": "التنقل السريع",
162162
"app.verify-email-banner.edit": "إذا كانت عنوان البريد الإلكتروني خاطئة، يرجى <E>تعديله</E> في إعدادات الحساب.",
@@ -1463,7 +1463,9 @@
14631463
"projects.table-controls.hashtags.placeholder": "التصفية حسب الوسوم...",
14641464
"projects.table-controls.hidden.label": "مخفي",
14651465
"projects.table-controls.search.placeholder": "ابحث عن المشاريع...",
1466+
"projects.table.keyboard-row-hint": "المشروع {title}. استخدم الأسهم لأعلى ولأسفل للتحرك؛ اضغط على Enter أو Space للفتح.",
14661467
"projects.table.last-edited": "آخر تعديل",
1468+
"projects.table.untitled": "بدون عنوان",
14671469
"purchases.automatic-payments-warning.description": "المدفوعات التلقائية هي <b>أكثر ملاءمة</b> بكثير، وست<b>وفر لك الوقت</b>، و<b>تضمن عدم إلغاء الاشتراكات</b> عن طريق الخطأ.",
14681470
"purchases.automatic-payments-warning.title": "الدفع التلقائي ليس مطلوبًا للاشتراك",
14691471
"purchases.automatic-payments.are-enabled": "المدفوعات التلقائية مفعلة",

src/packages/frontend/i18n/trans/de_DE.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@
156156
"ai-generate-document.modal.title": "Erzeuge ein {docName} Dokument mit KI",
157157
"ai-generator.select_llm": "Sprachmodell auswählen",
158158
"app.fullscreen-button.tooltip": "Vollbildmodus, fokussiert auf das aktuelle Dokument oder die aktuelle Seite",
159-
"app.hotkey.dialog.help_text": "Klicken Sie auf Rahmen oben • Taste 0 zeigt/fokussiert Chat • Tasten 1–9 fokussieren Rahmen • Tippen zum Suchen • ↑↓ navigieren • Eingabetaste zum Öffnen • ESC zum Schließen",
159+
"app.hotkey.dialog.help_text": "Klicke auf Rahmen oben • Taste 0 schaltet Chat um • Tasten 1–9 fokussieren Rahmen • Tippen, um zu suchen • ↑↓ navigieren • Eingabetaste zum Öffnen • ESC zum Schließen",
160160
"app.hotkey.dialog.search_placeholder": "Dateien und Seiten durchsuchen...",
161161
"app.hotkey.dialog.title": "Schnellnavigation",
162162
"app.verify-email-banner.edit": "Wenn die E-Mail-Adresse falsch ist, bitte <E>bearbeiten</E> Sie sie in den Kontoeinstellungen.",
@@ -1463,7 +1463,9 @@
14631463
"projects.table-controls.hashtags.placeholder": "Nach Hashtags filtern...",
14641464
"projects.table-controls.hidden.label": "Versteckt",
14651465
"projects.table-controls.search.placeholder": "Projekte durchsuchen...",
1466+
"projects.table.keyboard-row-hint": "Projekt {title}. Verwenden Sie die Pfeiltasten nach oben und unten, um sich zu bewegen; drücken Sie Enter oder Leertaste, um zu öffnen.",
14661467
"projects.table.last-edited": "Zuletzt bearbeitet",
1468+
"projects.table.untitled": "Unbenannt",
14671469
"purchases.automatic-payments-warning.description": "Automatische Zahlungen sind viel <b>bequemer</b>, <b>ersparen Ihnen Zeit</b> und <b>stellen sicher, dass Abonnements nicht versehentlich gekündigt werden</b>.",
14681470
"purchases.automatic-payments-warning.title": "Automatische Zahlungen sind NICHT erforderlich, um ein Abonnement zu haben",
14691471
"purchases.automatic-payments.are-enabled": "Automatische Zahlungen sind aktiviert",

src/packages/frontend/i18n/trans/es_ES.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@
156156
"ai-generate-document.modal.title": "Generar un documento {docName} usando IA",
157157
"ai-generator.select_llm": "Seleccionar modelo de idioma",
158158
"app.fullscreen-button.tooltip": "Modo de pantalla completa, centrado en el documento o página actual.",
159-
"app.hotkey.dialog.help_text": "Haz clic en los marcos de arriba • La tecla 0 muestra/enfoca el chat • Las teclas 1–9 enfocan los marcos • Escribe para buscar • ↑↓ navega • Intro para abrir • ESC para cerrar",
159+
"app.hotkey.dialog.help_text": "Haz clic en los marcos de arriba • La tecla 0 alterna el chat • Las teclas 1–9 enfocan los marcos • Escribe para buscar • ↑↓ navega • Return para abrir • ESC para cerrar",
160160
"app.hotkey.dialog.search_placeholder": "Buscar archivos y páginas...",
161161
"app.hotkey.dialog.title": "Navegación Rápida",
162162
"app.verify-email-banner.edit": "Si la dirección de correo electrónico es incorrecta, por favor <E>edítala</E> en la configuración de la cuenta.",
@@ -1463,7 +1463,9 @@
14631463
"projects.table-controls.hashtags.placeholder": "Filtrar por hashtags...",
14641464
"projects.table-controls.hidden.label": "Oculto",
14651465
"projects.table-controls.search.placeholder": "Buscar proyectos...",
1466+
"projects.table.keyboard-row-hint": "Proyecto {title}. Usa las flechas Arriba y Abajo para moverte; presiona Enter o Espacio para abrir.",
14661467
"projects.table.last-edited": "Última edición",
1468+
"projects.table.untitled": "Sin título",
14671469
"purchases.automatic-payments-warning.description": "Los pagos automáticos son mucho <b>más convenientes</b>, te <b>ahorrarán tiempo</b> y <b>asegurarán que las suscripciones no se cancelen</b> por accidente.",
14681470
"purchases.automatic-payments-warning.title": "Los pagos automáticos NO son necesarios para tener una suscripción",
14691471
"purchases.automatic-payments.are-enabled": "Los pagos automáticos están habilitados",

0 commit comments

Comments
 (0)