make Claude sidebar sub-tabs keyboard-activatable
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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
0f6c5af1d9
commit
6337bec152
@@ -58,6 +58,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
|||||||
|
|
||||||
### Fixed
|
### 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`
|
- The default keymap is no longer silently disabled at startup: `default.yaml`
|
||||||
bound Tab/Shift+Tab to undefined `focus.next`/`focus.previous` intents, which
|
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,
|
made the loader drop the entire preset (palette, quick-open, find-in-files,
|
||||||
|
|||||||
@@ -1229,11 +1229,15 @@ class _TabStrip extends StatelessWidget {
|
|||||||
for (final t in SidebarTab.values)
|
for (final t in SidebarTab.values)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 16),
|
padding: const EdgeInsets.only(right: 16),
|
||||||
child: GestureDetector(
|
child: Semantics(
|
||||||
|
button: true,
|
||||||
|
selected: t == current,
|
||||||
|
label: _label(t),
|
||||||
|
excludeSemantics: true,
|
||||||
onTap: () => onPick(t),
|
onTap: () => onPick(t),
|
||||||
child: MouseRegion(
|
child: ClideTappable(
|
||||||
cursor: SystemMouseCursors.click,
|
onTap: () => onPick(t),
|
||||||
child: Container(
|
builder: (ctx, hovered, _) => Container(
|
||||||
padding: const EdgeInsets.only(bottom: 3),
|
padding: const EdgeInsets.only(bottom: 3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
@@ -1246,7 +1250,7 @@ class _TabStrip extends StatelessWidget {
|
|||||||
child: ClideText(
|
child: ClideText(
|
||||||
_label(t),
|
_label(t),
|
||||||
fontSize: clideFontSmall,
|
fontSize: clideFontSmall,
|
||||||
color: t == current ? tokens.globalForeground : tokens.globalTextMuted,
|
color: t == current || hovered ? tokens.globalForeground : tokens.globalTextMuted,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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/builtin/claude/src/transcript_reader.dart';
|
||||||
import 'package:clide/kernel/kernel.dart';
|
import 'package:clide/kernel/kernel.dart';
|
||||||
import 'package:flutter/services.dart' show LogicalKeyboardKey;
|
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 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import '../../helpers/kernel_fixture.dart';
|
import '../../helpers/kernel_fixture.dart';
|
||||||
@@ -106,6 +106,20 @@ void main() {
|
|||||||
expect(find.text('TODAY'), findsOneWidget);
|
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 {
|
testWidgets('Config tab renders the settings table over ClaudeConfig', (tester) async {
|
||||||
final dir = Directory.systemTemp.createTempSync('cfg');
|
final dir = Directory.systemTemp.createTempSync('cfg');
|
||||||
addTearDown(() => dir.deleteSync(recursive: true));
|
addTearDown(() => dir.deleteSync(recursive: true));
|
||||||
|
|||||||
Reference in New Issue
Block a user