diff --git a/ui/src/common/index.js b/ui/src/common/index.js index f64d4fe0..35622568 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 00000000..4daad26f --- /dev/null +++ b/ui/src/common/useSearchRefocus.js @@ -0,0 +1,50 @@ +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) + + useEffect(() => { + const params = new URLSearchParams(location.search) + const filterStr = params.get('filter') || '{}' + + let filter = {} + try { + filter = JSON.parse(filterStr) + } catch (e) { + // Invalid JSON, ignore + } + + const searchValue = getSearchValue(filter) + + if (prevSearchValue.current && !searchValue) { + // 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() + } + }) + } + + prevSearchValue.current = searchValue + }, [location.search]) +} diff --git a/ui/src/common/useSearchRefocus.test.js b/ui/src/common/useSearchRefocus.test.js new file mode 100644 index 00000000..2bce8320 --- /dev/null +++ b/ui/src/common/useSearchRefocus.test.js @@ -0,0 +1,84 @@ +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest' +import { renderHook } from '@testing-library/react-hooks' +import { useSearchRefocus } from './useSearchRefocus' + +const mockLocation = { search: '' } +vi.mock('react-router-dom', () => ({ + useLocation: () => mockLocation, +})) + +describe('useSearchRefocus', () => { + let container + let rafCallbacks + + beforeEach(() => { + rafCallbacks = [] + vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { + rafCallbacks.push(cb) + return rafCallbacks.length + }) + + container = document.createElement('div') + container.innerHTML = ` +