@@ -256,209 +256,96 @@ These phases outline areas that need further accessibility work. Explore in deta
256256
257257### Application Entry Points
258258
259- ** Flow: **
259+ CoCalc is a single-page application with this startup flow:
260260
261- 1 . ** ` packages/static/src/app.html ` ** - Minimal HTML template with empty ` <head> ` and container divs
262- 2 . ** ` packages/static/src/webapp-cocalc.ts ` ** - Entry point that calls ` init() `
263- 3 . ** ` packages/frontend/entry-point.ts ` ** - Initializes Redux, stores, and all app subsystems
261+ 1 . ** ` packages/static/src/app.html ` ** - Base HTML template with React container div
262+ 2 . ** ` packages/static/src/webapp-cocalc.ts ` ** - Entry point that initializes the app
263+ 3 . ** ` packages/frontend/entry-point.ts ` ** - Initializes Redux stores and app subsystems
2642644 . ** ` packages/frontend/app/render.tsx ` ** - Mounts React app to ` #cocalc-webapp-container `
265- 5 . ** ` packages/frontend/app/page.tsx ` ** - Main App component with navigation, content layout
265+ 5 . ** ` packages/frontend/app/page.tsx ` ** - Main App component with navigation and layout
266266
267- ### Current Structure
267+ Key files implementing WCAG AA compliance:
268268
269- - ** static ** package: Builds static assets (webpack) for the SPA
270- - ** frontend** package: React components, Redux state, app logic
271- - ** app.html ** : Base template (extremely minimal - needs enhancement)
272- - ** Entry ** : Uses React 18 ` createRoot() ` for client-side rendering
269+ - ` packages/frontend/app/localize.tsx ` - Dynamic ` lang ` attribute on ` <html> `
270+ - ` packages/ frontend/browser.ts ` - ` set_window_title() ` and ` set_meta_description() ` functions
271+ - ` packages/static/src/meta.tsx ` - Viewport and meta tags
272+ - ` packages/frontend/customize.tsx ` - Page description from customization settings
273273
274- ### WCAG AA Improvements Needed
274+ ### Running Lighthouse Accessibility Audits
275275
276- #### 1. HTML Root & Head Elements ( ` app.html ` & ` meta.tsx ` )
276+ ** In Chrome DevTools: **
277277
278- - [x] ✅ Add ` lang ` attribute to ` <html> ` for screen reader language detection - ** Fixed in ` packages/frontend/app/localize.tsx ` ** (dynamically set from i18n locale)
279- - [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 ` **
280- - [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
281- - [x] ✅ Add ` <meta name="description"> ` for page description - ** Fixed in ` packages/frontend/browser.ts ` ** (added ` set_meta_description() ` function) and ** ` packages/frontend/customize.tsx ` ** (called on customize init with format ` {site_name}: {site_description} ` )
282- - [ ] Link favicon and apple-touch-icon
283- - [ ] Add ** skip links** for keyboard navigation (skip to main content, skip nav)
278+ 1 . Open your local CoCalc instance (e.g., ` http://localhost:5000 ` )
279+ 2 . Open DevTools (F12)
280+ 3 . Go to ** Lighthouse** tab
281+ 4 . Select ** Desktop** device
282+ 5 . Select only ** Accessibility** (uncheck Performance, Best Practices, SEO)
283+ 6 . Click ** Analyze page load**
284+ 7 . Save the report as JSON: click menu → ** Save as JSON**
284285
285- #### 2. Document Structure
286-
287- - [ ] Ensure React app renders proper semantic HTML structure
288- - [ ] Root ` <main> ` landmark for primary content (✅ partially done in page.tsx)
289- - [ ] ` <nav> ` for top navigation (✅ done in page.tsx)
290- - [ ] ` <aside> ` for sidebars (need to verify)
291- - [ ] Dynamic page ` <title> ` based on context (projects, files, pages)
292-
293- #### 3. Focus Management & Keyboard
294-
295- - [ ] Skip to main content link (functional, keyboard-accessible)
296- - [ ] Focus visible styles for keyboard users (` :focus-visible ` )
297- - [ ] Focus trap for modals (ensure focus doesn't escape)
298- - [ ] Tab order validation (logical flow through page)
299- - [ ] Return key handling for interactive elements
300-
301- #### 4. Color & Contrast
302-
303- - [ ] Verify WCAG AA contrast ratios (4.5:1 for normal text, 3:1 for large text)
304- - [ ] Test with color blindness simulators
305- - [ ] Ensure no information conveyed by color alone
306-
307- #### 5. Images & Icons
308-
309- - [ ] All decorative images: ` aria-hidden="true" ` or empty ` alt="" `
310- - [ ] Functional images: meaningful ` alt ` text
311- - [ ] Icon-only buttons: ` aria-label ` (✅ mostly done)
312-
313- #### 6. Forms & Inputs
314-
315- - [ ] All ` <input> ` elements have associated ` <label> ` or ` aria-label `
316- - [ ] Required fields marked with ` aria-required="true" `
317- - [ ] Error messages linked via ` aria-describedby `
318- - [ ] Form validation messages announced to screen readers
319-
320- #### 7. Headings & Structure
321-
322- - [ ] Proper heading hierarchy (h1 → h2 → h3, no skips)
323- - [ ] Meaningful heading text (not "Click here", "More")
324- - [ ] One h1 per page (main topic/title)
325-
326- #### 8. Alerts & Notifications
327-
328- - [ ] Success/error messages: ` role="alert" ` with ` aria-live="assertive" `
329- - [ ] Info messages: ` aria-live="polite" `
330- - [ ] Notification timeout announcements
331-
332- ### Testing Strategy
333-
334- 1 . ** Chrome DevTools Accessibility Audit**
335- - Run DevTools → Lighthouse → Accessibility
336- - Document all failures and warnings
337- - Prioritize by impact and frequency
338-
339- 2 . ** Manual Testing**
340- - Keyboard navigation (Tab, Shift+Tab, Enter, Escape)
341- - Screen reader testing (NVDA, JAWS, or macOS VoiceOver)
342- - Color contrast checking (use WebAIM contrast checker)
343- - Zoom testing (up to 200% at 1280px width)
344-
345- 3 . ** Automated Testing**
346- - axe DevTools browser extension
347- - WAVE browser extension
348- - Pa11y CLI tool for batch testing
349-
350- ### Implementation Priority
351-
352- ** High Priority** (impacts many users):
353-
354- - HTML lang attribute and meta tags
355- - Skip links
356- - Color contrast fixes
357- - Form label associations
358- - Heading hierarchy
359-
360- ** Medium Priority** (improves usability):
361-
362- - Focus visible styles
363- - Modal focus traps
364- - Dynamic page titles
365- - Confirmation dialogs
366-
367- ** Low Priority** (nice to have):
368-
369- - Advanced ARIA patterns
370- - Internationalization meta tags
371- - Schema.org microdata
286+ Reports are automatically timestamped (e.g., ` localhost_5000-20251113T152932.json ` ) - save to ` dev/ ` directory.
372287
373288## Processing Lighthouse JSON Reports
374289
375- When analyzing Lighthouse accessibility audit reports, use Python and ` jq ` to extract data:
376-
377- ### Quick Summary of Audit Results
290+ ### Extract Summary of Results
378291
379292``` bash
380- # Parse Lighthouse report to see pass/fail counts for key audits
381293python3 << 'EOF '
382294import json
383295
384296with open('dev/localhost_5000-TIMESTAMP.json') as f:
385297 report = json.load(f)
386298
387- audits_to_check = [
388- 'aria-required-parent',
389- 'aria-required-children',
390- 'aria-command-name',
391- 'image-alt',
392- 'label-content-name-mismatch',
393- 'link-name',
394- 'color-contrast',
395- ]
396-
397- print("Lighthouse Accessibility Audit Results")
299+ audits = ['aria-required-parent', 'aria-required-children', 'aria-command-name',
300+ 'image-alt', 'label-content-name-mismatch', 'link-name', 'color-contrast']
301+
302+ print("Lighthouse Accessibility Audit Summary")
398303print("=" * 70)
399304
400- for audit_id in audits_to_check :
305+ for audit_id in audits :
401306 if audit_id in report['audits']:
402307 audit = report['audits'][audit_id]
403308 score = audit.get('score')
404- passed = len(audit.get('details', {}).get('passed', []))
405309 failed = len(audit.get('details', {}).get('failed', []))
406- status = "✓ PASS" if score == 1 else f"✗ FAIL ({score})"
407- print(f"{audit_id:35} | {status:12} | Pass: {passed:2} | Fail: {failed:2}")
408-
310+ status = "✓ PASS" if score == 1 else f"✗ FAIL ({failed} issues)"
311+ print(f"{audit_id:35} {status}")
409312EOF
410313```
411314
412- ### Detailed Failure Analysis
315+ ### Extract Failure Details
413316
414317``` bash
415- # Show failure details for specific audit
416318python3 << 'EOF '
417319import json
418320
419321with open('dev/localhost_5000-TIMESTAMP.json') as f:
420322 report = json.load(f)
421323
422- audit_id = 'aria-required-parent' # Change to audit you want to inspect
324+ # Change audit_id to inspect a specific audit
325+ audit_id = 'aria-required-parent'
423326if audit_id in report['audits']:
424- audit = report['audits'][audit_id]
425- failed = audit.get('details', {}).get('failed', [])
426-
427- print(f"\nAudit: {audit_id}")
428- print(f"Failed items: {len(failed)}\n")
429-
430- for item in failed[:5]: # Show first 5
431- selector = item.get('node', {}).get('selector', 'unknown')
432- snippet = item.get('node', {}).get('snippet', '')
433- explanation = item.get('node', {}).get('explanation', '')
434- print(f"Selector: {selector}")
435- print(f"HTML: {snippet[:100]}")
436- print(f"Issue: {explanation[:150]}")
437- print()
438-
327+ failed = report['audits'][audit_id].get('details', {}).get('failed', [])
328+ print(f"Audit: {audit_id} - {len(failed)} issues\n")
329+ for item in failed[:5]:
330+ print(f"Selector: {item['node']['selector']}")
331+ print(f"Issue: {item['node']['explanation'][:150]}\n")
439332EOF
440333```
441334
442- ### Using jq for Quick Inspection
335+ ### Quick jq Inspection
443336
444337``` bash
445- # List all audit IDs in the report
338+ # List all audit IDs
446339jq ' .audits | keys[]' dev/localhost_5000-TIMESTAMP.json
447340
448- # Count failed items for specific audit
341+ # Count failures for specific audit
449342jq ' .audits["aria-required-parent"].details.failed | length' dev/localhost_5000-TIMESTAMP.json
450343
451- # Show failed node selectors
344+ # Show failure selectors
452345jq ' .audits["aria-required-parent"].details.failed[].node.selector' dev/localhost_5000-TIMESTAMP.json
453346```
454347
455- ### Key Points
456-
457- 1 . ** File location** : Reports are saved to ` dev/localhost_5000-TIMESTAMP.json ` after each Lighthouse run
458- 2 . ** Structure** : ` report['audits'][audit_id]['details']['failed'] ` contains failure array
459- 3 . ** Node info** : Each failure has ` .node ` with selector, snippet, explanation
460- 4 . ** Score values** : score = 1 means PASS, score = 0 means FAIL
461- 5 . ** Performance** : Python scripts are faster than jq for summary reports
348+ ** Key** : ` report['audits'][audit_id]['details']['failed'] ` contains the failure array with ` .node.selector ` , ` .node.snippet ` , and ` .node.explanation ` .
462349
463350## Lighthouse Accessibility Audit Results (Desktop)
464351
@@ -468,78 +355,24 @@ jq '.audits["aria-required-parent"].details.failed[].node.selector' dev/localhos
468355
469356### Failures Found (7 issues)
470357
471- 1 . ** [ color-contrast] ( https://dequeuniversity.com/rules/axe/4.11/color-contrast ) ** (14 items) ⚠️ ** DEFER**
472- - Background and foreground colors don't meet WCAG AA ratios (4.5:1 normal, 3:1 large)
473- - ** Plan** : Handle via custom antd theme with 3 options: "Antd (standard)", "Cocalc", "Accessibility"
474- - Store in preferences/appearance config
475- - Ignore contrast requirements for ornamental details (e.g., footer)
476-
477- 2 . ** [ aria-required-parent] ( https://dequeuniversity.com/rules/axe/4.11/aria-required-parent ) ** (4 items) ✅ ** FIXED**
478- - Ant Design Tabs: ` role="tab" ` elements missing required ` tablist ` parent role
479- - Fixed by adding ` role="tablist" ` to SortableTabs container in ` packages/frontend/components/sortable-tabs.tsx ` (line 115)
480- - Creates proper ARIA hierarchy: tablist parent → Ant Design's role="tab" children
481- - Fixes tabs in projects-nav and file-tabs components
482-
483- 3 . ** [ image-alt] ( https://dequeuniversity.com/rules/axe/4.11/image-alt ) ** (3 items) ✅ ** FIXED**
484- - All avatar images inside ` .ant-avatar ` components missing ` [alt] ` attributes
485- - These images convey meaningful information about users/projects/models, not decorative
486- - Fixed in:
487- - ` packages/frontend/account/avatar/avatar.tsx ` - user avatar images with ` alt="User {username}" ` (used in collaborators, etc.)
488- - ` packages/frontend/components/language-model-icon.tsx ` - LLM model icons with ` alt="{vendorName} language model" `
489- - ` packages/frontend/projects/project-title.tsx ` - project avatar in titles with ` src={avatar} alt="Project avatar" `
490- - ` packages/frontend/projects/project-avatar.tsx ` - project avatar display with ` src={avatarImage} alt="Project avatar" `
491- - ` packages/frontend/projects/projects-nav.tsx ` - project avatar in nav with ` src={...} alt="Project avatar" `
492- - ` packages/frontend/projects/projects-table-columns.tsx ` - project avatars in table and collaborator avatars in filters with appropriate alt text
493- - Changed from ` icon={<img src={...} />} ` to ` src={...} alt="..." ` to properly expose alt attribute to Ant Design Avatar
494-
495- 4 . ** [ label-content-name-mismatch] ( https://dequeuniversity.com/rules/axe/4.11/label-content-name-mismatch ) ** (3 items) ✅ ** FIXED**
496- - Fixed in ` packages/frontend/projects/projects-table-controls.tsx ` :
497- - "Hidden" switch: ` aria-label="Toggle hidden projects" ` (was "Show hidden projects")
498- - "Deleted" switch: ` aria-label="Toggle deleted projects" ` (was "Show deleted projects")
499- - "Create Project" button: ` aria-label="Create a new project ..." ` (was "Create a new project")
500- - Visible text now matches or is included in accessible names
501-
502- 5 . ** [ aria-command-name] ( https://dequeuniversity.com/rules/axe/4.11/aria-command-name ) ** (1 item) ✅ ** FIXED**
503- - Fixed by adding ` aria-label="Admin" ` to admin NavTab in ` packages/frontend/app/page.tsx ` (line 233)
504- - Now admin button has accessible name for screen readers
505-
506- 6 . ** [ link-name] ( https://dequeuniversity.com/rules/axe/4.11/link-name ) ** (1 item) ✅ ** FIXED**
507- - CoCalc logo link was missing accessible name
508- - Fixed by:
509- - Adding ` aria-label="CoCalc homepage" ` to AppLogo in ` packages/frontend/app/logo.tsx ` (line 39)
510- - Updated ` <A> ` component in ` packages/frontend/components/A.tsx ` to accept and forward ` aria-label ` prop
511-
512- 7 . ** [ aria-required-children] ( https://dequeuniversity.com/rules/axe/4.11/aria-required-children ) ** (2 items) 🔄 ** IN PROGRESS**
513- - tablist parent has invalid children (role=button, buttons that should be role=tab)
514- - Root cause: SortableTab wrapper adds ` role="button" ` which is invalid inside tablist
515- - Issue in: Projects nav tabs and file tabs where SortableTab (with dnd-kit) wraps the tab elements
516- - Solution needed: Adjust SortableTab wrapper to use ` role="tab" ` or restructure to avoid role conflict
517-
518- ### Latest Report Analysis (2025-11-13 15:22:38 UTC)
519-
520- ** Final Fixes Applied:**
521-
522- ** label-content-name-mismatch** (3 items) ✅ ** FIXED**
523-
524- - Removed duplicate aria-labels from Switch components (kept only visible text)
525- - Removed mismatched aria-label from Create button (visible text is sufficient)
526- - Switches now use checkedChildren/unCheckedChildren for visible text only
527- - Create button uses visible text without aria-label override
528-
529- ** aria-required-children** (2 items) ✅ ** FIXED**
530-
531- - Added ` role="tab" ` to SortableTab wrapper in ` packages/frontend/components/sortable-tabs.tsx ` (line 154)
532- - This overrides the ` role="button" ` that dnd-kit attributes add via spread operator
533- - Now tablist only has role="tab" children (both wrapper and inner Ant Design tab)
534-
535- ** aria-required-parent** (4 items) 🔄 ** PENDING VERIFICATION**
358+ #. ** [ color-contrast] ( https://dequeuniversity.com/rules/axe/4.11/color-contrast ) ** (14 items) ⚠️ ** DEFER**
536359
537- - May be auto-fixed by the SortableTab role="tab" change
538- - Need to run Lighthouse again to verify
360+ - Background and foreground colors don't meet WCAG AA ratios (4.5:1 normal, 3:1 large)
361+ - ** Plan** : Handle via custom antd theme with 3 options: "Antd (standard)", "Cocalc", "Accessibility"
362+ - Store in preferences/appearance config
363+ - Ignore contrast requirements for ornamental details (e.g., footer)
364+
365+ #. ** [ aria-required-children] ( https://dequeuniversity.com/rules/axe/4.11/aria-required-children ) ** (2 items) 🔄 ** IN PROGRESS**
539366
540- ### Current Status Summary (2025-11-13 15:29:32 UTC)
367+ - tablist parent has invalid children (role=button, buttons that should be role=tab)
368+ - Root cause: SortableTab wrapper adds ` role="button" ` which is invalid inside tablist
369+ - Issue in: Projects nav tabs and file tabs where SortableTab (with dnd-kit) wraps the tab elements
370+ - Solution needed: Adjust SortableTab wrapper to use ` role="tab" ` or restructure to avoid role conflict
541371
542- ** Report** : localhost_5000-20251113T152932.json
372+ #. ** aria-required-parent** (4 items) 🔄 ** PENDING VERIFICATION**
373+
374+ - May be auto-fixed by the SortableTab role="tab" change
375+ - Need to run Lighthouse again to verify
543376
544377After all fixes:
545378
@@ -551,8 +384,6 @@ After all fixes:
551384- ❌ aria-required-children: 2 failures (tablist has non-tab/status children)
552385- ⚠️ color-contrast: 8+ items - DEFERRED to custom antd theme implementation
553386
554- ** Score** : 5/7 audits passing (71% of issues fixed)
555-
556387### Remaining Issue: aria-required-parent & aria-required-children
557388
558389** 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.
0 commit comments