TF-3911 Support keyboard shortcut actions ESC to unfocus search input on web

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2025-07-29 03:45:51 +07:00
committed by Dat H. Pham
parent 226f6520a7
commit f169ff5feb
5 changed files with 164 additions and 81 deletions
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:tmail_ui_user/features/base/extensions/logical_key_set_helper.dart';
import 'package:tmail_ui_user/features/base/shortcut/mail/mail_list_action_shortcut_type.dart';
import 'package:tmail_ui_user/features/base/shortcut/mail/mail_view_action_shortcut_type.dart';
import 'package:tmail_ui_user/features/base/shortcut/search/search_action_shortcut_type.dart';
class AppShortcutManager {
static MailViewActionShortcutType? getMailViewActionFromEvent(KeyEvent event) {
@@ -42,4 +43,14 @@ class AppShortcutManager {
return null;
}
}
static SearchActionShortcutType? getSearchActionFromEvent(KeyEvent event) {
final keysPressed = HardwareKeyboard.instance.logicalKeysPressed;
log('AppShortcutManager::getSearchActionFromEvent: Keys pressed: $keysPressed');
if (keysPressed.isOnly(LogicalKeyboardKey.escape)) {
return SearchActionShortcutType.unFocus;
} else {
return null;
}
}
}
@@ -0,0 +1,4 @@
enum SearchActionShortcutType {
unFocus;
}
@@ -27,6 +27,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/quick_sear
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_all_recent_search_latest_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/quick_search_email_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_recent_search_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_keyboard_shortcut_actions_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart';
@@ -52,6 +53,7 @@ class SearchController extends BaseController with DateRangePickerMixin {
SearchQuery? get searchQuery => searchEmailFilter.value.text;
FocusNode searchFocus = FocusNode();
FocusNode? keyboardFocusNode;
String currentSearchText = '';
SearchController(
@@ -64,11 +66,17 @@ class SearchController extends BaseController with DateRangePickerMixin {
void onInit() {
super.onInit();
searchFocus.addListener(_onSearchFocusChanged);
onKeyboardShortcutInit();
}
void _onSearchFocusChanged() {
log('SearchController::_onSearchFocusChanged: ${searchFocus.hasFocus}');
isSearchInputFocused.value = searchFocus.hasFocus;
if (searchFocus.hasFocus) {
refocusKeyboardShortcutFocus();
} else {
clearKeyboardShortcutFocus();
}
}
void openAdvanceSearch() {
@@ -336,6 +344,7 @@ class SearchController extends BaseController with DateRangePickerMixin {
searchInputController.dispose();
searchFocus.removeListener(_onSearchFocusChanged);
searchFocus.dispose();
onKeyboardShortcutDispose();
super.onClose();
}
}
@@ -0,0 +1,47 @@
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart' hide SearchController;
import 'package:tmail_ui_user/features/base/shortcut/app_shortcut_manager.dart';
import 'package:tmail_ui_user/features/base/shortcut/search/search_action_shortcut_type.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart';
extension HandleKeyboardShortcutActionsExtension on SearchController {
void onKeyboardShortcutInit() {
if (PlatformInfo.isWeb) {
keyboardFocusNode = FocusNode();
}
}
void onKeyboardShortcutDispose() {
if (PlatformInfo.isWeb) {
keyboardFocusNode?.dispose();
}
}
void clearKeyboardShortcutFocus() {
if (keyboardFocusNode?.hasFocus == true) {
keyboardFocusNode?.unfocus();
}
}
void refocusKeyboardShortcutFocus() {
if (keyboardFocusNode?.hasFocus == false) {
keyboardFocusNode?.requestFocus();
}
}
void onKeyDownEventAction(KeyEvent event) {
final shortcutType = AppShortcutManager.getSearchActionFromEvent(event);
log('$runtimeType::onKeyDownEventAction:🔥Shortcut triggered: $shortcutType');
if (shortcutType == null) return;
handleSearchShortcutAction(shortcutType);
}
void handleSearchShortcutAction(SearchActionShortcutType shortcutType) {
log('$runtimeType::handleSearchShortcutAction: 🔥 Shortcut triggered: $shortcutType');
if (searchFocus.hasFocus) {
searchFocus.unfocus();
}
}
}
@@ -20,10 +20,12 @@ import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:model/email/presentation_email.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart';
import 'package:tmail_ui_user/features/base/widget/keyboard/keyboard_handler_wrapper.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search;
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_keyboard_shortcut_actions_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_overlay.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/icon_open_advanced_search_widget.dart';
@@ -52,6 +54,96 @@ class SearchInputFormWidget extends StatelessWidget with AppLoaderMixin {
@override
Widget build(BuildContext context) {
return Obx(() {
Widget searchInputForm = QuickSearchInputForm<PresentationEmail, EmailAddress, RecentSearch>(
maxHeight: 52,
suggestionsBoxVerticalOffset: 0.0,
minInputLengthAutocomplete: _dashBoardController.minInputLengthAutocomplete,
textFieldConfiguration: _createConfiguration(context),
suggestionsBoxDecoration: const QuickSearchSuggestionsBoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(16)),
),
debounceDuration: const Duration(milliseconds: 300),
listActionButton: const [
QuickSearchFilter.hasAttachment,
QuickSearchFilter.last7Days,
QuickSearchFilter.fromMe,
QuickSearchFilter.starred,
],
actionButtonBuilder: (context, filterAction, suggestionsListState) {
if (filterAction is QuickSearchFilter) {
return buildListButtonForQuickSearchForm(
context,
filterAction,
suggestionsListState);
} else {
return const SizedBox.shrink();
}
},
buttonActionCallback: (filterAction) {
if (filterAction is QuickSearchFilter) {
_searchController.addQuickSearchFilterToSuggestionSearchView(filterAction);
}
},
listActionPadding: const EdgeInsets.only(left: 12, right: 12, top: 12, bottom: 6),
titleHeaderRecent: Padding(
padding: const EdgeInsets.only(left: 12, right: 12, bottom: 8, top: 12),
child: Text(
AppLocalizations.of(context).recent,
style: ThemeUtils.defaultTextStyleInterFont.copyWith(
fontSize: 13.0,
color: AppColor.colorTextButtonHeaderThread,
fontWeight: FontWeight.w500
)
)
),
buttonShowAllResult: (context, keyword, _) {
if (keyword is String) {
return _buildShowAllResultButton(context, keyword);
} else {
return const SizedBox.shrink();
}
},
loadingBuilder: (context) => Padding(
padding: const EdgeInsets.only(bottom: 16),
child: loadingWidget
),
fetchRecentActionCallback: (pattern) {
final accountId = _dashBoardController.accountId.value;
final userName = _dashBoardController.sessionCurrent?.username;
if (accountId == null || userName == null) {
logError('SearchInputFormWidget::fetchRecentActionCallback: accountId or userName is null');
return <RecentSearch>[];
} else {
return _searchController.getAllRecentSearchAction(
accountId,
userName,
pattern,
);
}
},
itemRecentBuilder: (context, recent) => RecentSearchItemTileWidget(recent),
onRecentSelected: _invokeSelectRecentItem,
suggestionsCallback: _dashBoardController.quickSearchEmails,
itemBuilder: (context, email) => EmailQuickSearchItemTileWidget(
email,
_dashBoardController.selectedMailbox.value,
searchQuery: SearchQuery(_searchController.currentSearchText.trim())),
onSuggestionSelected: _invokeSelectSuggestionItem,
contactItemBuilder: (context, emailAddress) => ContactQuickSearchItem(emailAddress: emailAddress),
contactSuggestionsCallback: _dashBoardController.getContactSuggestion,
onContactSuggestionSelected: _invokeSelectContactSuggestion,
);
if (_searchController.keyboardFocusNode != null) {
searchInputForm = KeyboardHandlerWrapper(
focusNode: _searchController.keyboardFocusNode!,
onKeyDownEventAction: _searchController.onKeyDownEventAction,
child: searchInputForm,
);
}
return PortalTarget(
visible: _searchController.isAdvancedSearchViewOpen.isTrue,
portalFollower: PointerInterceptor(
@@ -73,87 +165,7 @@ class SearchInputFormWidget extends StatelessWidget with AppLoaderMixin {
),
),
portalFollower: const AdvancedSearchFilterOverlay(),
child: QuickSearchInputForm<PresentationEmail, EmailAddress, RecentSearch>(
maxHeight: 52,
suggestionsBoxVerticalOffset: 0.0,
minInputLengthAutocomplete: _dashBoardController.minInputLengthAutocomplete,
textFieldConfiguration: _createConfiguration(context),
suggestionsBoxDecoration: const QuickSearchSuggestionsBoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(16)),
),
debounceDuration: const Duration(milliseconds: 300),
listActionButton: const [
QuickSearchFilter.hasAttachment,
QuickSearchFilter.last7Days,
QuickSearchFilter.fromMe,
QuickSearchFilter.starred,
],
actionButtonBuilder: (context, filterAction, suggestionsListState) {
if (filterAction is QuickSearchFilter) {
return buildListButtonForQuickSearchForm(
context,
filterAction,
suggestionsListState);
} else {
return const SizedBox.shrink();
}
},
buttonActionCallback: (filterAction) {
if (filterAction is QuickSearchFilter) {
_searchController.addQuickSearchFilterToSuggestionSearchView(filterAction);
}
},
listActionPadding: const EdgeInsets.only(left: 12, right: 12, top: 12, bottom: 6),
titleHeaderRecent: Padding(
padding: const EdgeInsets.only(left: 12, right: 12, bottom: 8, top: 12),
child: Text(
AppLocalizations.of(context).recent,
style: ThemeUtils.defaultTextStyleInterFont.copyWith(
fontSize: 13.0,
color: AppColor.colorTextButtonHeaderThread,
fontWeight: FontWeight.w500
)
)
),
buttonShowAllResult: (context, keyword, _) {
if (keyword is String) {
return _buildShowAllResultButton(context, keyword);
} else {
return const SizedBox.shrink();
}
},
loadingBuilder: (context) => Padding(
padding: const EdgeInsets.only(bottom: 16),
child: loadingWidget
),
fetchRecentActionCallback: (pattern) {
final accountId = _dashBoardController.accountId.value;
final userName = _dashBoardController.sessionCurrent?.username;
if (accountId == null || userName == null) {
logError('SearchInputFormWidget::fetchRecentActionCallback: accountId or userName is null');
return <RecentSearch>[];
} else {
return _searchController.getAllRecentSearchAction(
accountId,
userName,
pattern,
);
}
},
itemRecentBuilder: (context, recent) => RecentSearchItemTileWidget(recent),
onRecentSelected: _invokeSelectRecentItem,
suggestionsCallback: _dashBoardController.quickSearchEmails,
itemBuilder: (context, email) => EmailQuickSearchItemTileWidget(
email,
_dashBoardController.selectedMailbox.value,
searchQuery: SearchQuery(_searchController.currentSearchText.trim())),
onSuggestionSelected: _invokeSelectSuggestionItem,
contactItemBuilder: (context, emailAddress) => ContactQuickSearchItem(emailAddress: emailAddress),
contactSuggestionsCallback: _dashBoardController.getContactSuggestion,
onContactSuggestionSelected: _invokeSelectContactSuggestion,
)
child: searchInputForm,
),
);
});