From 6337bec152c97c19c918d08fca1d882dc58d9a33 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 3 Jun 2026 09:19:35 +0200 Subject: [PATCH] make Claude sidebar sub-tabs keyboard-activatable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Activity / Team / Config sub-tabs were built on a raw GestureDetector, so Tab traversal skipped them and Enter/Space did nothing — a gap against the repo's a11y contract. Switch to ClideTappable (focusable, Enter/Space activates) wrapped in button + selected semantics, and add a test that drives the switch via ActivateIntent rather than a pointer tap. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 5 +++++ lib/builtin/claude/src/claude_meta_sidebar.dart | 14 +++++++++----- .../builtin/claude/claude_meta_sidebar_test.dart | 16 +++++++++++++++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7d1776a..b8661474 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Fixed +- The Claude sidebar's Activity / Team / Config sub-tabs are now keyboard- + activatable: they were pointer-only (raw `GestureDetector`), so Tab traversal + skipped them and Enter/Space did nothing. They now use `ClideTappable` + (focusable, Enter/Space → activate) and carry button + selected semantics. + (T-182) - The default keymap is no longer silently disabled at startup: `default.yaml` bound Tab/Shift+Tab to undefined `focus.next`/`focus.previous` intents, which made the loader drop the entire preset (palette, quick-open, find-in-files, diff --git a/lib/builtin/claude/src/claude_meta_sidebar.dart b/lib/builtin/claude/src/claude_meta_sidebar.dart index 9c12c800..c30f564e 100644 --- a/lib/builtin/claude/src/claude_meta_sidebar.dart +++ b/lib/builtin/claude/src/claude_meta_sidebar.dart @@ -1229,11 +1229,15 @@ class _TabStrip extends StatelessWidget { for (final t in SidebarTab.values) Padding( padding: const EdgeInsets.only(right: 16), - child: GestureDetector( + child: Semantics( + button: true, + selected: t == current, + label: _label(t), + excludeSemantics: true, onTap: () => onPick(t), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: Container( + child: ClideTappable( + onTap: () => onPick(t), + builder: (ctx, hovered, _) => Container( padding: const EdgeInsets.only(bottom: 3), decoration: BoxDecoration( border: Border( @@ -1246,7 +1250,7 @@ class _TabStrip extends StatelessWidget { child: ClideText( _label(t), fontSize: clideFontSmall, - color: t == current ? tokens.globalForeground : tokens.globalTextMuted, + color: t == current || hovered ? tokens.globalForeground : tokens.globalTextMuted, ), ), ), diff --git a/test/builtin/claude/claude_meta_sidebar_test.dart b/test/builtin/claude/claude_meta_sidebar_test.dart index 94ee0800..e792a5c8 100644 --- a/test/builtin/claude/claude_meta_sidebar_test.dart +++ b/test/builtin/claude/claude_meta_sidebar_test.dart @@ -11,7 +11,7 @@ import 'package:clide/builtin/claude/src/transcript_publisher.dart'; import 'package:clide/builtin/claude/src/transcript_reader.dart'; import 'package:clide/kernel/kernel.dart'; import 'package:flutter/services.dart' show LogicalKeyboardKey; -import 'package:flutter/widgets.dart' show EditableText, SizedBox, Semantics; +import 'package:flutter/widgets.dart' show ActivateIntent, Actions, EditableText, SizedBox, Semantics; import 'package:flutter_test/flutter_test.dart'; import '../../helpers/kernel_fixture.dart'; @@ -106,6 +106,20 @@ void main() { expect(find.text('TODAY'), findsOneWidget); }); + testWidgets('a sub-tab is keyboard-activatable, not pointer-only (T-182)', (tester) async { + await tester.pumpWidget(harness(f, sidebar(stats: stats))); + await tester.pumpAndSettle(); + + // The tab wraps its label in a ClideTappable, so the keymap's Enter/Space + // → ActivateIntent reaches it. Dispatch ActivateIntent through the real + // Actions path (what KeymapService does) — no pointer tap — and the body + // switches. Mirrors test/widgets/src/clide_tappable_test.dart. + Actions.invoke(tester.element(find.text('Team')), const ActivateIntent()); + await tester.pump(); + expect(find.text('No team active.'), findsOneWidget); + expect(find.text('TODAY'), findsNothing); + }); + testWidgets('Config tab renders the settings table over ClaudeConfig', (tester) async { final dir = Directory.systemTemp.createTempSync('cfg'); addTearDown(() => dir.deleteSync(recursive: true));