From 5d362ca99013fdc7a0f66a7ea0643639ea42e9d6 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:59:23 +0300 Subject: [PATCH] Fix Plus voting hotkeys firing while typing or using shortcuts --- .../plus-voting/core/usePlusVoting.ts | 20 ++++++++++++ ...-10-16-plus-voting-hotkeys-while-typing.md | 5 +++ e2e/pages/plus/voting-page.ts | 9 ++++++ e2e/pages/search/global-search-dialog.ts | 18 +++++++++++ e2e/plus.spec.ts | 32 +++++++++++++++++++ 5 files changed, 84 insertions(+) create mode 100644 changelog/2026-10-16-plus-voting-hotkeys-while-typing.md diff --git a/app/features/plus-voting/core/usePlusVoting.ts b/app/features/plus-voting/core/usePlusVoting.ts index d56f31935..82b75dd08 100644 --- a/app/features/plus-voting/core/usePlusVoting.ts +++ b/app/features/plus-voting/core/usePlusVoting.ts @@ -127,6 +127,8 @@ function resolveInitialVotingState( function useVoteWithKeysEffect(vote: (type: "upvote" | "downvote") => void) { React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { + if (!isVoteHotkey(e)) return; + if (e.code === "KeyS") { vote("downvote"); } else if (e.code === "KeyK") { @@ -142,6 +144,24 @@ function useVoteWithKeysEffect(vote: (type: "upvote" | "downvote") => void) { }, [vote]); } +/** A bare key press not meant for a shortcut, a text field or an open dialog. */ +function isVoteHotkey(e: KeyboardEvent) { + if (e.repeat || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) { + return false; + } + + const target = e.target; + if (!(target instanceof HTMLElement)) return true; + + return !( + target.isContentEditable || + target instanceof HTMLInputElement || + target instanceof HTMLTextAreaElement || + target instanceof HTMLSelectElement || + target.closest("[popover], dialog") + ); +} + function previousUser({ usersForVoting, votes, diff --git a/changelog/2026-10-16-plus-voting-hotkeys-while-typing.md b/changelog/2026-10-16-plus-voting-hotkeys-while-typing.md new file mode 100644 index 000000000..f899e957c --- /dev/null +++ b/changelog/2026-10-16-plus-voting-hotkeys-while-typing.md @@ -0,0 +1,5 @@ +--- +navItem: plus +type: bug +--- +Plus Server voting no longer casts votes when typing in search or using keyboard shortcuts like Ctrl+K diff --git a/e2e/pages/plus/voting-page.ts b/e2e/pages/plus/voting-page.ts index f45a60b0d..8958fc27a 100644 --- a/e2e/pages/plus/voting-page.ts +++ b/e2e/pages/plus/voting-page.ts @@ -21,10 +21,19 @@ export class PlusVotingPage { await navigate({ page: this.page, url: PLUS_VOTING_PAGE }); } + progress({ voted, total }: { voted: number; total: number }) { + return this.page.getByTitle(`Voting progress ${voted} out of ${total}`); + } + async upvoteCurrent() { await this.locators.upvoteButton.click(); } + /** Votes with the desktop keyboard shortcuts: S for -1, K for +1. */ + async voteWithKey(type: "upvote" | "downvote") { + await this.page.keyboard.press(type === "upvote" ? "k" : "s"); + } + async submitVotes() { await submit(this.page, this.locators.submitVotesButton); } diff --git a/e2e/pages/search/global-search-dialog.ts b/e2e/pages/search/global-search-dialog.ts index 8616ca38a..94cd419ff 100644 --- a/e2e/pages/search/global-search-dialog.ts +++ b/e2e/pages/search/global-search-dialog.ts @@ -31,6 +31,19 @@ export class GlobalSearchDialog { await this.locators.dialog.waitFor({ state: "visible" }); } + /** Desktop Chrome's user agent is not a Mac's, so the shortcut is Ctrl+K. */ + async openWithShortcut() { + await this.page.keyboard.press("Control+k"); + await this.locators.dialog.waitFor({ state: "visible" }); + } + + async close() { + // Escape on a filled search input clears it instead of closing the dialog + await this.locators.input.clear(); + await this.page.keyboard.press("Escape"); + await this.locators.dialog.waitFor({ state: "hidden" }); + } + /** Which kind of entity the query searches; remembered across dialog openings. */ async selectType(type: keyof typeof TYPE_LABELS) { await this.locators.dialog.getByText(TYPE_LABELS[type]).click(); @@ -40,6 +53,11 @@ export class GlobalSearchDialog { await this.locators.input.fill(query); } + /** Types key by key, firing keydown events unlike {@link search}. */ + async type(query: string) { + await this.locators.input.pressSequentially(query); + } + async selectOption(name: string) { const option = this.option(name); await option.waitFor({ state: "visible" }); diff --git a/e2e/plus.spec.ts b/e2e/plus.spec.ts index d96110a09..b5b7823b4 100644 --- a/e2e/plus.spec.ts +++ b/e2e/plus.spec.ts @@ -20,6 +20,7 @@ import { SuggestionCommentPage } from "./pages/plus/suggestion-comment-page"; import { PlusSuggestionsPage } from "./pages/plus/suggestions-page"; import { PlusVotingPage } from "./pages/plus/voting-page"; import { PlusVotingResultsPage } from "./pages/plus/voting-results-page"; +import { GlobalSearchDialog } from "./pages/search/global-search-dialog"; const SUGGESTED_NAME = "SuggestedSue"; const SUGGESTED_DISCORD_ID = "1000000000000000001"; @@ -208,4 +209,35 @@ test.describe("Plus Server", () => { await expect(voting.locators.votingOngoingInfo).toBeVisible(); await isNotVisible(voting.locators.upvoteButton); }); + + test("votes with the S and K keys, but not while using global search", async ({ + page, + factories, + }) => { + await factories.UserFactory.grant(ADMIN_ID, { plusTier: 1 }); + await factories.UserFactory.grant(NZAP_TEST_ID, { plusTier: 1 }); + for (const user of await factories.UserFactory.createMany(2)) { + await factories.UserFactory.grant(user.id, { plusTier: 1 }); + } + + await impersonate(page); + await setPlusVotingActive(page, true); + + const voting = new PlusVotingPage(page); + await voting.goto(); + await expect(voting.progress({ voted: 0, total: 3 })).toBeVisible(); + + await voting.voteWithKey("upvote"); + await expect(voting.progress({ voted: 1, total: 3 })).toBeVisible(); + + const search = new GlobalSearchDialog(page); + await search.openWithShortcut(); + await search.type("sk"); + await expect(search.locators.input).toHaveValue("sk"); + await search.close(); + await expect(voting.progress({ voted: 1, total: 3 })).toBeVisible(); + + await voting.voteWithKey("downvote"); + await expect(voting.progress({ voted: 2, total: 3 })).toBeVisible(); + }); });