Skip to content

Commit 331adf9

Browse files
committed
Support keyboard shortcut actions ESC to close composer on web
Signed-off-by: dab246 <[email protected]>
1 parent 9d04ac7 commit 331adf9

11 files changed

+151
-47
lines changed

lib/features/base/shortcut/app_shortcut_manager.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import 'package:core/utils/app_logger.dart';
22
import 'package:flutter/services.dart';
33
import 'package:tmail_ui_user/features/base/extensions/logical_key_set_helper.dart';
4+
import 'package:tmail_ui_user/features/base/shortcut/composer/composer_action_shortcut_type.dart';
45
import 'package:tmail_ui_user/features/base/shortcut/mail/mail_list_action_shortcut_type.dart';
56
import 'package:tmail_ui_user/features/base/shortcut/mail/mail_view_action_shortcut_type.dart';
67
import 'package:tmail_ui_user/features/base/shortcut/search/search_action_shortcut_type.dart';
78

89
class AppShortcutManager {
10+
static const int escapeKeyCode = 27;
11+
912
static MailViewActionShortcutType? getMailViewActionFromEvent(KeyEvent event) {
1013
final keysPressed = HardwareKeyboard.instance.logicalKeysPressed;
1114
log('AppShortcutManager::getMailViewActionFromEvent: Keys pressed: $keysPressed');
@@ -53,4 +56,23 @@ class AppShortcutManager {
5356
return null;
5457
}
5558
}
59+
60+
static ComposerActionShortcutType? getComposerActionFromEvent(KeyEvent event) {
61+
final keysPressed = HardwareKeyboard.instance.logicalKeysPressed;
62+
log('AppShortcutManager::getComposerActionFromEvent: Keys pressed: $keysPressed');
63+
if (keysPressed.isOnly(LogicalKeyboardKey.escape)) {
64+
return ComposerActionShortcutType.closeView;
65+
} else {
66+
return null;
67+
}
68+
}
69+
70+
static ComposerActionShortcutType? getComposerActionFromKeyCode(int? keyCode) {
71+
log('AppShortcutManager::getComposerActionFromKeyCode: Keys pressed: $keyCode');
72+
if (keyCode == escapeKeyCode) {
73+
return ComposerActionShortcutType.closeView;
74+
} else {
75+
return null;
76+
}
77+
}
5678
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
enum ComposerActionShortcutType {
3+
closeView;
4+
}

lib/features/composer/presentation/composer_controller.dart

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import 'package:tmail_ui_user/features/composer/presentation/extensions/auto_cre
5858
import 'package:tmail_ui_user/features/composer/presentation/extensions/get_draft_mailbox_id_for_composer_extension.dart';
5959
import 'package:tmail_ui_user/features/composer/presentation/extensions/get_outbox_mailbox_id_for_composer_extension.dart';
6060
import 'package:tmail_ui_user/features/composer/presentation/extensions/get_sent_mailbox_id_for_composer_extension.dart';
61+
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_keyboard_shortcut_actions_extension.dart';
6162
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_message_failure_extension.dart';
6263
import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart';
6364
import 'package:tmail_ui_user/features/composer/presentation/extensions/sanitize_signature_in_email_content_extension.dart';
@@ -205,6 +206,7 @@ class ComposerController extends BaseController
205206
FocusNode? ccAddressFocusNodeKeyboard;
206207
FocusNode? bccAddressFocusNodeKeyboard;
207208
FocusNode? replyToAddressFocusNodeKeyboard;
209+
FocusNode? keyboardShortcutFocusNode;
208210

209211
StreamSubscription<html.Event>? _subscriptionOnDragEnter;
210212
StreamSubscription<html.Event>? _subscriptionOnDragOver;
@@ -251,7 +253,6 @@ class ComposerController extends BaseController
251253
TransformHtmlEmailContentInteractor get transformHtmlEmailContentInteractor => _transformHtmlEmailContentInteractor;
252254

253255
late Worker uploadInlineImageWorker;
254-
late Worker dashboardViewStateWorker;
255256
late bool _isEmailBodyLoaded;
256257

257258
ComposerController(
@@ -292,6 +293,7 @@ class ComposerController extends BaseController
292293
_listenStreamEvent();
293294
_beforeReconnectManager.addListener(onBeforeReconnect);
294295
_injectBinding();
296+
onKeyboardShortcutInit();
295297
}
296298

297299
@override
@@ -335,6 +337,7 @@ class ComposerController extends BaseController
335337
} else {
336338
richTextMobileTabletController = null;
337339
}
340+
onKeyboardShortcutDispose();
338341
super.onClose();
339342
}
340343

@@ -366,7 +369,6 @@ class ComposerController extends BaseController
366369
bccEmailAddressController.dispose();
367370
replyToEmailAddressController.dispose();
368371
uploadInlineImageWorker.dispose();
369-
dashboardViewStateWorker.dispose();
370372
scrollController.dispose();
371373
scrollControllerEmailAddress.removeListener(_scrollControllerEmailAddressListener);
372374
scrollControllerEmailAddress.dispose();
@@ -546,18 +548,7 @@ class ComposerController extends BaseController
546548
}
547549

548550
void _scrollControllerEmailAddressListener() {
549-
if (toEmailAddressController.text.isNotEmpty) {
550-
keyToEmailTagEditor.currentState?.closeSuggestionBox();
551-
}
552-
if (ccEmailAddressController.text.isNotEmpty) {
553-
keyCcEmailTagEditor.currentState?.closeSuggestionBox();
554-
}
555-
if (bccEmailAddressController.text.isNotEmpty) {
556-
keyBccEmailTagEditor.currentState?.closeSuggestionBox();
557-
}
558-
if (replyToEmailAddressController.text.isNotEmpty) {
559-
keyReplyToEmailTagEditor.currentState?.closeSuggestionBox();
560-
}
551+
_closeSuggestionBox();
561552
}
562553

563554
void createFocusNodeInput() {
@@ -767,7 +758,7 @@ class ComposerController extends BaseController
767758
}
768759
_sendButtonState = ButtonState.disabled;
769760

770-
clearFocus(context);
761+
clearFocus();
771762

772763
if (toEmailAddressController.text.isNotEmpty
773764
|| ccEmailAddressController.text.isNotEmpty
@@ -1076,7 +1067,7 @@ class ComposerController extends BaseController
10761067
}
10771068

10781069
void openPickAttachmentMenu(BuildContext context, List<Widget> actionTiles) {
1079-
clearFocus(context);
1070+
clearFocus();
10801071

10811072
(ContextMenuBuilder(context)
10821073
..addHeader((ContextMenuHeaderBuilder(const Key('attachment_picker_context_menu_header_builder'))
@@ -1351,12 +1342,23 @@ class ComposerController extends BaseController
13511342
}
13521343
}
13531344

1354-
void clearFocus(BuildContext context) {
1355-
log('ComposerController::clearFocus:');
1345+
void clearFocus() {
13561346
if (PlatformInfo.isMobile) {
13571347
htmlEditorApi?.unfocus();
1348+
FocusManager.instance.primaryFocus?.unfocus();
1349+
} else {
1350+
toAddressFocusNode?.unfocus();
1351+
ccAddressFocusNode?.unfocus();
1352+
bccAddressFocusNode?.unfocus();
1353+
replyToAddressFocusNode?.unfocus();
1354+
}
1355+
}
1356+
1357+
void onClickOutsideComposer() {
1358+
clearFocus();
1359+
if (PlatformInfo.isWeb) {
1360+
refocusKeyboardShortcutFocus();
13581361
}
1359-
FocusScope.of(context).unfocus();
13601362
}
13611363

13621364
void _closeComposerAction({dynamic result, bool closeOverlays = false}) {
@@ -1606,8 +1608,8 @@ class ComposerController extends BaseController
16061608
}
16071609
}
16081610

1609-
void insertImage(BuildContext context, double maxWith) async {
1610-
clearFocus(context);
1611+
void insertImage(BuildContext context, double maxWith) {
1612+
clearFocus();
16111613

16121614
if (responsiveUtils.isMobile(context)) {
16131615
maxWithEditor = maxWith - 40;
@@ -1644,8 +1646,8 @@ class ComposerController extends BaseController
16441646
}
16451647
}
16461648

1647-
void handleClickDeleteComposer(BuildContext context) {
1648-
clearFocus(context);
1649+
void handleClickDeleteComposer() {
1650+
clearFocus();
16491651
_closeComposerAction();
16501652
}
16511653

@@ -1894,28 +1896,27 @@ class ComposerController extends BaseController
18941896
}
18951897

18961898
_closeComposerButtonState = ButtonState.disabled;
1899+
autoCreateEmailTag();
18971900

18981901
if (_validateCloseComposerWithoutSave()) {
18991902
log('ComposerController::handleClickCloseComposer: ARGUMENTS is NULL or EMAIL NOT LOADED');
19001903
_closeComposerButtonState = ButtonState.enabled;
1901-
clearFocus(context);
1904+
clearFocus();
19021905
_closeComposerAction();
19031906
return;
19041907
}
19051908

19061909
final isChanged = await _validateEmailChange();
19071910

19081911
if (isChanged && context.mounted) {
1909-
clearFocus(context);
1912+
clearFocus();
19101913
await _showConfirmDialogSaveMessage(context);
19111914
return;
19121915
}
19131916

1914-
if (context.mounted) {
1915-
_closeComposerButtonState = ButtonState.enabled;
1916-
clearFocus(context);
1917-
_closeComposerAction();
1918-
}
1917+
_closeComposerButtonState = ButtonState.enabled;
1918+
clearFocus();
1919+
_closeComposerAction();
19191920
}
19201921

19211922
bool _validateCloseComposerWithoutSave() {

lib/features/composer/presentation/composer_view.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class ComposerView extends GetWidget<ComposerController> {
4141
responsiveUtils: controller.responsiveUtils,
4242
mobile: MobileContainerView(
4343
onCloseViewAction: () => controller.handleClickCloseComposer(context),
44-
onClearFocusAction: () => controller.clearFocus(context),
44+
onClearFocusAction: controller.clearFocus,
4545
backgroundColor: MobileAppBarComposerWidgetStyle.backgroundColor,
4646
childBuilder: (context, constraints) => SafeArea(
4747
left: !controller.responsiveUtils.isLandscapeMobile(context),
@@ -269,7 +269,7 @@ class ComposerView extends GetWidget<ComposerController> {
269269
if (controller.isContentHeightExceeded.isTrue && PlatformInfo.isIOS) {
270270
return ViewEntireMessageWithMessageClippedWidget(
271271
buttonActionName: AppLocalizations.of(context).viewEntireMessage.toUpperCase(),
272-
onViewEntireMessageAction: () => controller.viewEntireContent(context),
272+
onViewEntireMessageAction: controller.viewEntireContent,
273273
topPadding: 12,
274274
);
275275
} else {
@@ -289,7 +289,7 @@ class ComposerView extends GetWidget<ComposerController> {
289289
tablet: TabletContainerView(
290290
keyboardRichTextController: controller.richTextMobileTabletController!.richTextController,
291291
onCloseViewAction: () => controller.handleClickCloseComposer(context),
292-
onClearFocusAction: () => controller.clearFocus(context),
292+
onClearFocusAction: controller.clearFocus,
293293
childBuilder: (context, constraints) => Container(
294294
color: ComposerStyle.mobileBackgroundColor,
295295
child: Column(
@@ -463,7 +463,7 @@ class ComposerView extends GetWidget<ComposerController> {
463463
if (controller.isContentHeightExceeded.isTrue && PlatformInfo.isIOS) {
464464
return ViewEntireMessageWithMessageClippedWidget(
465465
buttonActionName: AppLocalizations.of(context).viewEntireMessage.toUpperCase(),
466-
onViewEntireMessageAction: () => controller.viewEntireContent(context),
466+
onViewEntireMessageAction: controller.viewEntireContent,
467467
topPadding: 12,
468468
);
469469
} else {
@@ -479,7 +479,7 @@ class ComposerView extends GetWidget<ComposerController> {
479479
imagePaths: controller.imagePaths,
480480
hasReadReceipt: controller.hasRequestReadReceipt.value,
481481
isMarkAsImportant: controller.isMarkAsImportant.value,
482-
deleteComposerAction: () => controller.handleClickDeleteComposer(context),
482+
deleteComposerAction: controller.handleClickDeleteComposer,
483483
saveToDraftAction: () => controller.handleClickSaveAsDraftsButton(context),
484484
sendMessageAction: () => controller.handleClickSendButton(context),
485485
requestReadReceiptAction: () => controller.toggleRequestReadReceipt(context),

lib/features/composer/presentation/composer_view_web.dart

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import 'package:flutter/material.dart';
44
import 'package:get/get.dart';
55
import 'package:model/email/prefix_email_address.dart';
66
import 'package:pointer_interceptor/pointer_interceptor.dart';
7+
import 'package:tmail_ui_user/features/base/widget/keyboard/keyboard_handler_wrapper.dart';
78
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
89
import 'package:tmail_ui_user/features/composer/presentation/extensions/composer_print_draft_extension.dart';
910
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_edit_recipient_extension.dart';
11+
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_keyboard_shortcut_actions_extension.dart';
1012
import 'package:tmail_ui_user/features/composer/presentation/extensions/mark_as_important_extension.dart';
1113
import 'package:tmail_ui_user/features/composer/presentation/extensions/remove_draggable_email_address_between_recipient_fields_extension.dart';
1214
import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart';
@@ -39,12 +41,12 @@ class ComposerView extends GetWidget<ComposerController> {
3941

4042
@override
4143
Widget build(BuildContext context) {
42-
return ResponsiveWidget(
44+
final bodyWidget = ResponsiveWidget(
4345
responsiveUtils: controller.responsiveUtils,
4446
mobile: MobileResponsiveContainerView(
4547
childBuilder: (context, constraints) {
4648
return GestureDetector(
47-
onTap: () => controller.clearFocus(context),
49+
onTap: controller.onClickOutsideComposer,
4850
child: Column(
4951
crossAxisAlignment: CrossAxisAlignment.start,
5052
children: [
@@ -68,7 +70,7 @@ class ComposerView extends GetWidget<ComposerController> {
6870
toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context),
6971
saveToDraftsAction: () => controller.handleClickSaveAsDraftsButton(context),
7072
saveToTemplateAction: () => controller.handleClickSaveAsTemplateButton(context),
71-
deleteComposerAction: () => controller.handleClickDeleteComposer(context),
73+
deleteComposerAction: controller.handleClickDeleteComposer,
7274
)),
7375
ConstrainedBox(
7476
constraints: BoxConstraints(
@@ -252,6 +254,7 @@ class ComposerView extends GetWidget<ComposerController> {
252254
uploadError: uploadError
253255
),
254256
onInitialContentLoadComplete: controller.onInitialContentLoadCompleteWeb,
257+
onKeyDownEditorAction: controller.onKeyDownEditorAction,
255258
)),
256259
),
257260
Obx(() {
@@ -340,7 +343,7 @@ class ComposerView extends GetWidget<ComposerController> {
340343
desktop: Obx(() => DesktopResponsiveContainerView(
341344
childBuilder: (context, constraints) {
342345
return GestureDetector(
343-
onTap: () => controller.clearFocus(context),
346+
onTap: controller.onClickOutsideComposer,
344347
child: Column(children: [
345348
Obx(() => DesktopAppBarComposerWidget(
346349
imagePaths: controller.imagePaths,
@@ -543,6 +546,7 @@ class ComposerView extends GetWidget<ComposerController> {
543546
uploadError: uploadError
544547
),
545548
onInitialContentLoadComplete: controller.onInitialContentLoadCompleteWeb,
549+
onKeyDownEditorAction: controller.onKeyDownEditorAction,
546550
);
547551
}),
548552
),
@@ -585,7 +589,7 @@ class ComposerView extends GetWidget<ComposerController> {
585589
openRichToolbarAction: controller.richTextWebController!.toggleFormattingOptions,
586590
attachFileAction: () => controller.openFilePickerByType(context, FileType.any),
587591
insertImageAction: () => controller.insertImage(context, constraints.maxWidth),
588-
deleteComposerAction: () => controller.handleClickDeleteComposer(context),
592+
deleteComposerAction: controller.handleClickDeleteComposer,
589593
saveToDraftAction: () => controller.handleClickSaveAsDraftsButton(context),
590594
sendMessageAction: () => controller.handleClickSendButton(context),
591595
isEmailChanged: controller.isEmailChanged.isTrue,
@@ -662,7 +666,7 @@ class ComposerView extends GetWidget<ComposerController> {
662666
tablet: TabletResponsiveContainerView(
663667
childBuilder: (context, constraints) {
664668
return GestureDetector(
665-
onTap: () => controller.clearFocus(context),
669+
onTap: controller.onClickOutsideComposer,
666670
child: Column(children: [
667671
Obx(() => DesktopAppBarComposerWidget(
668672
imagePaths: controller.imagePaths,
@@ -862,6 +866,7 @@ class ComposerView extends GetWidget<ComposerController> {
862866
uploadError: uploadError
863867
),
864868
onInitialContentLoadComplete: controller.onInitialContentLoadCompleteWeb,
869+
onKeyDownEditorAction: controller.onKeyDownEditorAction,
865870
)),
866871
),
867872
Obx(() {
@@ -903,7 +908,7 @@ class ComposerView extends GetWidget<ComposerController> {
903908
openRichToolbarAction: controller.richTextWebController!.toggleFormattingOptions,
904909
attachFileAction: () => controller.openFilePickerByType(context, FileType.any),
905910
insertImageAction: () => controller.insertImage(context, constraints.maxWidth),
906-
deleteComposerAction: () => controller.handleClickDeleteComposer(context),
911+
deleteComposerAction: controller.handleClickDeleteComposer,
907912
saveToDraftAction: () => controller.handleClickSaveAsDraftsButton(context),
908913
sendMessageAction: () => controller.handleClickSendButton(context),
909914
isEmailChanged: controller.isEmailChanged.isTrue,
@@ -971,5 +976,15 @@ class ComposerView extends GetWidget<ComposerController> {
971976
},
972977
)
973978
);
979+
980+
if (controller.keyboardShortcutFocusNode != null) {
981+
return KeyboardHandlerWrapper(
982+
focusNode: controller.keyboardShortcutFocusNode!,
983+
onKeyDownEventAction: controller.onKeyDownEventAction,
984+
child: bodyWidget,
985+
);
986+
} else {
987+
return bodyWidget;
988+
}
974989
}
975990
}

lib/features/composer/presentation/extensions/handle_content_height_exceeded_extension.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import 'package:core/presentation/constants/constants_ui.dart';
33
import 'package:core/presentation/extensions/color_extension.dart';
44
import 'package:core/utils/app_logger.dart';
5-
import 'package:flutter/material.dart';
65
import 'package:get/get.dart';
76
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
87
import 'package:tmail_ui_user/features/composer/presentation/view/mobile/editor_fullscreen_dialog_view.dart';
@@ -14,8 +13,8 @@ extension HandleContentHeightExceededExtension on ComposerController {
1413
isContentHeightExceeded.value = height == ConstantsUI.composerHtmlContentMaxHeight;
1514
}
1615

17-
Future<void> viewEntireContent(BuildContext context) async {
18-
clearFocus(context);
16+
Future<void> viewEntireContent() async {
17+
clearFocus();
1918

2019
final currentContent = await getContentInEditor();
2120
log('HandleContentHeightExceededExtension::showComposerFullscreen:currentContent = $currentContent');

0 commit comments

Comments
 (0)