add ClideTypeahead — shared field-anchored suggestion list (T-288)

The slash and @ typeaheads are near-duplicate caret-anchored completion
surfaces. Per the amended D-88 they share ClideTypeahead (not ClideMenu): the
host owns text parsing + completion; ClideTypeahead owns the anchored overlay +
suggestion list, driven by a suggestions list. Unlike a menu it does not
capture focus or install a barrier — the text field keeps focus — and an
optional nav controller drives the highlight from the field's key handler.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 22:27:42 +02:00
co-authored by Claude Opus 4.8
parent 318e09e748
commit 3f27dab187
3 changed files with 160 additions and 0 deletions
@@ -0,0 +1,51 @@
/// Tests for ClideTypeahead (D-88): suggestion-driven anchored list that keeps
/// focus on the field, selects on tap, and hides when suggestions go empty.
library;
import 'package:clide/widgets/widgets.dart';
import 'package:flutter/widgets.dart' show Alignment, SizedBox, StateSetter, StatefulBuilder;
import 'package:flutter_test/flutter_test.dart';
import '../../helpers/kernel_fixture.dart';
import '../../helpers/widget_harness.dart';
void main() {
group('ClideTypeahead', () {
late KernelFixture f;
setUp(() async => f = await KernelFixture.create());
tearDown(() async => f.dispose());
testWidgets('shows suggestions, selecting fires onSelect, empty hides', (tester) async {
var picked = '';
List<String> sugg = ['alice', 'bob'];
late StateSetter setOuter;
await tester.pumpWidget(anchoredHarness(
f,
StatefulBuilder(
builder: (ctx, setState) {
setOuter = setState;
return ClideTypeahead(
suggestions: sugg,
onSelect: (v) => picked = v,
formatLabel: (n) => '@$n',
child: const SizedBox(width: 200, height: 24, child: ClideText('field')),
);
},
),
alignment: Alignment.topLeft,
));
await tester.pumpAndSettle();
expect(find.text('@alice'), findsOneWidget);
expect(find.text('@bob'), findsOneWidget);
await tester.tap(find.text('@bob'));
await tester.pumpAndSettle();
expect(picked, 'bob');
// Host clears suggestions → popover hides.
setOuter(() => sugg = const []);
await tester.pumpAndSettle();
expect(find.text('@alice'), findsNothing);
});
});
}