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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+47
@@ -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,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user