diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 856515c7b4..0614bcf167 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -201,8 +201,8 @@ more = More
[editor]
buttons.heading.tooltip = Add heading
-buttons.bold.tooltip = Add bold text
-buttons.italic.tooltip = Add italic text
+buttons.bold.tooltip = Add bold text (Ctrl+B / ⌘B)
+buttons.italic.tooltip = Add italic text (Ctrl+I / ⌘I)
buttons.quote.tooltip = Quote text
buttons.code.tooltip = Add code
buttons.link.tooltip = Add a link
diff --git a/templates/shared/combomarkdowneditor.tmpl b/templates/shared/combomarkdowneditor.tmpl
index febca5ec7f..ce1be8b48e 100644
--- a/templates/shared/combomarkdowneditor.tmpl
+++ b/templates/shared/combomarkdowneditor.tmpl
@@ -25,8 +25,8 @@ Template Attributes:
{{end}}
{{svg "octicon-heading"}}
- {{svg "octicon-bold"}}
- {{svg "octicon-italic"}}
+ {{svg "octicon-bold"}}
+ {{svg "octicon-italic"}}
{{svg "octicon-quote"}}
diff --git a/tests/e2e/markdown-editor.test.e2e.ts b/tests/e2e/markdown-editor.test.e2e.ts
index d0ab88fe26..c2d4057bc9 100644
--- a/tests/e2e/markdown-editor.test.e2e.ts
+++ b/tests/e2e/markdown-editor.test.e2e.ts
@@ -517,3 +517,36 @@ test('Multiple combo markdown: insert table', async ({page}) => {
await expect(comboboxTwo).toHaveValue('| Header | Header | Header |\n|---------|---------|---------|\n| Content | Content | Content |\n| Content | Content | Content |\n');
await save_visual(page);
});
+
+test('Markdown bold/italic toolbar and shortcut', async ({page}) => {
+ const initText = `line 1\nline 2\nline 3\nline 4`;
+
+ const response = await page.goto('/user2/repo1/issues/new');
+ expect(response?.status()).toBe(200);
+
+ const textarea = page.locator('textarea[name=content]');
+ await textarea.fill(initText);
+ await textarea.focus();
+ await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('line 1'), it.value.indexOf('line 2')));
+
+ // Cases: bold via toolbar, bold via shortcut, repeat w/ italics
+ page.locator('md-bold').click();
+ await expect(textarea).toHaveValue(`**line 1**\nline 2\nline 3\nline 4`);
+ page.locator('md-bold').click();
+ await expect(textarea).toHaveValue(`line 1\nline 2\nline 3\nline 4`);
+
+ await textarea.press('ControlOrMeta+KeyB');
+ await expect(textarea).toHaveValue(`**line 1**\nline 2\nline 3\nline 4`);
+ await textarea.press('ControlOrMeta+KeyB');
+ await expect(textarea).toHaveValue(`line 1\nline 2\nline 3\nline 4`);
+
+ page.locator('md-italic').click();
+ await expect(textarea).toHaveValue(`_line 1_\nline 2\nline 3\nline 4`);
+ page.locator('md-italic').click();
+ await expect(textarea).toHaveValue(`line 1\nline 2\nline 3\nline 4`);
+
+ await textarea.press('ControlOrMeta+KeyI');
+ await expect(textarea).toHaveValue(`_line 1_\nline 2\nline 3\nline 4`);
+ await textarea.press('ControlOrMeta+KeyI');
+ await expect(textarea).toHaveValue(`line 1\nline 2\nline 3\nline 4`);
+});
diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js
index d96628c644..37544bf10f 100644
--- a/web_src/js/features/comp/ComboMarkdownEditor.js
+++ b/web_src/js/features/comp/ComboMarkdownEditor.js
@@ -99,6 +99,12 @@ class ComboMarkdownEditor {
this.textareaMarkdownToolbar.querySelector('button[data-md-action="new-table"]')?.setAttribute('data-modal', `div[data-markdown-table-modal-id="${this.elementIdSuffix}"]`);
this.textareaMarkdownToolbar.querySelector('button[data-md-action="new-link"]')?.setAttribute('data-modal', `div[data-markdown-link-modal-id="${this.elementIdSuffix}"]`);
+ // Find all data-md-ctrl-shortcut elements in the markdown toolbar.
+ const shortcutKeys = new Map();
+ for (const el of this.textareaMarkdownToolbar.querySelectorAll('[data-md-ctrl-shortcut]')) {
+ shortcutKeys.set(el.getAttribute('data-md-ctrl-shortcut'), el);
+ }
+
// Track whether any actual input or pointer action was made after focusing, and only intercept Tab presses after that.
this.tabEnabled = false;
// This tracks whether last Tab action was ignored, and if it immediately happens *again*, lose focus.
@@ -145,6 +151,17 @@ class ComboMarkdownEditor {
if (!this.breakLine()) return; // Nothing changed, let the default handler work.
this.options?.onContentChanged?.(this, e);
e.preventDefault();
+ } else if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey) {
+ const normalizedShortcutKey = e.key.charCodeAt(0) <= 127 ?
+ // if ascii, e.key is preferred as it is agnostic to keyboard layouts (QWERTY/Dvorak)...
+ e.key.toLowerCase() :
+ // if not ascii, e.code is used to support keyboards w/ other writing systems (eg. и or ბ); "KeyB" transformed to "b" to compare against the shortcut character.
+ e.code.replace('Key', '').toLowerCase();
+ const shortcutElement = shortcutKeys.get(normalizedShortcutKey);
+ if (shortcutElement) {
+ shortcutElement.click();
+ e.preventDefault();
+ }
} else if (noModifiers) {
this.activateTabHandling();
}