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/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_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/mail/mail_view_action_shortcut_type.dart';
|
||||||
|
import 'package:tmail_ui_user/features/base/shortcut/search/search_action_shortcut_type.dart';
|
||||||
|
|
||||||
class AppShortcutManager {
|
class AppShortcutManager {
|
||||||
static MailViewActionShortcutType? getMailViewActionFromEvent(KeyEvent event) {
|
static MailViewActionShortcutType? getMailViewActionFromEvent(KeyEvent event) {
|
||||||
@@ -42,4 +43,14 @@ class AppShortcutManager {
|
|||||||
return null;
|
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/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/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/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_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/email_sort_order_type.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/model/search/quick_search_filter.dart';
|
||||||
@@ -52,6 +53,7 @@ class SearchController extends BaseController with DateRangePickerMixin {
|
|||||||
SearchQuery? get searchQuery => searchEmailFilter.value.text;
|
SearchQuery? get searchQuery => searchEmailFilter.value.text;
|
||||||
|
|
||||||
FocusNode searchFocus = FocusNode();
|
FocusNode searchFocus = FocusNode();
|
||||||
|
FocusNode? keyboardFocusNode;
|
||||||
String currentSearchText = '';
|
String currentSearchText = '';
|
||||||
|
|
||||||
SearchController(
|
SearchController(
|
||||||
@@ -64,11 +66,17 @@ class SearchController extends BaseController with DateRangePickerMixin {
|
|||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
searchFocus.addListener(_onSearchFocusChanged);
|
searchFocus.addListener(_onSearchFocusChanged);
|
||||||
|
onKeyboardShortcutInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSearchFocusChanged() {
|
void _onSearchFocusChanged() {
|
||||||
log('SearchController::_onSearchFocusChanged: ${searchFocus.hasFocus}');
|
log('SearchController::_onSearchFocusChanged: ${searchFocus.hasFocus}');
|
||||||
isSearchInputFocused.value = searchFocus.hasFocus;
|
isSearchInputFocused.value = searchFocus.hasFocus;
|
||||||
|
if (searchFocus.hasFocus) {
|
||||||
|
refocusKeyboardShortcutFocus();
|
||||||
|
} else {
|
||||||
|
clearKeyboardShortcutFocus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void openAdvanceSearch() {
|
void openAdvanceSearch() {
|
||||||
@@ -336,6 +344,7 @@ class SearchController extends BaseController with DateRangePickerMixin {
|
|||||||
searchInputController.dispose();
|
searchInputController.dispose();
|
||||||
searchFocus.removeListener(_onSearchFocusChanged);
|
searchFocus.removeListener(_onSearchFocusChanged);
|
||||||
searchFocus.dispose();
|
searchFocus.dispose();
|
||||||
|
onKeyboardShortcutDispose();
|
||||||
super.onClose();
|
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:model/email/presentation_email.dart';
|
||||||
import 'package:pointer_interceptor/pointer_interceptor.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/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/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/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/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/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/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/advanced_search_filter_overlay.dart';
|
||||||
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/icon_open_advanced_search_widget.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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Obx(() {
|
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(
|
return PortalTarget(
|
||||||
visible: _searchController.isAdvancedSearchViewOpen.isTrue,
|
visible: _searchController.isAdvancedSearchViewOpen.isTrue,
|
||||||
portalFollower: PointerInterceptor(
|
portalFollower: PointerInterceptor(
|
||||||
@@ -73,87 +165,7 @@ class SearchInputFormWidget extends StatelessWidget with AppLoaderMixin {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
portalFollower: const AdvancedSearchFilterOverlay(),
|
portalFollower: const AdvancedSearchFilterOverlay(),
|
||||||
child: QuickSearchInputForm<PresentationEmail, EmailAddress, RecentSearch>(
|
child: searchInputForm,
|
||||||
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,
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user