mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-27 13:06:13 -05:00
Fix Plus voting hotkeys firing while typing or using shortcuts
This commit is contained in:
@@ -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,
|
||||
|
||||
5
changelog/2026-10-16-plus-voting-hotkeys-while-typing.md
Normal file
5
changelog/2026-10-16-plus-voting-hotkeys-while-typing.md
Normal file
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user