From 419d865811908d20477827b0b258c74a51cef1e7 Mon Sep 17 00:00:00 2001 From: Boris Rorsvort Date: Sun, 25 Jan 2026 18:21:42 +0100 Subject: [PATCH] fixes --- ui/src/common/useSearchRefocus.js | 24 +++++++++++++++++++++--- ui/src/common/useSearchRefocus.test.js | 24 ++++++++++++++++-------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/ui/src/common/useSearchRefocus.js b/ui/src/common/useSearchRefocus.js index 4e8c22d2b..4daad26f9 100644 --- a/ui/src/common/useSearchRefocus.js +++ b/ui/src/common/useSearchRefocus.js @@ -1,6 +1,20 @@ import { useEffect, useRef } from 'react' import { useLocation } from 'react-router-dom' +// Search field names used by SearchInput across different list views: +// - 'name': AlbumList, ArtistList, LibraryList, PlayerList, RadioList, UserList +// - 'title': SongList +// - 'q': PlaylistList +// If a new list view uses a different source field, add it here. +const SEARCH_FIELDS = ['name', 'title', 'q'] + +const getSearchValue = (filter) => { + for (const field of SEARCH_FIELDS) { + if (filter[field]) return filter[field] + } + return '' +} + export const useSearchRefocus = () => { const location = useLocation() const prevSearchValue = useRef(null) @@ -16,15 +30,19 @@ export const useSearchRefocus = () => { // Invalid JSON, ignore } - const searchValue = filter.name || filter.title || filter.q || '' + const searchValue = getSearchValue(filter) if (prevSearchValue.current && !searchValue) { - setTimeout(() => { + // Use requestAnimationFrame to wait for React to finish re-rendering + // after the URL change before focusing the input + requestAnimationFrame(() => { + // Selector depends on react-admin's internal class naming. + // If react-admin changes these class names, this will need updating. const input = document.querySelector('[class*="RaSearchInput"] input') if (input) { input.focus() } - }, 100) + }) } prevSearchValue.current = searchValue diff --git a/ui/src/common/useSearchRefocus.test.js b/ui/src/common/useSearchRefocus.test.js index 20f1cfb90..2bce8320d 100644 --- a/ui/src/common/useSearchRefocus.test.js +++ b/ui/src/common/useSearchRefocus.test.js @@ -9,9 +9,15 @@ vi.mock('react-router-dom', () => ({ describe('useSearchRefocus', () => { let container + let rafCallbacks beforeEach(() => { - vi.useFakeTimers() + rafCallbacks = [] + vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { + rafCallbacks.push(cb) + return rafCallbacks.length + }) + container = document.createElement('div') container.innerHTML = `
@@ -23,10 +29,15 @@ describe('useSearchRefocus', () => { }) afterEach(() => { - vi.useRealTimers() + vi.restoreAllMocks() document.body.removeChild(container) }) + const flushRAF = () => { + rafCallbacks.forEach((cb) => cb()) + rafCallbacks = [] + } + it('focuses the input when search filter is cleared', () => { const input = container.querySelector('input') const focusSpy = vi.spyOn(input, 'focus') @@ -38,8 +49,7 @@ describe('useSearchRefocus', () => { mockLocation.search = '?filter={}' rerender() - - vi.advanceTimersByTime(100) + flushRAF() expect(focusSpy).toHaveBeenCalledTimes(1) }) @@ -53,8 +63,7 @@ describe('useSearchRefocus', () => { mockLocation.search = '?filter={}' rerender() - - vi.advanceTimersByTime(100) + flushRAF() expect(focusSpy).not.toHaveBeenCalled() }) @@ -68,8 +77,7 @@ describe('useSearchRefocus', () => { mockLocation.search = '?filter={"name":"other"}' rerender() - - vi.advanceTimersByTime(100) + flushRAF() expect(focusSpy).not.toHaveBeenCalled() })