From 113a6565ca25187a6df60d5e1da0aec25ad15267 Mon Sep 17 00:00:00 2001 From: filipvnencak Date: Mon, 24 Aug 2026 12:38:52 +0200 Subject: [PATCH 1/3] fix(SF): add `onSearchChange` prop for global search updates and improve chip collapse logic --- src/SearchFilter/SearchFilter.tsx | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/SearchFilter/SearchFilter.tsx b/src/SearchFilter/SearchFilter.tsx index 208b2d9..9f0989e 100644 --- a/src/SearchFilter/SearchFilter.tsx +++ b/src/SearchFilter/SearchFilter.tsx @@ -39,6 +39,8 @@ export interface SearchFilterProps extends Omit void compact?: boolean // shrink the bar to 28px with smaller padding/text (left search icon stays normal) onFinish?: (filters: Filter[]) => void + // live global-search text (root-level typing / in-place search-chip editing); fires '' when cleared or committed as a chip + onSearchChange?: (search: string) => void enableGlobalSearch?: boolean globalSearchConfig?: { enableMultiple?: boolean @@ -73,6 +75,7 @@ export const SearchFilter = forwardRef( filters = [], onChange, onFinish, + onSearchChange, options = [], groupOptions = [], quickActions, @@ -119,6 +122,12 @@ export const SearchFilter = forwardRef( // index of the currently highlighted dropdown option (React state instead of browser focus) const [highlightedOptionIndex, setHighlightedOptionIndex] = useState(null) + // search is global search text only while no filter's value dropdown is open + useEffect(() => { + if (!onSearchChange) return + onSearchChange(!dropdownParentId ? search : '') + }, [search, dropdownParentId]) + const parentOption = options.find( (option) => dropdownParentId && option.id === getFilterFromId(dropdownParentId), ) @@ -279,9 +288,25 @@ export const SearchFilter = forwardRef( setDropdownParentId(null) } + // with enableMultiple: false only the newest global search chip survives a commit + const collapseGlobalSearch = (next: Filter[]) => { + if (globalSearchConfig?.enableMultiple !== false) return next + let lastSearchIndex = -1 + next.forEach((filter, index) => { + if (getFilterFromId(filter.id) === SEARCH_FILTER_ID) lastSearchIndex = index + }) + if (lastSearchIndex === -1) return next + return next.filter( + (filter, index) => + getFilterFromId(filter.id) !== SEARCH_FILTER_ID || index === lastSearchIndex, + ) + } + const handleClose = (filters: Filter[]) => { // remove any filters that have no values - const updatedFilters = filters.filter((filter) => filter.values && filter.values.length > 0) + const updatedFilters = collapseGlobalSearch( + filters.filter((filter) => filter.values && filter.values.length > 0), + ) onChange(updatedFilters) // clear the inline search text and close the dropdown @@ -827,7 +852,7 @@ export const SearchFilter = forwardRef( ) => { if (config?.restart) { // update filters - onChange(filters) + onChange(collapseGlobalSearch(filters)) // clear chip editing state before going back to root closeSearch() // go back to the group menu when this filter belongs to one From 9fbcdf3e61f1a2fbbcb8fa8ad5490db55ee94fa4 Mon Sep 17 00:00:00 2001 From: filipvnencak Date: Mon, 24 Aug 2026 14:44:33 +0200 Subject: [PATCH 2/3] fix(SF): add `onSearchChange` prop for global search updates and improve chip collapse logic --- src/SearchFilter/SearchFilter.tsx | 36 ++++++++++++++++--------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/SearchFilter/SearchFilter.tsx b/src/SearchFilter/SearchFilter.tsx index 9f0989e..0beccdc 100644 --- a/src/SearchFilter/SearchFilter.tsx +++ b/src/SearchFilter/SearchFilter.tsx @@ -39,8 +39,8 @@ export interface SearchFilterProps extends Omit void compact?: boolean // shrink the bar to 28px with smaller padding/text (left search icon stays normal) onFinish?: (filters: Filter[]) => void - // live global-search text (root-level typing / in-place search-chip editing); fires '' when cleared or committed as a chip - onSearchChange?: (search: string) => void + // live search text plus the filter whose value dropdown is open (null at root level) + onSearchChange?: (search: string, filter: string | null) => void enableGlobalSearch?: boolean globalSearchConfig?: { enableMultiple?: boolean @@ -122,10 +122,8 @@ export const SearchFilter = forwardRef( // index of the currently highlighted dropdown option (React state instead of browser focus) const [highlightedOptionIndex, setHighlightedOptionIndex] = useState(null) - // search is global search text only while no filter's value dropdown is open useEffect(() => { - if (!onSearchChange) return - onSearchChange(!dropdownParentId ? search : '') + onSearchChange?.(search, dropdownParentId) }, [search, dropdownParentId]) const parentOption = options.find( @@ -288,17 +286,18 @@ export const SearchFilter = forwardRef( setDropdownParentId(null) } - // with enableMultiple: false only the newest global search chip survives a commit - const collapseGlobalSearch = (next: Filter[]) => { + // with enableMultiple: false a commit leaves a single global search chip: + // the one being edited when keepId names it, otherwise the newest + const collapseGlobalSearch = (next: Filter[], keepId?: string) => { if (globalSearchConfig?.enableMultiple !== false) return next - let lastSearchIndex = -1 - next.forEach((filter, index) => { - if (getFilterFromId(filter.id) === SEARCH_FILTER_ID) lastSearchIndex = index - }) - if (lastSearchIndex === -1) return next + const searchIds = next + .filter((filter) => getFilterFromId(filter.id) === SEARCH_FILTER_ID) + .map((filter) => filter.id) + if (searchIds.length < 2) return next + const keep = + keepId && searchIds.includes(keepId) ? keepId : searchIds[searchIds.length - 1] return next.filter( - (filter, index) => - getFilterFromId(filter.id) !== SEARCH_FILTER_ID || index === lastSearchIndex, + (filter) => getFilterFromId(filter.id) !== SEARCH_FILTER_ID || filter.id === keep, ) } @@ -524,9 +523,12 @@ export const SearchFilter = forwardRef( const id = editingSearchChipId if (!id) return const text = search.trim() - const updatedFilters = text - ? filters.map((f) => (f.id === id ? { ...f, values: [{ id: text, label: text }] } : f)) - : filters.filter((f) => f.id !== id) + const updatedFilters = collapseGlobalSearch( + text + ? filters.map((f) => (f.id === id ? { ...f, values: [{ id: text, label: text }] } : f)) + : filters.filter((f) => f.id !== id), + id, + ) closeSearch() onChange(updatedFilters) onFinish && onFinish(updatedFilters) From 6904a753615a92c78f4530ed8092379f18e9b734 Mon Sep 17 00:00:00 2001 From: filipvnencak Date: Tue, 25 Aug 2026 12:56:17 +0200 Subject: [PATCH 3/3] fix(SF): refactor search handling to use refs for improved state management and update logic --- src/SearchFilter/SearchFilter.tsx | 43 +++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/SearchFilter/SearchFilter.tsx b/src/SearchFilter/SearchFilter.tsx index 0beccdc..19ee065 100644 --- a/src/SearchFilter/SearchFilter.tsx +++ b/src/SearchFilter/SearchFilter.tsx @@ -122,9 +122,24 @@ export const SearchFilter = forwardRef( // index of the currently highlighted dropdown option (React state instead of browser focus) const [highlightedOptionIndex, setHighlightedOptionIndex] = useState(null) - useEffect(() => { - onSearchChange?.(search, dropdownParentId) - }, [search, dropdownParentId]) + // search text and dropdown scope both change programmatically (chip commit, menu open), + // so the ref carries the settled pair to every onSearchChange call + const searchStateRef = useRef<{ search: string; parentId: string | null }>({ + search: '', + parentId: null, + }) + + const updateSearch = (next: string) => { + searchStateRef.current.search = next + setSearch(next) + onSearchChange?.(next, searchStateRef.current.parentId) + } + + const updateDropdownParentId = (next: string | null) => { + searchStateRef.current.parentId = next + setDropdownParentId(next) + onSearchChange?.(searchStateRef.current.search, next) + } const parentOption = options.find( (option) => dropdownParentId && option.id === getFilterFromId(dropdownParentId), @@ -239,14 +254,14 @@ export const SearchFilter = forwardRef( const inlineSuggestion = suggestedOption?.label || '' const closeSearch = () => { - setSearch('') + updateSearch('') setEditingSearchChipId(null) setIsEditingExisting(false) } const openOptions = (options: Option[], parentId: string | null) => { setOptions(options) - setDropdownParentId(parentId) + updateDropdownParentId(parentId) } type OpenInitialOptionsConfig = { @@ -283,7 +298,7 @@ export const SearchFilter = forwardRef( const closeOptions = () => { setOptions(null) - setDropdownParentId(null) + updateDropdownParentId(null) } // with enableMultiple: false a commit leaves a single global search chip: @@ -331,7 +346,7 @@ export const SearchFilter = forwardRef( if (option.isGroup && option.groupItems) { openOptions(option.groupItems, option.id) - setSearch('') + updateSearch('') setTimeout(() => searchInputRef.current?.focus(), 0) return } @@ -450,7 +465,7 @@ export const SearchFilter = forwardRef( handleClose(updatedFilters) } else if (config?.restart) { // go back to the group menu when this filter belongs to one - setSearch('') + updateSearch('') setEditingSearchChipId(null) setIsEditingExisting(false) openOptionsAfterFilter(parentId, updatedFilters) @@ -488,7 +503,7 @@ export const SearchFilter = forwardRef( // parentId case: preserve editingSearchChipId — it is cleared by handleClose when done // RESET SEARCH - setSearch('') + updateSearch('') } const handleEditFilter = (id: string) => { @@ -499,7 +514,7 @@ export const SearchFilter = forwardRef( if (filter && getFilterFromId(id) === SEARCH_FILTER_ID) { const raw = filter.values?.[0]?.label || String(filter.values?.[0]?.id || '') setEditingSearchChipId(id) - setSearch(raw.replace(/%/g, '')) // strip LIKE wildcards for display + updateSearch(raw.replace(/%/g, '')) // strip LIKE wildcards for display closeOptions() return } @@ -510,9 +525,9 @@ export const SearchFilter = forwardRef( const firstCustomValue = filter?.values?.find((v) => v.isCustom) if (firstCustomValue) { - setSearch(firstCustomValue.label || String(firstCustomValue.id)) + updateSearch(firstCustomValue.label || String(firstCustomValue.id)) } else { - setSearch('') + updateSearch('') } handleEditFilterValues(id, filter) @@ -999,7 +1014,7 @@ export const SearchFilter = forwardRef( searchInputRef={editingSearchChipId === filter.id ? chipSearchRef : undefined} search={{ value: search, - onChange: (e) => setSearch(e.target.value), + onChange: (e) => updateSearch(e.target.value), onKeyDown: handleChipInputKeyDown, }} onEdit={handleEditFilter} @@ -1028,7 +1043,7 @@ export const SearchFilter = forwardRef( placeholder={filters.length ? '' : getEmptyPlaceholder(enableGlobalSearch)} onChange={(e) => { const val = e.target.value - setSearch(val) + updateSearch(val) if (val && !dropdownOptions) openInitialOptions(undefined, { filters }) }} onKeyDown={handleInputKeyDown}