diff --git a/apps/readest-app/src/__tests__/components/Dropdown.test.tsx b/apps/readest-app/src/__tests__/components/Dropdown.test.tsx new file mode 100644 index 00000000..f783547f --- /dev/null +++ b/apps/readest-app/src/__tests__/components/Dropdown.test.tsx @@ -0,0 +1,94 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { render, screen, cleanup, fireEvent } from '@testing-library/react'; + +import Dropdown from '@/components/Dropdown'; +import { DropdownProvider } from '@/context/DropdownContext'; + +vi.mock('@/hooks/useTranslation', () => ({ + useTranslation: () => (s: string) => s, +})); + +afterEach(() => cleanup()); + +// Simulates the browser behavior where pressing Enter/Space on a focused +// + + + , + ); + +describe('Dropdown keyboard activation', () => { + it('opens when Enter is pressed on the toggle button', () => { + renderDropdown(); + const toggle = screen.getByRole('button', { name: 'Test Menu' }); + + toggle.focus(); + pressActivationKey(toggle, 'Enter'); + + expect(toggle.getAttribute('aria-expanded')).toBe('true'); + }); + + it('opens when Space is pressed on the toggle button', () => { + renderDropdown(); + const toggle = screen.getByRole('button', { name: 'Test Menu' }); + + toggle.focus(); + pressActivationKey(toggle, ' '); + + expect(toggle.getAttribute('aria-expanded')).toBe('true'); + }); + + it('closes when Enter is pressed again on an open dropdown', () => { + renderDropdown(); + const toggle = screen.getByRole('button', { name: 'Test Menu' }); + + toggle.focus(); + pressActivationKey(toggle, 'Enter'); + expect(toggle.getAttribute('aria-expanded')).toBe('true'); + + pressActivationKey(toggle, 'Enter'); + expect(toggle.getAttribute('aria-expanded')).toBe('false'); + }); + + it('stops propagation on Enter to prevent global shortcuts from firing', () => { + renderDropdown(); + const toggle = screen.getByRole('button', { name: 'Test Menu' }); + + const onWindowKeyDown = vi.fn(); + window.addEventListener('keydown', onWindowKeyDown); + try { + toggle.focus(); + fireEvent.keyDown(toggle, { key: 'Enter' }); + expect(onWindowKeyDown).not.toHaveBeenCalled(); + } finally { + window.removeEventListener('keydown', onWindowKeyDown); + } + }); + + it('stops propagation on Space to prevent global shortcuts from firing', () => { + renderDropdown(); + const toggle = screen.getByRole('button', { name: 'Test Menu' }); + + const onWindowKeyDown = vi.fn(); + window.addEventListener('keydown', onWindowKeyDown); + try { + toggle.focus(); + fireEvent.keyDown(toggle, { key: ' ' }); + expect(onWindowKeyDown).not.toHaveBeenCalled(); + } finally { + window.removeEventListener('keydown', onWindowKeyDown); + } + }); +}); diff --git a/apps/readest-app/src/components/Dropdown.tsx b/apps/readest-app/src/components/Dropdown.tsx index 2795cde5..e397f5fc 100644 --- a/apps/readest-app/src/components/Dropdown.tsx +++ b/apps/readest-app/src/components/Dropdown.tsx @@ -96,7 +96,11 @@ const Dropdown: React.FC = ({ const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { - if (!isOpen) setIsDropdownOpen(true); + // Let the native button click (dispatched by the browser for Enter/Space + // on a focused button) drive the toggle — toggling here would race with + // that click and cancel it out. We still stop propagation so global + // shortcuts bound to Enter/Space (e.g. next page in the reader) don't + // fire while a dropdown button is focused. e.stopPropagation(); } else if (e.key === 'Escape') { setIsDropdownOpen(false); diff --git a/apps/readest-app/src/components/MenuItem.tsx b/apps/readest-app/src/components/MenuItem.tsx index 051d223c..6240fd80 100644 --- a/apps/readest-app/src/components/MenuItem.tsx +++ b/apps/readest-app/src/components/MenuItem.tsx @@ -111,7 +111,7 @@ const MenuItem: React.FC = ({
setIsDetailsOpen(e.currentTarget.open)}>