Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,8 @@ build/

# FVM Version Cache
.fvm/

# Agent runtime notes (never committed)
AI_AGENT_CHANGES/
AGENT.md
AGENTS.md
176 changes: 95 additions & 81 deletions lib/src/modules/common/search_list/integrated_search_textfield.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class IntegratedSearchTextField extends StatefulWidget {
required this.queryTextController,
this.showCrossbutton = false,
this.borderRadius = 5,
this.trailing,
Key? key,
}) : super(key: key);

Expand All @@ -36,6 +37,8 @@ class IntegratedSearchTextField extends StatefulWidget {
final VoidCallback? onMicTap;
final double borderRadius;

final Widget? trailing;

// final SearchListBloc _searchListBloc=SearchListBloc();

@override
Expand All @@ -49,95 +52,106 @@ class _IntegratedSearchTextFieldState extends State<IntegratedSearchTextField> {
FlutterTts _flutterTts = FlutterTts();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
elevation: widget.elevation ?? 0,
child: TextField(
onTap: widget.onTap,
controller: widget.queryTextController,
style: TextStyle(color: Colors.black),
autofocus: widget.autoFocus ?? true,
textInputAction: widget.textInputAction,
keyboardType: widget.keyboardType,
onSubmitted: widget.onSubmitted,
decoration: InputDecoration(
filled: true,
fillColor: widget.bgColor,
prefixIcon: widget.prefixIcon,
isDense: true,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(widget.borderRadius),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
hintText: widget.searchFieldLabel,
suffixIcon: Row(
children: <Widget>[
if (widget.showCrossbutton)
Container(
width: 18,
height: 18,
margin: EdgeInsets.fromLTRB(10, 10, 10, 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9),
color: Colors.black.withOpacity(0.4),
),
child: InkWell(
onTap: widget.queryTextController.clear,
child: Icon(
Icons.close,
size: 12,
color: Colors.white,
),
final searchCard = Card(
elevation: widget.elevation ?? 0,
child: TextField(
onTap: widget.onTap,
controller: widget.queryTextController,
style: TextStyle(color: Colors.black),
autofocus: widget.autoFocus ?? true,
textInputAction: widget.textInputAction,
keyboardType: widget.keyboardType,
onSubmitted: widget.onSubmitted,
decoration: InputDecoration(
filled: true,
fillColor: widget.bgColor,
prefixIcon: widget.prefixIcon,
isDense: true,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(widget.borderRadius),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
hintText: widget.searchFieldLabel,
suffixIcon: Row(
children: <Widget>[
if (widget.showCrossbutton)
Container(
width: 18,
height: 18,
margin: EdgeInsets.fromLTRB(10, 10, 10, 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9),
color: Colors.black.withOpacity(0.4),
),
child: InkWell(
onTap: widget.queryTextController.clear,
child: Icon(
Icons.close,
size: 12,
color: Colors.white,
),
),
if (widget.searchThroughMic)
SizedBox(
width: 20,
child: IconButton(
onPressed: widget.onMicTap ??
() async {
await _speakPrompt();
await Future.delayed(
const Duration(seconds: 1),
),
if (widget.searchThroughMic)
SizedBox(
width: 20,
child: IconButton(
onPressed: widget.onMicTap ??
() async {
await _speakPrompt();
await Future.delayed(
const Duration(seconds: 1),
);
var available = await speech.initialize();
if (available) {
await speech.listen(
onResult: (result) {
setState(() {
print(widget.queryTextController.text);
recognizedText = result.recognizedWords;
widget.queryTextController.text =
recognizedText;
});
if (result.finalResult) {
speech.stop();
}
},
);
var available = await speech.initialize();
if (available) {
await speech.listen(
onResult: (result) {
setState(() {
print(widget.queryTextController.text);
recognizedText = result.recognizedWords;
widget.queryTextController.text =
recognizedText;
});
if (result.finalResult) {
speech.stop();
}
},
);
}
},
icon: Icon(Icons.mic),
),
}
},
icon: Icon(Icons.mic),
),
],
),
suffixIconConstraints: BoxConstraints(
maxHeight: 38,
maxWidth: 38,
),
hintStyle: TextStyle(
color: Colors.black26,
),
),
],
),
suffixIconConstraints: BoxConstraints(
maxHeight: 38,
maxWidth: 38,
),
hintStyle: TextStyle(
color: Colors.black26,
),
),
),
);

return Padding(
key: const Key('integrated-search-textfield-padding'),
padding: const EdgeInsets.all(8.0),
child: widget.trailing == null
? searchCard
: Row(
children: <Widget>[
Expanded(child: searchCard),
const SizedBox(width: 8),
widget.trailing!,
Comment thread
rgulati-f2k marked this conversation as resolved.
],
),
);
}

Future<void> _speakPrompt() async {
Expand Down
7 changes: 7 additions & 0 deletions lib/src/modules/common/search_list/search_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class SearchList<T> extends StatefulWidget {
this.showCrossbutton = false,
this.bottomGradient,
this.actionWidget,
this.searchBarTrailing,
Key? key,
}) : assert(!showDefaultAppBar ? textEditingController != null : true),
super(key: key);
Expand Down Expand Up @@ -112,6 +113,10 @@ class SearchList<T> extends StatefulWidget {
final LinearGradient? bottomGradient;
final Widget? actionWidget;

/// Optional widget rendered beside the search field (e.g. a filter button).
/// Null by default, so every existing consumer renders byte-identically.
final Widget? searchBarTrailing;

@override
_SearchListState<T> createState() => _SearchListState<T>();
}
Expand Down Expand Up @@ -236,6 +241,7 @@ class _SearchListState<T> extends State<SearchList<T>> {
queryTextController: searchQueryController!,
searchFieldLabel: widget.searchBarTitle ?? 'Search',
showCrossbutton: widget.showCrossbutton,
trailing: widget.searchBarTrailing,
Comment thread
rgulati-f2k marked this conversation as resolved.
),
Expanded(child: _child),
],
Expand Down Expand Up @@ -290,6 +296,7 @@ class _SearchListState<T> extends State<SearchList<T>> {
widget.searchBarTitle ?? 'Search',
showCrossbutton: widget.showCrossbutton,
borderRadius: 14,
trailing: widget.searchBarTrailing,
),
],
),
Expand Down
59 changes: 59 additions & 0 deletions test/integrated_search_textfield_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// [REQ-177] IntegratedSearchTextField.trailing: an optional widget rendered beside the
// search field. Null by default, so an existing consumer's widget tree is unchanged.
import 'package:fa_flutter_ui_kit/src/modules/common/search_list/integrated_search_textfield.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
Widget wrap(Widget child) => MaterialApp(home: Scaffold(body: child));

testWidgets(
'renders the Card directly (no wrapping Row) when trailing is null',
(tester) async {
await tester.pumpWidget(
wrap(
IntegratedSearchTextField(
searchFieldLabel: 'Search',
queryTextController: TextEditingController(),
),
),
);

// find.byKey('search-trailing') can never fail here since that
// key is only ever attached by a caller-supplied trailing widget — assert the actual
// structural invariant instead: the outer Padding's child is the Card itself, not a Row.
final padding = tester.widget<Padding>(
find.byKey(const Key('integrated-search-textfield-padding')));
expect(padding.child, isA<Card>());
expect(find.byType(TextField), findsOneWidget);
});

testWidgets(
'renders the trailing widget beside the search field, inside a Row, when supplied',
(tester) async {
await tester.pumpWidget(
wrap(
IntegratedSearchTextField(
searchFieldLabel: 'Search',
queryTextController: TextEditingController(),
trailing: const Icon(Icons.filter_list, key: Key('search-trailing')),
),
),
);

expect(find.byKey(const Key('search-trailing')), findsOneWidget);
expect(find.byType(TextField), findsOneWidget);

// find.byType(Row) alone also matches the suffixIcon Row inside
// the TextField's decoration, so it passes even if `trailing` were placed elsewhere.
// Assert the actual wiring: the outer Padding's child is a Row whose children are
// Expanded(searchCard) followed by the trailing widget itself.
final padding = tester.widget<Padding>(
find.byKey(const Key('integrated-search-textfield-padding')));
expect(padding.child, isA<Row>());
final row = padding.child! as Row;
expect(row.children.first, isA<Expanded>());
expect((row.children.first as Expanded).child, isA<Card>());
expect(row.children.last.key, const Key('search-trailing'));
});
}
39 changes: 39 additions & 0 deletions test/search_list_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SearchList.searchBarTrailing is plumbed to IntegratedSearchTextField
// on both the SearchBarInBody and SearchWithAppBar code paths, but neither was pumped by a
// widget test. Covers both here.
import 'package:fa_flutter_ui_kit/src/modules/common/search_list/search_list.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
Widget buildSearchList(SearchListType type) {
return MaterialApp(
home: SearchList<String>(
data: const ['Alpha', 'Beta'],
selectedItem: (_) {},
itemBuilder: (item, isSelected) => Text(item),
type: type,
searchBarTrailing:
const Icon(Icons.filter_list, key: Key('search-trailing')),
),
);
}

testWidgets(
'SearchBarInBody renders searchBarTrailing beside the search field',
(tester) async {
await tester.pumpWidget(buildSearchList(SearchListType.SearchBarInBody));
await tester.pump();

expect(find.byKey(const Key('search-trailing')), findsOneWidget);
});

testWidgets(
'SearchWithAppBar renders searchBarTrailing beside the search field',
(tester) async {
await tester.pumpWidget(buildSearchList(SearchListType.SearchWithAppBar));
await tester.pump();

expect(find.byKey(const Key('search-trailing')), findsOneWidget);
});
}