diff --git a/src/components/Poll/PollOptionSelector.tsx b/src/components/Poll/PollOptionSelector.tsx index 20d304b05d..eecbce4bba 100644 --- a/src/components/Poll/PollOptionSelector.tsx +++ b/src/components/Poll/PollOptionSelector.tsx @@ -1,6 +1,5 @@ import clsx from 'clsx'; -import debounce from 'lodash.debounce'; -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import type { PollOptionResponseData, PollState, @@ -91,17 +90,13 @@ export const PollOptionSelector = ({ ? vote_counts_by_option[maxVotedOptionIds[0]] : 0; - const toggleVote = useMemo( - () => - debounce(() => { - if (!canCastVote) return; - const haveVotedForTheOption = !!ownVotesByOptionId[option.id]; - return haveVotedForTheOption - ? poll.removeVote(ownVotesByOptionId[option.id].id, message.id) - : poll.castVote(option.id, message.id); - }, 100), - [canCastVote, message.id, option.id, ownVotesByOptionId, poll], - ); + const toggleVote = useCallback(() => { + if (!canCastVote) return; + const haveVotedForTheOption = !!ownVotesByOptionId[option.id]; + return haveVotedForTheOption + ? poll.removeVote(ownVotesByOptionId[option.id].id, message.id) + : poll.castVote(option.id, message.id); + }, [canCastVote, message.id, option.id, ownVotesByOptionId, poll]); const avatarDisplayInfo = useMemo( () => @@ -126,6 +121,8 @@ export const PollOptionSelector = ({ ? (event) => { if (event.key !== 'Enter' && event.key !== ' ') return; event.preventDefault(); + // a held key would otherwise toggle the vote on every auto-repeat + if (event.repeat) return; toggleVote(); } : undefined diff --git a/src/components/Poll/__tests__/PollOptionList.test.tsx b/src/components/Poll/__tests__/PollOptionList.test.tsx index 7e141d0ea3..2279805abf 100644 --- a/src/components/Poll/__tests__/PollOptionList.test.tsx +++ b/src/components/Poll/__tests__/PollOptionList.test.tsx @@ -326,6 +326,51 @@ describe('PollOptionList', () => { }); }); + it('ignores auto-repeated key presses on a held key', async () => { + const poll = new Poll({ + client: fromPartial({}), + poll: pollWithNoVotes, + }); + const castVoteSpy = vi.spyOn(poll, 'castVote').mockResolvedValue(fromPartial({})); + const removeVoteSpy = vi.spyOn(poll, 'removeVote').mockResolvedValue(fromPartial({})); + + const { container } = renderComponent({ poll }); + const firstOption = container.querySelector(VOTABLE_OPTION_SELECTOR) as HTMLElement; + + act(() => { + fireEvent.keyDown(firstOption, { key: 'Enter' }); + fireEvent.keyDown(firstOption, { key: 'Enter', repeat: true }); + fireEvent.keyDown(firstOption, { key: ' ', repeat: true }); + }); + + await waitFor(() => { + expect(castVoteSpy).toHaveBeenCalledTimes(1); + }); + expect(removeVoteSpy).not.toHaveBeenCalled(); + }); + + it('sends the vote right away on click', () => { + const poll = new Poll({ + client: fromPartial({}), + poll: pollWithNoVotes, + }); + const castVoteSpy = vi.spyOn(poll, 'castVote').mockResolvedValue(fromPartial({})); + + const { container } = renderComponent({ poll }); + const firstOption = container.querySelector(VOTABLE_OPTION_SELECTOR) as HTMLElement; + + act(() => { + fireEvent.click(firstOption); + }); + + // checked synchronously: a debounced or otherwise delayed vote would not be sent yet + expect(castVoteSpy).toHaveBeenCalledTimes(1); + expect(castVoteSpy).toHaveBeenCalledWith( + pollWithNoVotes.options[0].id, + defaultMessageContext.message.id, + ); + }); + it('passes axe on the default poll option list', async () => { const poll = new Poll({ client: fromPartial({}), poll: generatePoll() }); const { container } = renderComponent({ poll });