This commit is contained in:
Boris Rorsvort 2026-01-25 18:21:42 +01:00
parent 36451071f4
commit 419d865811
2 changed files with 37 additions and 11 deletions

View File

@ -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

View File

@ -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 = `
<div class="RaSearchInput-input">
@ -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()
})