diff --git a/app/assets/javascripts/discourse/app/components/d-modal-body.js b/app/assets/javascripts/discourse/app/components/d-modal-body.js index 4231de24df..1b51515aea 100644 --- a/app/assets/javascripts/discourse/app/components/d-modal-body.js +++ b/app/assets/javascripts/discourse/app/components/d-modal-body.js @@ -5,6 +5,8 @@ export default Component.extend({ fixed: false, submitOnEnter: true, dismissable: true, + attributeBindings: ["tabindex"], + tabindex: -1, didInsertElement() { this._super(...arguments); diff --git a/app/assets/javascripts/discourse/app/components/d-modal.js b/app/assets/javascripts/discourse/app/components/d-modal.js index 941a0ca556..3d6a6bfb48 100644 --- a/app/assets/javascripts/discourse/app/components/d-modal.js +++ b/app/assets/javascripts/discourse/app/components/d-modal.js @@ -185,9 +185,14 @@ export default Component.extend({ !autofocusedElement || document.activeElement !== autofocusedElement ) { - innerContainer - .querySelectorAll(focusableElements + ", button:not(.modal-close)")[0] - ?.focus(); + // if there's not autofocus, or the activeElement, is not the autofocusable element + // attempt to focus the first of the focusable elements or just the modal-body + // to make it possible to scroll with arrow down/up + ( + innerContainer.querySelector( + focusableElements + ", button:not(.modal-close)" + ) || innerContainer.querySelector(".modal-body") + )?.focus(); } return; diff --git a/app/assets/javascripts/discourse/app/controllers/keyboard-shortcuts-help.js b/app/assets/javascripts/discourse/app/controllers/keyboard-shortcuts-help.js index 0386451056..a1bcbfa432 100644 --- a/app/assets/javascripts/discourse/app/controllers/keyboard-shortcuts-help.js +++ b/app/assets/javascripts/discourse/app/controllers/keyboard-shortcuts-help.js @@ -2,17 +2,28 @@ import Controller from "@ember/controller"; import I18n from "I18n"; import { translateModKey } from "discourse/lib/utilities"; import ModalFunctionality from "discourse/mixins/modal-functionality"; +import { extraKeyboardShortcutsHelp } from "discourse/lib/keyboard-shortcuts"; const KEY = "keyboard_shortcuts_help"; - const SHIFT = I18n.t("shortcut_modifier_key.shift"); const ALT = translateModKey("Alt"); +const META = translateModKey("Meta"); const CTRL = I18n.t("shortcut_modifier_key.ctrl"); const ENTER = I18n.t("shortcut_modifier_key.enter"); const COMMA = I18n.t(`${KEY}.shortcut_key_delimiter_comma`); const PLUS = I18n.t(`${KEY}.shortcut_key_delimiter_plus`); +const translationForExtraShortcuts = { + shift: SHIFT, + alt: ALT, + meta: META, + ctrl: CTRL, + enter: ENTER, + comma: COMMA, + plus: PLUS, +}; + function buildHTML(keys1, keys2, keysDelimiter, shortcutsDelimiter) { const allKeys = [keys1, keys2] .reject((keys) => keys.length === 0) @@ -59,184 +70,272 @@ export default Controller.extend(ModalFunctionality, { }, _defineShortcuts() { - this.set("shortcuts", { - jump_to: { - home: buildShortcut("jump_to.home", { keys1: ["g", "h"] }), - latest: buildShortcut("jump_to.latest", { keys1: ["g", "l"] }), - new: buildShortcut("jump_to.new", { keys1: ["g", "n"] }), - unread: buildShortcut("jump_to.unread", { keys1: ["g", "u"] }), - categories: buildShortcut("jump_to.categories", { keys1: ["g", "c"] }), - top: buildShortcut("jump_to.top", { keys1: ["g", "t"] }), - bookmarks: buildShortcut("jump_to.bookmarks", { keys1: ["g", "b"] }), - profile: buildShortcut("jump_to.profile", { keys1: ["g", "p"] }), - messages: buildShortcut("jump_to.messages", { keys1: ["g", "m"] }), - drafts: buildShortcut("jump_to.drafts", { keys1: ["g", "d"] }), - next: buildShortcut("jump_to.next", { keys1: ["g", "j"] }), - previous: buildShortcut("jump_to.previous", { keys1: ["g", "k"] }), - }, - navigation: { - back: buildShortcut("navigation.back", { keys1: ["u"] }), - jump: buildShortcut("navigation.jump", { keys1: ["#"] }), - up_down: buildShortcut("navigation.up_down", { - keys1: ["k"], - keys2: ["j"], - shortcutsDelimiter: "slash", - }), - open: buildShortcut("navigation.open", { - keys1: ["o"], - keys2: [ENTER], - }), - next_prev: buildShortcut("navigation.next_prev", { - keys1: [SHIFT, "j"], - keys2: [SHIFT, "k"], - keysDelimiter: PLUS, - shortcutsDelimiter: "slash", - }), - go_to_unread_post: buildShortcut("navigation.go_to_unread_post", { - keys1: [SHIFT, "l"], - keysDelimiter: PLUS, - }), - }, + let shortcuts = { + jump_to: { shortcuts: this._buildJumpToSection() }, application: { - hamburger_menu: buildShortcut("application.hamburger_menu", { - keys1: ["="], - }), - user_profile_menu: buildShortcut("application.user_profile_menu", { - keys1: ["p"], - }), - create: buildShortcut("application.create", { keys1: ["c"] }), - show_incoming_updated_topics: buildShortcut( - "application.show_incoming_updated_topics", - { keys1: ["."] } - ), - search: buildShortcut("application.search", { - keys1: ["/"], - keys2: [CTRL, ALT, "f"], - keysDelimiter: PLUS, - }), - help: buildShortcut("application.help", { keys1: ["?"] }), - dismiss_new: buildShortcut("application.dismiss_new", { - keys1: ["x", "r"], - }), - dismiss_topics: buildShortcut("application.dismiss_topics", { - keys1: ["x", "t"], - }), - log_out: buildShortcut("application.log_out", { - keys1: [SHIFT, "z"], - keys2: [SHIFT, "z"], - keysDelimiter: PLUS, - shortcutsDelimiter: "space", - }), - }, - composing: { - return: buildShortcut("composing.return", { - keys1: [SHIFT, "c"], - keysDelimiter: PLUS, - }), - fullscreen: buildShortcut("composing.fullscreen", { - keys1: [SHIFT, "F11"], - keysDelimiter: PLUS, - }), - }, - bookmarks: { - enter: buildShortcut("bookmarks.enter", { keys1: [ENTER] }), - later_today: buildShortcut("bookmarks.later_today", { - keys1: ["l", "t"], - shortcutsDelimiter: "space", - }), - later_this_week: buildShortcut("bookmarks.later_this_week", { - keys1: ["l", "w"], - shortcutsDelimiter: "space", - }), - tomorrow: buildShortcut("bookmarks.tomorrow", { - keys1: ["n", "d"], - shortcutsDelimiter: "space", - }), - next_business_week: buildShortcut("bookmarks.next_business_week", { - keys1: ["n", "b", "w"], - shortcutsDelimiter: "space", - }), - next_business_day: buildShortcut("bookmarks.next_business_day", { - keys1: ["n", "b", "d"], - shortcutsDelimiter: "space", - }), - custom: buildShortcut("bookmarks.custom", { - keys1: ["c", "r"], - shortcutsDelimiter: "space", - }), - none: buildShortcut("bookmarks.none", { - keys1: ["n", "r"], - shortcutsDelimiter: "space", - }), - delete: buildShortcut("bookmarks.delete", { - keys1: ["d", "d"], - shortcutsDelimiter: "space", - }), + shortcuts: { + hamburger_menu: buildShortcut("application.hamburger_menu", { + keys1: ["="], + }), + user_profile_menu: buildShortcut("application.user_profile_menu", { + keys1: ["p"], + }), + create: buildShortcut("application.create", { keys1: ["c"] }), + show_incoming_updated_topics: buildShortcut( + "application.show_incoming_updated_topics", + { keys1: ["."] } + ), + search: buildShortcut("application.search", { + keys1: ["/"], + keys2: [CTRL, ALT, "f"], + keysDelimiter: PLUS, + }), + help: buildShortcut("application.help", { keys1: ["?"] }), + dismiss_new: buildShortcut("application.dismiss_new", { + keys1: ["x", "r"], + }), + dismiss_topics: buildShortcut("application.dismiss_topics", { + keys1: ["x", "t"], + }), + log_out: buildShortcut("application.log_out", { + keys1: [SHIFT, "z"], + keys2: [SHIFT, "z"], + keysDelimiter: PLUS, + shortcutsDelimiter: "space", + }), + }, }, actions: { - bookmark_topic: buildShortcut("actions.bookmark_topic", { - keys1: ["f"], - }), - reply_as_new_topic: buildShortcut("actions.reply_as_new_topic", { - keys1: ["t"], - }), - reply_topic: buildShortcut("actions.reply_topic", { - keys1: [SHIFT, "r"], - keysDelimiter: PLUS, - }), - reply_post: buildShortcut("actions.reply_post", { keys1: ["r"] }), - quote_post: buildShortcut("actions.quote_post", { keys1: ["q"] }), - pin_unpin_topic: buildShortcut("actions.pin_unpin_topic", { - keys1: [SHIFT, "p"], - keysDelimiter: PLUS, - }), - share_topic: buildShortcut("actions.share_topic", { - keys1: [SHIFT, "s"], - keysDelimiter: PLUS, - }), - share_post: buildShortcut("actions.share_post", { keys1: ["s"] }), - like: buildShortcut("actions.like", { keys1: ["l"] }), - flag: buildShortcut("actions.flag", { keys1: ["!"] }), - bookmark: buildShortcut("actions.bookmark", { keys1: ["b"] }), - edit: buildShortcut("actions.edit", { keys1: ["e"] }), - delete: buildShortcut("actions.delete", { keys1: ["d"] }), - mark_muted: buildShortcut("actions.mark_muted", { keys1: ["m", "m"] }), - mark_regular: buildShortcut("actions.mark_regular", { - keys1: ["m", "r"], - }), - mark_tracking: buildShortcut("actions.mark_tracking", { - keys1: ["m", "t"], - }), - mark_watching: buildShortcut("actions.mark_watching", { - keys1: ["m", "w"], - }), - print: buildShortcut("actions.print", { - keys1: [translateModKey("Meta"), "p"], - keysDelimiter: PLUS, - }), - defer: buildShortcut("actions.defer", { - keys1: [SHIFT, "u"], - keysDelimiter: PLUS, - }), - topic_admin_actions: buildShortcut("actions.topic_admin_actions", { - keys1: [SHIFT, "a"], - keysDelimiter: PLUS, - }), + shortcuts: { + bookmark_topic: buildShortcut("actions.bookmark_topic", { + keys1: ["f"], + }), + reply_as_new_topic: buildShortcut("actions.reply_as_new_topic", { + keys1: ["t"], + }), + reply_topic: buildShortcut("actions.reply_topic", { + keys1: [SHIFT, "r"], + keysDelimiter: PLUS, + }), + reply_post: buildShortcut("actions.reply_post", { keys1: ["r"] }), + quote_post: buildShortcut("actions.quote_post", { keys1: ["q"] }), + pin_unpin_topic: buildShortcut("actions.pin_unpin_topic", { + keys1: [SHIFT, "p"], + keysDelimiter: PLUS, + }), + share_topic: buildShortcut("actions.share_topic", { + keys1: [SHIFT, "s"], + keysDelimiter: PLUS, + }), + share_post: buildShortcut("actions.share_post", { keys1: ["s"] }), + like: buildShortcut("actions.like", { keys1: ["l"] }), + flag: buildShortcut("actions.flag", { keys1: ["!"] }), + bookmark: buildShortcut("actions.bookmark", { keys1: ["b"] }), + edit: buildShortcut("actions.edit", { keys1: ["e"] }), + delete: buildShortcut("actions.delete", { keys1: ["d"] }), + mark_muted: buildShortcut("actions.mark_muted", { + keys1: ["m", "m"], + }), + mark_regular: buildShortcut("actions.mark_regular", { + keys1: ["m", "r"], + }), + mark_tracking: buildShortcut("actions.mark_tracking", { + keys1: ["m", "t"], + }), + mark_watching: buildShortcut("actions.mark_watching", { + keys1: ["m", "w"], + }), + print: buildShortcut("actions.print", { + keys1: [META, "p"], + keysDelimiter: PLUS, + }), + defer: buildShortcut("actions.defer", { + keys1: [SHIFT, "u"], + keysDelimiter: PLUS, + }), + topic_admin_actions: buildShortcut("actions.topic_admin_actions", { + keys1: [SHIFT, "a"], + keysDelimiter: PLUS, + }), + }, + }, + navigation: { + shortcuts: { + back: buildShortcut("navigation.back", { keys1: ["u"] }), + jump: buildShortcut("navigation.jump", { keys1: ["#"] }), + up_down: buildShortcut("navigation.up_down", { + keys1: ["k"], + keys2: ["j"], + shortcutsDelimiter: "slash", + }), + open: buildShortcut("navigation.open", { + keys1: ["o"], + keys2: [ENTER], + }), + next_prev: buildShortcut("navigation.next_prev", { + keys1: [SHIFT, "j"], + keys2: [SHIFT, "k"], + keysDelimiter: PLUS, + shortcutsDelimiter: "slash", + }), + go_to_unread_post: buildShortcut("navigation.go_to_unread_post", { + keys1: [SHIFT, "l"], + keysDelimiter: PLUS, + }), + }, + }, + composing: { + shortcuts: { + return: buildShortcut("composing.return", { + keys1: [SHIFT, "c"], + keysDelimiter: PLUS, + }), + fullscreen: buildShortcut("composing.fullscreen", { + keys1: [SHIFT, "F11"], + keysDelimiter: PLUS, + }), + }, + }, + bookmarks: { + shortcuts: { + enter: buildShortcut("bookmarks.enter", { keys1: [ENTER] }), + later_today: buildShortcut("bookmarks.later_today", { + keys1: ["l", "t"], + shortcutsDelimiter: "space", + }), + later_this_week: buildShortcut("bookmarks.later_this_week", { + keys1: ["l", "w"], + shortcutsDelimiter: "space", + }), + tomorrow: buildShortcut("bookmarks.tomorrow", { + keys1: ["n", "d"], + shortcutsDelimiter: "space", + }), + next_business_week: buildShortcut("bookmarks.next_business_week", { + keys1: ["n", "b", "w"], + shortcutsDelimiter: "space", + }), + next_business_day: buildShortcut("bookmarks.next_business_day", { + keys1: ["n", "b", "d"], + shortcutsDelimiter: "space", + }), + custom: buildShortcut("bookmarks.custom", { + keys1: ["c", "r"], + shortcutsDelimiter: "space", + }), + none: buildShortcut("bookmarks.none", { + keys1: ["n", "r"], + shortcutsDelimiter: "space", + }), + delete: buildShortcut("bookmarks.delete", { + keys1: ["d", "d"], + shortcutsDelimiter: "space", + }), + }, }, search_menu: { - prev_next: buildShortcut("search_menu.prev_next", { - keys1: ["↑"], - keys2: ["↓"], - shortcutsDelimiter: "slash", - }), - insert_url: buildShortcut("search_menu.insert_url", { - keys1: ["a"], - }), - full_page_search: buildShortcut("search_menu.full_page_search", { - keys1: [translateModKey("Meta"), "Enter"], - keysDelimiter: PLUS, - }), + shortcuts: { + prev_next: buildShortcut("search_menu.prev_next", { + keys1: ["↑"], + keys2: ["↓"], + shortcutsDelimiter: "slash", + }), + insert_url: buildShortcut("search_menu.insert_url", { + keys1: ["a"], + }), + full_page_search: buildShortcut("search_menu.full_page_search", { + keys1: [META, "Enter"], + keysDelimiter: PLUS, + }), + }, }, + }; + this._buildExtraShortcuts(shortcuts); + this._addCountsToShortcutCategories(shortcuts); + this.set("shortcuts", shortcuts); + }, + + _buildExtraShortcuts(shortcuts) { + for (const [category, helps] of Object.entries( + extraKeyboardShortcutsHelp + )) { + helps.forEach((help) => { + if (!shortcuts[category]) { + shortcuts[category] = {}; + } + + if (!shortcuts[category].shortcuts) { + shortcuts[category].shortcuts = {}; + } + + shortcuts[category].shortcuts[help.name] = buildShortcut( + help.name, + this._transformExtraDefinition(help.definition) + ); + }); + } + }, + + _addCountsToShortcutCategories(shortcuts) { + for (const [category, shortcutCategory] of Object.entries(shortcuts)) { + shortcuts[category].count = Object.keys( + shortcutCategory.shortcuts + ).length; + } + }, + + _transformExtraDefinition(definition) { + if (definition.keys1) { + definition.keys1 = definition.keys1.map((key) => + this._translateKeys(key) + ); + } + if (definition.keys2) { + definition.keys2 = definition.keys2.map((key) => + this._translateKeys(key) + ); + } + if (definition.keysDelimiter) { + definition.keysDelimiter = this._translateKeys(definition.keysDelimiter); + } + if (definition.shortcutsDelimiter) { + definition.shortcutsDelimiter = this._translateKeys( + definition.shortcutsDelimiter + ); + } + return definition; + }, + + _translateKeys(string) { + for (const [matcher, replacement] of Object.entries( + translationForExtraShortcuts + )) { + string = string.replace(matcher, replacement); + } + return string; + }, + + _buildJumpToSection() { + const shortcuts = { + home: buildShortcut("jump_to.home", { keys1: ["g", "h"] }), + latest: buildShortcut("jump_to.latest", { keys1: ["g", "l"] }), + new: buildShortcut("jump_to.new", { keys1: ["g", "n"] }), + unread: buildShortcut("jump_to.unread", { keys1: ["g", "u"] }), + categories: buildShortcut("jump_to.categories", { keys1: ["g", "c"] }), + top: buildShortcut("jump_to.top", { keys1: ["g", "t"] }), + bookmarks: buildShortcut("jump_to.bookmarks", { keys1: ["g", "b"] }), + profile: buildShortcut("jump_to.profile", { keys1: ["g", "p"] }), + }; + if (this.siteSettings.enable_personal_messages) { + shortcuts.messages = buildShortcut("jump_to.messages", { + keys1: ["g", "m"], + }); + } + Object.assign(shortcuts, { + drafts: buildShortcut("jump_to.drafts", { keys1: ["g", "d"] }), + next: buildShortcut("jump_to.next", { keys1: ["g", "j"] }), + previous: buildShortcut("jump_to.previous", { keys1: ["g", "k"] }), }); + return shortcuts; }, }); diff --git a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js index a60af675ed..7aed8878ae 100644 --- a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js +++ b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js @@ -13,6 +13,24 @@ import { INPUT_DELAY } from "discourse-common/config/environment"; import { ajax } from "discourse/lib/ajax"; import { headerOffset } from "discourse/lib/offset-calculator"; +let extraKeyboardShortcutsHelp = {}; +function addExtraKeyboardShortcutHelp(help) { + const category = help.category; + if (extraKeyboardShortcutsHelp[category]) { + extraKeyboardShortcutsHelp[category] = extraKeyboardShortcutsHelp[ + category + ].concat([help]); + } else { + extraKeyboardShortcutsHelp[category] = [help]; + } +} + +export function clearExtraKeyboardShortcutHelp() { + extraKeyboardShortcutsHelp = {}; +} + +export { extraKeyboardShortcutsHelp as extraKeyboardShortcutsHelp }; + const DEFAULT_BINDINGS = { "!": { postAction: "showFlags" }, "#": { handler: "goToPost", anonymous: true }, @@ -209,6 +227,16 @@ export default { * - path - a specific path to limit the shortcut to .e.g /latest * - postAction - binds the shortcut to fire the specified post action when a * post is selected + * - help - adds the shortcut to the keyboard shortcuts modal. `help` is an object + * with key/value pairs + * { + * category: String, + * name: String, + * definition: (See function `buildShortcut` in + * app/assets/javascripts/discourse/app/controllers/keyboard-shortcuts-help.js + * for definition structure) + * } + * * - click - allows to provide a selector on which a click event * will be triggered, eg: { click: ".topic.last .title" } **/ @@ -218,6 +246,9 @@ export default { shortcut = shortcut.trim(); let newBinding = Object.assign({ handler: callback }, opts); this.bindKey(shortcut, newBinding); + if (opts.help) { + addExtraKeyboardShortcutHelp(opts.help); + } }, // unbinds all the shortcuts in a key binding object e.g. diff --git a/app/assets/javascripts/discourse/app/templates/modal/keyboard-shortcuts-help.hbs b/app/assets/javascripts/discourse/app/templates/modal/keyboard-shortcuts-help.hbs index 9488750a37..97036766f7 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/keyboard-shortcuts-help.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/keyboard-shortcuts-help.hbs @@ -1,107 +1,12 @@ {{#d-modal-body id="keyboard-shortcuts-help"}} -