diff --git a/ui/src/common/index.js b/ui/src/common/index.js index f64d4fe0c..356225680 100644 --- a/ui/src/common/index.js +++ b/ui/src/common/index.js @@ -41,3 +41,4 @@ export * from './formatRange.js' export * from './playlistUtils.js' export * from './PathField.jsx' export * from './ParticipantsInfo' +export * from './useSearchRefocus' diff --git a/ui/src/common/useSearchRefocus.js b/ui/src/common/useSearchRefocus.js new file mode 100644 index 000000000..27898b969 --- /dev/null +++ b/ui/src/common/useSearchRefocus.js @@ -0,0 +1,19 @@ +import { useEffect } from 'react' + +export const useSearchRefocus = () => { + useEffect(() => { + const handleClick = (e) => { + const clearButton = e.target.closest('[aria-label*="clear" i]') + if (clearButton) { + setTimeout(() => { + const searchInput = document.querySelector('[class*="RaSearchInput"] input') + if (searchInput) { + searchInput.focus() + } + }, 800) + } + } + document.addEventListener('click', handleClick, true) + return () => document.removeEventListener('click', handleClick, true) + }, []) +} diff --git a/ui/src/common/useSearchRefocus.test.js b/ui/src/common/useSearchRefocus.test.js new file mode 100644 index 000000000..8498633fa --- /dev/null +++ b/ui/src/common/useSearchRefocus.test.js @@ -0,0 +1,65 @@ +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest' +import { renderHook } from '@testing-library/react-hooks' +import { useSearchRefocus } from './useSearchRefocus' + +describe('useSearchRefocus', () => { + let container + + beforeEach(() => { + vi.useFakeTimers() + container = document.createElement('div') + container.innerHTML = ` +
+ + +
+ ` + document.body.appendChild(container) + }) + + afterEach(() => { + vi.useRealTimers() + document.body.removeChild(container) + }) + + it('focuses the input after clicking clear button', () => { + renderHook(() => useSearchRefocus()) + + const clearButton = container.querySelector('[aria-label="clear search"]') + const input = container.querySelector('input') + const focusSpy = vi.spyOn(input, 'focus') + + clearButton.click() + + expect(focusSpy).not.toHaveBeenCalled() + + vi.advanceTimersByTime(600) + + expect(focusSpy).toHaveBeenCalledTimes(1) + }) + + it('does not focus if click is not on a clear button', () => { + renderHook(() => useSearchRefocus()) + + const input = container.querySelector('input') + const focusSpy = vi.spyOn(input, 'focus') + + input.click() + + vi.advanceTimersByTime(600) + + expect(focusSpy).not.toHaveBeenCalled() + }) + + it('cleans up event listener on unmount', () => { + const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener') + + const { unmount } = renderHook(() => useSearchRefocus()) + unmount() + + expect(removeEventListenerSpy).toHaveBeenCalledWith( + 'click', + expect.any(Function), + ) + }) +}) diff --git a/ui/src/layout/Layout.jsx b/ui/src/layout/Layout.jsx index e3f13d25f..44cf9b42c 100644 --- a/ui/src/layout/Layout.jsx +++ b/ui/src/layout/Layout.jsx @@ -7,6 +7,7 @@ import Menu from './Menu' import AppBar from './AppBar' import Notification from './Notification' import useCurrentTheme from '../themes/useCurrentTheme' +import { useSearchRefocus } from '../common' const useStyles = makeStyles({ root: { paddingBottom: (props) => (props.addPadding ? '80px' : 0) }, @@ -17,6 +18,7 @@ const Layout = (props) => { const queue = useSelector((state) => state.player?.queue) const classes = useStyles({ addPadding: queue.length > 0 }) const dispatch = useDispatch() + useSearchRefocus() const keyHandlers = { TOGGLE_MENU: useCallback(() => dispatch(toggleSidebar()), [dispatch]),