mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-06-18 22:48:32 +08:00
Currently our keyboard shortcut modal (triggered by the `?` key or with
the keyboard icon at the bottom of the nav menu) has some issues that
makes it difficult to use with a screenreader. I'm addressing a number
of these issues here.
There are no significant visual changes to the keyboard shortcut modal
here, it's all structural/functional.
Markup
- moving from unordered lists back to tables — this is tabular data, so
a table is the most natural way to navigate it. Each row now has `<td
class="shortcut-description">` and `<td
class="shortcut-key">`, plus a screereader-only `<thead>` with
"Description" & "Key" column labels
- each table is associated with its category heading via aria-labelledby
so screenreader users get category context when landing inside a table.
- added an explicit aria-label in addition to the existing placeholder
Search
- `buildShortcut` now returns `{ shortcut, shortcutText, description }`
instead of one HTML string... the HTML structure was getting in the way
so searching `g` was working but not `gh`
- added `SEARCH_ALIASES` for modifier keys (alt, ctrl, cmd, meta,
windows enter/return, etc)
modifier keys so users can find shortcuts regardless of the icon used
for the shortcut (especially useful on macOS).
- filter splits the query into whitespace tokens so every token must
match somewhere, this handles "gh", "g h", "command /", "ctrl alt f",
and multi-word descriptions
- removed per-keystroke `.trim()` because it broke using the space bar
Translations
- Shortcuts are now separate, this avoids awkward ordering in some
languages
Added relevant specs for search to make sure this keeps working in the
future.
449 lines
13 KiB
JavaScript
Vendored
449 lines
13 KiB
JavaScript
Vendored
import {
|
|
click,
|
|
currentURL,
|
|
fillIn,
|
|
focus,
|
|
triggerKeyEvent,
|
|
visit,
|
|
} from "@ember/test-helpers";
|
|
import { test } from "qunit";
|
|
import { cloneJSON } from "discourse/lib/object";
|
|
import { PLATFORM_KEY_MODIFIER } from "discourse/services/keyboard-shortcuts";
|
|
import topicFixtures from "discourse/tests/fixtures/topic";
|
|
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
|
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
|
import { i18n } from "discourse-i18n";
|
|
import TemplatesFixtures from "../fixtures/templates-fixtures";
|
|
|
|
function templatesPretender(server, helper) {
|
|
const repliesPath = "/discourse_templates";
|
|
const replies = TemplatesFixtures[repliesPath];
|
|
|
|
server.get(repliesPath, () => helper.response(replies));
|
|
replies.templates.forEach((template) =>
|
|
server.post(`${repliesPath}/${template.id}/use`, () => helper.response({}))
|
|
);
|
|
}
|
|
|
|
async function selectCategory() {
|
|
const categoryChooser = selectKit(".category-chooser");
|
|
await categoryChooser.expand();
|
|
await categoryChooser.selectRowByValue(2);
|
|
}
|
|
|
|
acceptance("discourse-templates", function (needs) {
|
|
needs.settings({
|
|
discourse_templates_enabled: true,
|
|
allow_uncategorized_topics: true,
|
|
tagging_enabled: true,
|
|
});
|
|
needs.user({
|
|
can_use_templates: true,
|
|
});
|
|
|
|
needs.pretender(templatesPretender);
|
|
|
|
test("Filtering by tags", async function (assert) {
|
|
await visit("/");
|
|
|
|
await click("#create-topic");
|
|
await selectCategory();
|
|
await click(".toolbar-menu__options-trigger");
|
|
await click(`button[title="${i18n("templates.insert_template")}"]`);
|
|
|
|
const tagDropdown = selectKit(".templates-filter-bar .tag-drop");
|
|
await tagDropdown.expand();
|
|
|
|
await tagDropdown.fillInFilter(
|
|
"cupcake",
|
|
".templates-filter-bar .tag-drop input"
|
|
);
|
|
assert.deepEqual(
|
|
tagDropdown.displayedContent(),
|
|
[
|
|
{
|
|
name: "cupcakes",
|
|
id: "cupcakes",
|
|
},
|
|
],
|
|
"filters tags in the dropdown"
|
|
);
|
|
|
|
await tagDropdown.selectRowByIndex(0);
|
|
assert
|
|
.dom(".templates-list .template-item")
|
|
.exists({ count: 1 }, "filters replies by tag");
|
|
|
|
await click("#template-item-1 .templates-apply");
|
|
|
|
assert
|
|
.dom(".d-editor-input")
|
|
.includesValue(
|
|
"Cupcake ipsum dolor sit amet cotton candy cheesecake jelly. Candy canes sugar plum soufflé sweet roll jelly-o danish jelly muffin. I love jelly-o powder topping carrot cake toffee.",
|
|
"inserts the template in the composer"
|
|
);
|
|
});
|
|
|
|
test("Filtering by text", async function (assert) {
|
|
await visit("/");
|
|
|
|
await click("#create-topic");
|
|
await selectCategory();
|
|
await click(".toolbar-menu__options-trigger");
|
|
await click(`button[title="${i18n("templates.insert_template")}"]`);
|
|
|
|
await fillIn(".templates-filter-bar input.templates-filter", "test");
|
|
assert
|
|
.dom(".templates-list .template-item")
|
|
.exists({ count: 2 }, "filters by text");
|
|
|
|
await click("#template-item-8 .templates-apply");
|
|
|
|
assert
|
|
.dom(".d-editor-input")
|
|
.includesValue(
|
|
"Testing testin **123**",
|
|
"inserts the template in the composer"
|
|
);
|
|
});
|
|
|
|
test("Replacing variables", async function (assert) {
|
|
await visit("/");
|
|
|
|
await click("#create-topic");
|
|
await selectCategory();
|
|
await click(".toolbar-menu__options-trigger");
|
|
await click(`button[title="${i18n("templates.insert_template")}"]`);
|
|
|
|
await click("#template-item-9 .templates-apply");
|
|
|
|
assert
|
|
.dom(".d-editor-input")
|
|
.includesValue("Hi there, regards eviltrout.", "replaces variables");
|
|
});
|
|
|
|
test("Navigate to source", async function (assert) {
|
|
await visit("/");
|
|
|
|
await click("#create-topic");
|
|
await selectCategory();
|
|
await click(".toolbar-menu__options-trigger");
|
|
await click(`button[title="${i18n("templates.insert_template")}"]`);
|
|
|
|
const tagDropdown = selectKit(".templates-filter-bar .tag-drop");
|
|
await tagDropdown.expand();
|
|
|
|
await tagDropdown.fillInFilter(
|
|
"lorem",
|
|
".templates-filter-bar .tag-drop input"
|
|
);
|
|
assert.deepEqual(
|
|
tagDropdown.displayedContent(),
|
|
[
|
|
{
|
|
name: "lorem",
|
|
id: "lorem",
|
|
},
|
|
],
|
|
"filters tags in the dropdown"
|
|
);
|
|
|
|
await tagDropdown.selectRowByIndex(0);
|
|
assert
|
|
.dom(".templates-list .template-item")
|
|
.exists({ count: 1 }, "filters replies by tag");
|
|
|
|
await click("#template-item-130 .template-item-title");
|
|
await click("#template-item-130 .template-item-source-link");
|
|
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
"/t/lorem-ipsum-dolor-sit-amet/130",
|
|
"navigates to the source"
|
|
);
|
|
});
|
|
|
|
test("Has ordering by relevance, usage, and title", async function (assert) {
|
|
await visit("/");
|
|
|
|
await click("#create-topic");
|
|
await selectCategory();
|
|
await click(".toolbar-menu__options-trigger");
|
|
await click(`button[title="${i18n("templates.insert_template")}"]`);
|
|
|
|
await fillIn(".templates-filter-bar input.templates-filter", "ipsum");
|
|
const templateItems = document.querySelectorAll(
|
|
".templates-list .template-item-title-text"
|
|
);
|
|
const titles = Array.from(templateItems).map((el) => el.textContent.trim());
|
|
|
|
assert.deepEqual(
|
|
titles,
|
|
[
|
|
"Cupcake Ipsum excerpt",
|
|
"Hipster ipsum excerpt",
|
|
"Liquor ipsum excerpt",
|
|
"Mussum Ipsum excerpt",
|
|
"Lorem ipsum dolor sit amet",
|
|
],
|
|
"orders templates by relevance, usage, and title"
|
|
);
|
|
});
|
|
|
|
test("Remembers selected tag between openings", async function (assert) {
|
|
await visit("/");
|
|
|
|
await click("#create-topic");
|
|
await selectCategory();
|
|
await click(".toolbar-menu__options-trigger");
|
|
await click(`button[title="${i18n("templates.insert_template")}"]`);
|
|
|
|
const tagDropdown = selectKit(".templates-filter-bar .tag-drop");
|
|
await tagDropdown.expand();
|
|
|
|
await tagDropdown.fillInFilter(
|
|
"cupcake",
|
|
".templates-filter-bar .tag-drop input"
|
|
);
|
|
await tagDropdown.selectRowByIndex(0);
|
|
assert.dom(".templates-list .template-item").exists({ count: 1 });
|
|
|
|
await click("#reply-control .toggle-save-and-close");
|
|
|
|
await click("#create-topic");
|
|
await selectCategory();
|
|
await click(".toolbar-menu__options-trigger");
|
|
await click(`button[title="${i18n("templates.insert_template")}"]`);
|
|
assert
|
|
.dom(".templates-list .template-item")
|
|
.exists({ count: 1 }, "preserves selected tag across composer sessions");
|
|
});
|
|
});
|
|
|
|
acceptance("with tags disabled in Settings", function (needs) {
|
|
needs.settings({
|
|
discourse_templates_enabled: true,
|
|
tagging_enabled: false,
|
|
});
|
|
needs.user({
|
|
can_use_templates: true,
|
|
});
|
|
|
|
needs.pretender(templatesPretender);
|
|
|
|
test("Filtering by tags", async function (assert) {
|
|
await visit("/");
|
|
|
|
await click("#create-topic");
|
|
await selectCategory();
|
|
await click(".toolbar-menu__options-trigger");
|
|
await click(`button[title="${i18n("templates.insert_template")}"]`);
|
|
|
|
assert
|
|
.dom(".templates-filter-bar .tag-drop")
|
|
.doesNotExist("tag drop down is not displayed");
|
|
});
|
|
});
|
|
|
|
acceptance("keyboard shortcut", function (needs) {
|
|
needs.settings({
|
|
discourse_templates_enabled: true,
|
|
tagging_enabled: true,
|
|
});
|
|
needs.user({
|
|
can_use_templates: true,
|
|
});
|
|
|
|
needs.pretender(templatesPretender);
|
|
|
|
const triggerKeyboardShortcut = async () => {
|
|
// Testing keyboard events is tough!
|
|
const isMac = PLATFORM_KEY_MODIFIER.toLowerCase() === "meta";
|
|
await triggerKeyEvent(document, "keydown", "I", {
|
|
...(isMac ? { metaKey: true } : { ctrlKey: true }),
|
|
shiftKey: true,
|
|
});
|
|
};
|
|
|
|
const assertTemplateWasInserted = async (assert, selector) => {
|
|
const tagDropdown = selectKit(".templates-filter-bar .tag-drop");
|
|
await tagDropdown.expand();
|
|
|
|
await tagDropdown.fillInFilter(
|
|
"cupcake",
|
|
".templates-filter-bar .tag-drop input"
|
|
);
|
|
await tagDropdown.selectRowByIndex(0);
|
|
await click("#template-item-1 .templates-apply");
|
|
|
|
assert
|
|
.dom(selector)
|
|
.includesValue(
|
|
"Cupcake ipsum dolor sit amet cotton candy cheesecake jelly. Candy canes sugar plum soufflé sweet roll jelly-o danish jelly muffin. I love jelly-o powder topping carrot cake toffee.",
|
|
"inserts the template in the textarea"
|
|
);
|
|
};
|
|
|
|
test("Help | Added shortcut to help modal", async function (assert) {
|
|
await visit("/");
|
|
await triggerKeyEvent(document, "keypress", "?".charCodeAt(0));
|
|
|
|
assert.dom(".shortcut-category-templates").exists();
|
|
assert.dom(".shortcut-category-templates tbody tr").exists({ count: 1 });
|
|
});
|
|
|
|
test("Composer | Title field focused | Template is inserted", async function (assert) {
|
|
await visit("/");
|
|
|
|
await click("#create-topic");
|
|
await selectCategory();
|
|
|
|
await triggerKeyboardShortcut();
|
|
await assertTemplateWasInserted(assert, ".d-editor-input");
|
|
});
|
|
|
|
test("Composer | Textarea focused | Template is inserted", async function (assert) {
|
|
await visit("/");
|
|
|
|
await click("#create-topic");
|
|
await selectCategory();
|
|
|
|
await focus(".d-editor-input");
|
|
|
|
await triggerKeyboardShortcut();
|
|
await assertTemplateWasInserted(assert, ".d-editor-input");
|
|
});
|
|
|
|
test("Modal | Templates modal | Show the modal if the preview is hidden", async function (assert) {
|
|
await visit("/");
|
|
|
|
await click("#create-topic");
|
|
await selectCategory();
|
|
|
|
await click(".toggle-preview");
|
|
|
|
await focus(".d-editor-input");
|
|
|
|
await triggerKeyboardShortcut();
|
|
assert
|
|
.dom(".d-modal.d-templates")
|
|
.exists("displays the standard templates modal");
|
|
});
|
|
|
|
test("Modal | Templates modal | Show the modal if a textarea is focused", async function (assert) {
|
|
// if the text area is outside a modal then simply show the insert template modal
|
|
// because there is no need to hijack
|
|
await visit("/u/charlie/preferences/profile");
|
|
|
|
await focus(".d-editor-input");
|
|
|
|
await triggerKeyboardShortcut();
|
|
assert
|
|
.dom(".d-modal.d-templates")
|
|
.exists("displays the standard templates modal");
|
|
});
|
|
|
|
test("Modal | Templates modal | Template is inserted", async function (assert) {
|
|
await visit("/u/charlie/preferences/profile");
|
|
|
|
await focus(".d-editor-input");
|
|
|
|
await triggerKeyboardShortcut();
|
|
await assertTemplateWasInserted(assert, ".d-editor-input");
|
|
});
|
|
|
|
test("Modal | Templates modal | Template is inserted", async function (assert) {
|
|
await visit("/u/charlie/preferences/profile");
|
|
|
|
await focus(".d-editor-input");
|
|
|
|
await triggerKeyboardShortcut();
|
|
|
|
const tagDropdown = selectKit(".templates-filter-bar .tag-drop");
|
|
await tagDropdown.expand();
|
|
|
|
await tagDropdown.fillInFilter(
|
|
"lorem",
|
|
".templates-filter-bar .tag-drop input"
|
|
);
|
|
assert.deepEqual(
|
|
tagDropdown.displayedContent(),
|
|
[
|
|
{
|
|
name: "lorem",
|
|
id: "lorem",
|
|
},
|
|
],
|
|
"filters tags in the dropdown"
|
|
);
|
|
|
|
await tagDropdown.selectRowByIndex(0);
|
|
assert
|
|
.dom(".templates-list .template-item")
|
|
.exists({ count: 1 }, "filters replies by tag");
|
|
|
|
await click("#template-item-130 .template-item-title");
|
|
await click("#template-item-130 .template-item-source-link");
|
|
|
|
assert.strictEqual(
|
|
currentURL(),
|
|
"/t/lorem-ipsum-dolor-sit-amet/130",
|
|
"navigates to the source"
|
|
);
|
|
});
|
|
|
|
test("Modal | Templates Modal | Stacked Modals | Template is inserted", async function (assert) {
|
|
await visit("/t/topic-for-group-moderators/2480");
|
|
await click(".show-more-actions");
|
|
await click(".show-post-admin-menu");
|
|
await click(".add-notice");
|
|
|
|
await focus(".d-modal__body textarea");
|
|
|
|
await triggerKeyboardShortcut();
|
|
await assertTemplateWasInserted(assert, ".d-modal__body textarea");
|
|
});
|
|
|
|
test("Modal | Templates Modal | Stacked Modals | Closing the template modal returns the focus to the original modal textarea", async function (assert) {
|
|
await visit("/t/topic-for-group-moderators/2480");
|
|
await click(".show-more-actions");
|
|
await click(".show-post-admin-menu");
|
|
await click(".add-notice");
|
|
|
|
await focus(".d-modal__body textarea");
|
|
assert
|
|
.dom(".d-templates-modal")
|
|
.doesNotExist("the templates modal does not exist yet");
|
|
await triggerKeyboardShortcut();
|
|
assert.dom(".d-templates-modal").exists("displays the templates modal");
|
|
|
|
await click(".d-templates-modal .btn.modal-close");
|
|
assert
|
|
.dom(".d-modal__body textarea")
|
|
.isFocused(
|
|
"focuses the original textarea again after closing the templates modal"
|
|
);
|
|
});
|
|
});
|
|
|
|
acceptance("buttons on topics", function (needs) {
|
|
needs.user();
|
|
needs.settings({
|
|
allow_uncategorized_topics: true,
|
|
});
|
|
|
|
needs.pretender((server, helper) => {
|
|
const topicResponse = cloneJSON(topicFixtures["/t/280/1.json"]);
|
|
topicResponse.is_template = true;
|
|
|
|
server.get("/t/280.json", () => helper.response(topicResponse));
|
|
server.get("/raw/280/1", () => [200, {}, "post raw content"]);
|
|
});
|
|
|
|
test("Can open composer using button on topic", async function (assert) {
|
|
await visit("/t/280");
|
|
|
|
await click(".template-new-topic");
|
|
assert.dom("textarea.d-editor-input").hasValue("post raw content");
|
|
});
|
|
});
|