shortcuts: Add Alt + Esc to cancel inputting an accelerator

Screenshot: screenshot.googleplex.com/BvXzPPQPsMQyAAr

Bug: b/216049298
Test: browser_tests
Change-Id: I6abe77508ab20fa3e3dcefb8e085eba68980bc48
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4764266
Reviewed-by: Michael Checo <michaelcheco@google.com>
Commit-Queue: Jimmy Gong <jimmyxgong@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1182204}
diff --git a/ash/webui/shortcut_customization_ui/resources/js/accelerator_view.ts b/ash/webui/shortcut_customization_ui/resources/js/accelerator_view.ts
index ce8753fd..46db152 100644
--- a/ash/webui/shortcut_customization_ui/resources/js/accelerator_view.ts
+++ b/ash/webui/shortcut_customization_ui/resources/js/accelerator_view.ts
@@ -46,6 +46,8 @@
 // change to the backend.
 const kAnimationTimeoutMs: number = 300;
 
+const kEscapeKey: number = 27;  // Keycode for VKEY_ESCAPE
+
 /**
  * @fileoverview
  * 'accelerator-view' is wrapper component for an accelerator. It maintains both
@@ -261,10 +263,18 @@
       this.hasError = false;
     }
 
+    const pendingAccelerator = this.keystrokeToAccelerator(e);
+    // Alt + Esc will exit input handling immediately.
+    if (pendingAccelerator.modifiers === Modifier.ALT &&
+        pendingAccelerator.keyCode === kEscapeKey) {
+      this.endCapture(/*shouldDelay=*/ false);
+      return;
+    }
+
     // Add the key pressed to pendingAccelerator.
     this.set(
         'pendingAcceleratorInfo.layoutProperties.standardAccelerator.accelerator',
-        this.keystrokeToAccelerator(e));
+        pendingAccelerator);
 
     if (this.isModifierKey(e)) {
       // Reset the keyDisplay property if the key is a modifier.
diff --git a/chrome/test/data/webui/chromeos/shortcut_customization/accelerator_edit_dialog_test.ts b/chrome/test/data/webui/chromeos/shortcut_customization/accelerator_edit_dialog_test.ts
index fcd8d04..ec10848f 100644
--- a/chrome/test/data/webui/chromeos/shortcut_customization/accelerator_edit_dialog_test.ts
+++ b/chrome/test/data/webui/chromeos/shortcut_customization/accelerator_edit_dialog_test.ts
@@ -173,7 +173,8 @@
     const acceleratorElements =
         dialog.querySelectorAll('accelerator-edit-view');
     const expectedHintMessage =
-        'Press 1-4 modifiers and 1 other key on your keyboard';
+        'Press 1-4 modifiers and 1 other key on your keyboard. To exit ' +
+        'editing mode, press alt + esc.';
     const statusMessageElement = strictQuery(
         '#acceleratorInfoText', acceleratorElements[0]!.shadowRoot,
         HTMLDivElement);
diff --git a/chrome/test/data/webui/chromeos/shortcut_customization/accelerator_edit_view_test.ts b/chrome/test/data/webui/chromeos/shortcut_customization/accelerator_edit_view_test.ts
index a7b5269..5e99868 100644
--- a/chrome/test/data/webui/chromeos/shortcut_customization/accelerator_edit_view_test.ts
+++ b/chrome/test/data/webui/chromeos/shortcut_customization/accelerator_edit_view_test.ts
@@ -218,7 +218,8 @@
 
     // Input hint message should be shown.
     const expectedHintMessage =
-        'Press 1-4 modifiers and 1 other key on your keyboard';
+        'Press 1-4 modifiers and 1 other key on your keyboard. To exit ' +
+        'editing mode, press alt + esc.';
     const statusMessageElement = strictQuery(
         '#acceleratorInfoText', editViewElement!.shadowRoot, HTMLDivElement);
     assertEquals(expectedHintMessage, statusMessageElement.textContent!.trim());
diff --git a/chrome/test/data/webui/chromeos/shortcut_customization/accelerator_view_test.ts b/chrome/test/data/webui/chromeos/shortcut_customization/accelerator_view_test.ts
index 5025c5f..398ab2f3 100644
--- a/chrome/test/data/webui/chromeos/shortcut_customization/accelerator_view_test.ts
+++ b/chrome/test/data/webui/chromeos/shortcut_customization/accelerator_view_test.ts
@@ -17,7 +17,7 @@
 import {setShortcutProviderForTesting} from 'chrome://shortcut-customization/js/mojo_interface_provider.js';
 import {AcceleratorConfigResult, AcceleratorSource, LayoutStyle, Modifier} from 'chrome://shortcut-customization/js/shortcut_types.js';
 import {AcceleratorResultData} from 'chrome://shortcut-customization/mojom-webui/ash/webui/shortcut_customization_ui/mojom/shortcut_customization.mojom-webui.js';
-import {assertEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
+import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
 import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
 import {isVisible} from 'chrome://webui-test/test_util.js';
 
@@ -563,4 +563,87 @@
     assertTrue(!!viewContainer.ariaLabel);
     assertTrue(regex.test(viewContainer.ariaLabel));
   });
+
+  test('CancelInputWithShortcut', async () => {
+    viewElement = initAcceleratorViewElement();
+    await flushTasks();
+
+    const acceleratorInfo = createStandardAcceleratorInfo(
+        Modifier.ALT,
+        /*key=*/ 221,
+        /*keyDisplay=*/ ']');
+
+    viewElement.acceleratorInfo = acceleratorInfo;
+    viewElement.source = AcceleratorSource.kAsh;
+    viewElement.action = 1;
+    // Enable the edit view.
+    viewElement.viewState = ViewState.EDIT;
+
+    await flush();
+
+    // Assert that this is in the EDIT state.
+    assertEquals(ViewState.EDIT, viewElement.viewState);
+
+    let ctrlKey = getInputKey('#ctrlKey');
+    let altKey = getInputKey('#altKey');
+    let shiftKey = getInputKey('#shiftKey');
+    let metaKey = getInputKey('#searchKey');
+    let pendingKey = getInputKey('#pendingKey');
+
+    // By default, no keys should be registered.
+    assertEquals(KeyInputState.NOT_SELECTED, ctrlKey.keyState);
+    assertEquals(KeyInputState.NOT_SELECTED, altKey.keyState);
+    assertEquals(KeyInputState.NOT_SELECTED, shiftKey.keyState);
+    assertEquals(KeyInputState.NOT_SELECTED, metaKey.keyState);
+    assertEquals(KeyInputState.NOT_SELECTED, pendingKey.keyState);
+    assertEquals('key', pendingKey.key);
+
+    // Simulate Alt.
+    viewElement.dispatchEvent(new KeyboardEvent('keydown', {
+      key: 'Alt',
+      keyCode: 18,
+      code: 'Alt',
+      ctrlKey: false,
+      altKey: true,
+      shiftKey: false,
+      metaKey: false,
+    }));
+
+    await flush();
+
+    assertEquals(KeyInputState.NOT_SELECTED, ctrlKey.keyState);
+    assertEquals(KeyInputState.MODIFIER_SELECTED, altKey.keyState);
+    assertEquals(KeyInputState.NOT_SELECTED, shiftKey.keyState);
+    assertEquals(KeyInputState.NOT_SELECTED, metaKey.keyState);
+    assertEquals(KeyInputState.NOT_SELECTED, pendingKey.keyState);
+
+    // Now press Escape.
+    viewElement.dispatchEvent(new KeyboardEvent('keydown', {
+      key: 'Escape',
+      keyCode: 27,
+      code: 'Escape',
+      ctrlKey: false,
+      altKey: true,
+      shiftKey: false,
+      metaKey: false,
+    }));
+
+    await flush();
+    ctrlKey = getInputKey('#ctrlKey');
+    altKey = getInputKey('#altKey');
+    shiftKey = getInputKey('#shiftKey');
+    metaKey = getInputKey('#searchKey');
+    pendingKey = getInputKey('#pendingKey');
+
+    assertEquals(KeyInputState.NOT_SELECTED, ctrlKey.keyState);
+    assertEquals(KeyInputState.MODIFIER_SELECTED, altKey.keyState);
+    assertEquals(KeyInputState.NOT_SELECTED, shiftKey.keyState);
+    assertEquals(KeyInputState.NOT_SELECTED, metaKey.keyState);
+    assertEquals(KeyInputState.NOT_SELECTED, pendingKey.keyState);
+
+    // Expect that press Alt + Esc will cancel the edit state.
+    assertEquals(ViewState.VIEW, viewElement.viewState);
+    assertFalse(viewElement.hasError);
+    assertEquals('', viewElement.statusMessage);
+  });
 });
diff --git a/chromeos/chromeos_strings.grd b/chromeos/chromeos_strings.grd
index f95b1da..b358dd04 100644
--- a/chromeos/chromeos_strings.grd
+++ b/chromeos/chromeos_strings.grd
@@ -4024,7 +4024,7 @@
           Cancel
         </message>
         <message name="IDS_SHORTCUT_CUSTOMIZATION_EDIT_VIEW_STATUS_MESSAGE" desc="Instructions to user when they're inputting a new shortcut">
-          Press 1-4 modifiers and 1 other key on your keyboard
+          Press 1-4 modifiers and 1 other key on your keyboard. To exit editing mode, press alt + esc.
         </message>
         <message name="IDS_SHORTCUT_CUSTOMIZATION_RESTORE_DEFAULT_ERROR_MESSAGE" desc="Description text shown to users that their default shortcut is used by another shortcut action">
           Shortcut is being used for "<ph name="CONFLICT_ACCEL_NAME">$1<ex>BRIGHTNESS_UP</ex></ph>". Edit or remove to resolve the conflict.
diff --git a/chromeos/chromeos_strings_grd/IDS_SHORTCUT_CUSTOMIZATION_EDIT_VIEW_STATUS_MESSAGE.png.sha1 b/chromeos/chromeos_strings_grd/IDS_SHORTCUT_CUSTOMIZATION_EDIT_VIEW_STATUS_MESSAGE.png.sha1
index c61fb2c..9cb5f64 100644
--- a/chromeos/chromeos_strings_grd/IDS_SHORTCUT_CUSTOMIZATION_EDIT_VIEW_STATUS_MESSAGE.png.sha1
+++ b/chromeos/chromeos_strings_grd/IDS_SHORTCUT_CUSTOMIZATION_EDIT_VIEW_STATUS_MESSAGE.png.sha1
@@ -1 +1 @@
-06f3c03a67b3c9556e45d899c3415446a4ba04f6
\ No newline at end of file
+aeb4e4b019f1d6db044659d0df63649b6d3109e5
\ No newline at end of file