diff --git a/CHANGELOG.md b/CHANGELOG.md index 19358afd3..c160eec98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Send anonymous server-side PostHog events as personless so unauthenticated requests don't inflate person counts. [#1367](https://github.com/sourcebot-dev/sourcebot/pull/1367) - [EE] Fixed Ask Sourcebot mermaid diagrams overflowing their container by contain-fitting them to both width and height, and made revealing a diagram from the answer jump it into view instantly to avoid over/undershooting. [#1373](https://github.com/sourcebot-dev/sourcebot/pull/1373) - Passed Zoekt index parameters via argv to preserve revision names with punctuation. [#1376](https://github.com/sourcebot-dev/sourcebot/pull/1376) +- Kept selected language filters visible while narrowing the filter list. [#1377](https://github.com/sourcebot-dev/sourcebot/pull/1377) ## [5.0.4] - 2026-06-18 diff --git a/packages/web/src/app/(app)/search/components/filterPanel/entry.tsx b/packages/web/src/app/(app)/search/components/filterPanel/entry.tsx index e55cccdb4..a56a65241 100644 --- a/packages/web/src/app/(app)/search/components/filterPanel/entry.tsx +++ b/packages/web/src/app/(app)/search/components/filterPanel/entry.tsx @@ -34,9 +34,13 @@ export const Entry = ({ countText = "999+"; } return ( -
onClicked()} > -
+ {Icon ? Icon : ( )} -
+ -

{displayName}

+ {displayName}

{displayName}

-
-
-
+ + + {countText} -
-
+ + ); } diff --git a/packages/web/src/app/(app)/search/components/filterPanel/filter.test.tsx b/packages/web/src/app/(app)/search/components/filterPanel/filter.test.tsx new file mode 100644 index 000000000..cc6982799 --- /dev/null +++ b/packages/web/src/app/(app)/search/components/filterPanel/filter.test.tsx @@ -0,0 +1,92 @@ +import { afterEach, describe, expect, test, vi } from 'vitest'; +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import { TooltipProvider } from '@/components/ui/tooltip'; +import { Filter } from './filter'; +import type { Entry } from './entry'; + +afterEach(() => { + cleanup(); +}); + +const entries: Entry[] = [ + { + key: 'powershell', + displayName: 'PowerShell', + count: 4, + isSelected: true, + isHidden: false, + isDisabled: false, + }, + { + key: 'c', + displayName: 'C', + count: 9, + isSelected: false, + isHidden: false, + isDisabled: false, + }, + { + key: 'go', + displayName: 'Go', + count: 12, + isSelected: false, + isHidden: false, + isDisabled: false, + }, +]; + +describe('Filter', () => { + test('renders entries as accessible toggle buttons', () => { + const onEntryClicked = vi.fn(); + + render( + + + , + ); + + const selectedEntry = screen.getByRole('button', { name: /PowerShell/ }); + const unselectedEntry = screen.getByRole('button', { name: /C/ }); + + expect(selectedEntry.getAttribute('aria-pressed')).toBe('true'); + expect(unselectedEntry.getAttribute('aria-pressed')).toBe('false'); + + fireEvent.click(selectedEntry); + + expect(onEntryClicked).toHaveBeenCalledWith('powershell'); + }); + + test('keeps selected entries visible and pinned while filtering', () => { + render( + + undefined} + isStreaming={false} + /> + , + ); + + fireEvent.change(screen.getByPlaceholderText('Search languages'), { + target: { value: 'C' }, + }); + + const selectedEntry = screen.getByText('PowerShell'); + const matchedEntry = screen.getByText('C'); + + expect(selectedEntry).toBeTruthy(); + expect(matchedEntry).toBeTruthy(); + expect(screen.queryByText('Go')).toBeNull(); + expect( + selectedEntry.compareDocumentPosition(matchedEntry) & Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + }); +}); diff --git a/packages/web/src/app/(app)/search/components/filterPanel/filter.tsx b/packages/web/src/app/(app)/search/components/filterPanel/filter.tsx index 0a76a50e6..51de864c3 100644 --- a/packages/web/src/app/(app)/search/components/filterPanel/filter.tsx +++ b/packages/web/src/app/(app)/search/components/filterPanel/filter.tsx @@ -36,8 +36,19 @@ export const Filter = ({ threshold: 0.3, }); + const selectedEntries = entries.filter((entry) => entry.isSelected); const result = fuse.search(searchFilter); - return result.map((result) => result.item); + const filteredEntries = new Map(); + + for (const entry of selectedEntries) { + filteredEntries.set(entry.key, entry); + } + + for (const { item } of result) { + filteredEntries.set(item.key, item); + } + + return [...filteredEntries.values()]; }, [entries, searchFilter]); return ( @@ -61,7 +72,7 @@ export const Filter = ({
- {filteredEntries + {[...filteredEntries] .sort((entryA, entryB) => compareEntries(entryB, entryA)) .map((entry) => (