Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14c6193367 |
No files matched your search
@@ -14,6 +14,7 @@ requirements
|
|||||||
TIS*
|
TIS*
|
||||||
!*.iss
|
!*.iss
|
||||||
screenshots/
|
screenshots/
|
||||||
|
issues/
|
||||||
|
|
||||||
# Environment secrets
|
# Environment secrets
|
||||||
backend/.env
|
backend/.env
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class _AppSearchBarState extends ConsumerState<AppSearchBar> {
|
|||||||
final LayerLink _layerLink = LayerLink();
|
final LayerLink _layerLink = LayerLink();
|
||||||
final Object _tapRegionGroupId = Object();
|
final Object _tapRegionGroupId = Object();
|
||||||
bool _isOverlayOpen = false;
|
bool _isOverlayOpen = false;
|
||||||
|
bool _isPointerOverOverlay = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(AppSearchBar oldWidget) {
|
void didUpdateWidget(AppSearchBar oldWidget) {
|
||||||
@@ -115,8 +116,8 @@ class _AppSearchBarState extends ConsumerState<AppSearchBar> {
|
|||||||
if (_focusNode.hasFocus && widget.enableHistory) {
|
if (_focusNode.hasFocus && widget.enableHistory) {
|
||||||
_showOverlay();
|
_showOverlay();
|
||||||
} else {
|
} else {
|
||||||
Future.delayed(const Duration(milliseconds: 150), () {
|
Future.delayed(const Duration(milliseconds: 200), () {
|
||||||
if (mounted && !_focusNode.hasFocus) {
|
if (mounted && !_focusNode.hasFocus && !_isPointerOverOverlay) {
|
||||||
_removeOverlay();
|
_removeOverlay();
|
||||||
|
|
||||||
if (widget.collapsible && _controller.text.trim().isEmpty) {
|
if (widget.collapsible && _controller.text.trim().isEmpty) {
|
||||||
@@ -144,7 +145,7 @@ class _AppSearchBarState extends ConsumerState<AppSearchBar> {
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted ||
|
if (!mounted ||
|
||||||
!_isOverlayOpen ||
|
!_isOverlayOpen ||
|
||||||
!_focusNode.hasFocus ||
|
(!_focusNode.hasFocus && !_isPointerOverOverlay) ||
|
||||||
_overlayEntry != null) {
|
_overlayEntry != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -169,9 +170,14 @@ class _AppSearchBarState extends ConsumerState<AppSearchBar> {
|
|||||||
offset: const Offset(0, 48),
|
offset: const Offset(0, 48),
|
||||||
child: TapRegion(
|
child: TapRegion(
|
||||||
groupId: _tapRegionGroupId,
|
groupId: _tapRegionGroupId,
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: MouseRegion(
|
||||||
|
onEnter: (_) => _isPointerOverOverlay = true,
|
||||||
|
onExit: (_) => _isPointerOverOverlay = false,
|
||||||
child: _buildHistoryOverlay(),
|
child: _buildHistoryOverlay(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -182,6 +188,7 @@ class _AppSearchBarState extends ConsumerState<AppSearchBar> {
|
|||||||
|
|
||||||
void _removeOverlay() {
|
void _removeOverlay() {
|
||||||
_isOverlayOpen = false;
|
_isOverlayOpen = false;
|
||||||
|
_isPointerOverOverlay = false;
|
||||||
if (_overlayEntry != null) {
|
if (_overlayEntry != null) {
|
||||||
if (_overlayEntry!.mounted) {
|
if (_overlayEntry!.mounted) {
|
||||||
_overlayEntry!.remove();
|
_overlayEntry!.remove();
|
||||||
@@ -208,7 +215,11 @@ class _AppSearchBarState extends ConsumerState<AppSearchBar> {
|
|||||||
|
|
||||||
if (displayList.isEmpty) return const SizedBox.shrink();
|
if (displayList.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
return Material(
|
return Focus(
|
||||||
|
canRequestFocus: false,
|
||||||
|
skipTraversal: true,
|
||||||
|
descendantsAreFocusable: false,
|
||||||
|
child: Material(
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
color: isDark ? AppColors.darkSurfaceCard : Colors.white,
|
color: isDark ? AppColors.darkSurfaceCard : Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@@ -337,30 +348,32 @@ class _AppSearchBarState extends ConsumerState<AppSearchBar> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
InkResponse(
|
IconButton(
|
||||||
radius: 16,
|
icon: Icon(
|
||||||
canRequestFocus: false,
|
|
||||||
onTap: () {
|
|
||||||
ref
|
|
||||||
.read(searchHistoryProvider.notifier)
|
|
||||||
.removeSearch(term);
|
|
||||||
if (!_focusNode.hasFocus) {
|
|
||||||
_focusNode.requestFocus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 9,
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
Icons.close_rounded,
|
Icons.close_rounded,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: isDark
|
color: isDark
|
||||||
? AppColors.darkTextMuted
|
? AppColors.darkTextMuted
|
||||||
: Colors.grey.shade400,
|
: Colors.grey.shade400,
|
||||||
),
|
),
|
||||||
|
splashRadius: 16,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 8,
|
||||||
),
|
),
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
tooltip: 'Remove',
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(searchHistoryProvider.notifier)
|
||||||
|
.removeSearch(term);
|
||||||
|
if (!_focusNode.hasFocus) {
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
}
|
||||||
|
if (_overlayEntry != null) {
|
||||||
|
_overlayEntry!.markNeedsBuild();
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -370,6 +383,7 @@ class _AppSearchBarState extends ConsumerState<AppSearchBar> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -408,6 +422,8 @@ class _AppSearchBarState extends ConsumerState<AppSearchBar> {
|
|||||||
!_isOverlayOpen &&
|
!_isOverlayOpen &&
|
||||||
widget.enableHistory) {
|
widget.enableHistory) {
|
||||||
_showOverlay();
|
_showOverlay();
|
||||||
|
} else if (_isOverlayOpen) {
|
||||||
|
_overlayEntry?.markNeedsBuild();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -443,11 +459,14 @@ class _AppSearchBarState extends ConsumerState<AppSearchBar> {
|
|||||||
},
|
},
|
||||||
child: TapRegion(
|
child: TapRegion(
|
||||||
groupId: _tapRegionGroupId,
|
groupId: _tapRegionGroupId,
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
onTapOutside: (event) {
|
onTapOutside: (event) {
|
||||||
|
if (!_isPointerOverOverlay) {
|
||||||
if (_focusNode.hasFocus) {
|
if (_focusNode.hasFocus) {
|
||||||
_focusNode.unfocus();
|
_focusNode.unfocus();
|
||||||
}
|
}
|
||||||
_removeOverlay();
|
_removeOverlay();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: CompositedTransformTarget(
|
child: CompositedTransformTarget(
|
||||||
link: _layerLink,
|
link: _layerLink,
|
||||||
|
|||||||
@@ -161,4 +161,89 @@ void main() {
|
|||||||
// Overlay is dismissed
|
// Overlay is dismissed
|
||||||
expect(find.text('Recent Searches'), findsNothing);
|
expect(find.text('Recent Searches'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('AppSearchBar inside showDialog supports clear all and item deletion', (tester) async {
|
||||||
|
final container = ProviderContainer();
|
||||||
|
addTearDown(container.dispose);
|
||||||
|
|
||||||
|
container.read(searchHistoryProvider);
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
final searchController = TextEditingController();
|
||||||
|
final searchFocusNode = FocusNode();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
UncontrolledProviderScope(
|
||||||
|
container: container,
|
||||||
|
child: MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierColor: Colors.black54,
|
||||||
|
builder: (ctx) {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 80, 16, 0),
|
||||||
|
child: Material(
|
||||||
|
child: AppSearchBar(
|
||||||
|
controller: searchController,
|
||||||
|
focusNode: searchFocusNode,
|
||||||
|
collapsible: false,
|
||||||
|
hint: 'Search students by LRN or Name...',
|
||||||
|
maxWidth: 600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
searchFocusNode.requestFocus();
|
||||||
|
},
|
||||||
|
child: const Text('Open Search Dialog'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.text('Open Search Dialog'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Recent Searches'), findsOneWidget);
|
||||||
|
expect(find.text('test item 1'), findsOneWidget);
|
||||||
|
expect(find.text('test item 2'), findsOneWidget);
|
||||||
|
|
||||||
|
// Find the close buttons in the history list
|
||||||
|
final closeIcons = find.byIcon(Icons.close_rounded);
|
||||||
|
expect(closeIcons, findsWidgets);
|
||||||
|
|
||||||
|
// Tap the first history item's delete button
|
||||||
|
await tester.tap(closeIcons.first);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 200));
|
||||||
|
|
||||||
|
// Item 1 should be removed, item 2 should still be there, and overlay should remain open!
|
||||||
|
expect(container.read(searchHistoryProvider), isNot(contains('test item 1')));
|
||||||
|
expect(container.read(searchHistoryProvider), contains('test item 2'));
|
||||||
|
expect(find.text('Recent Searches'), findsOneWidget);
|
||||||
|
expect(find.text('test item 2'), findsOneWidget);
|
||||||
|
|
||||||
|
// Tap Clear all
|
||||||
|
await tester.tap(find.text('Clear all'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// All items cleared and overlay closed
|
||||||
|
expect(container.read(searchHistoryProvider), isEmpty);
|
||||||
|
expect(find.text('Recent Searches'), findsNothing);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in new issue
Block a user