From c3ff1be7f50b05f0b96da966a4b1a46f53d1d6d4 Mon Sep 17 00:00:00 2001 From: Divyam Talwar Date: Sun, 28 Jun 2026 05:30:43 +0530 Subject: [PATCH 1/3] Preserve selected search filters during narrowing Constraint: Existing filter panel sorting already pins selected entries; the fix keeps that behavior while changing only filtered membership. Rejected: Replacing the filter UI or changing Fuse matching | unnecessary scope for issue #131. Confidence: high Scope-risk: narrow Directive: Keep selected filter chips visible even when the current filter text does not match their display name. Tested: node .yarn/releases/yarn-4.7.0.cjs workspace @sourcebot/web test 'src/app/(app)/search/components/filterPanel/filter.test.tsx'; node .yarn/releases/yarn-4.7.0.cjs workspace @sourcebot/web lint Not-tested: Full web build. --- .../components/filterPanel/filter.test.tsx | 66 +++++++++++++++++++ .../search/components/filterPanel/filter.tsx | 15 ++++- 2 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 packages/web/src/app/(app)/search/components/filterPanel/filter.test.tsx 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..14e934419 --- /dev/null +++ b/packages/web/src/app/(app)/search/components/filterPanel/filter.test.tsx @@ -0,0 +1,66 @@ +import { afterEach, describe, expect, test } 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('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) => ( Date: Sun, 28 Jun 2026 05:31:24 +0530 Subject: [PATCH 2/3] Record the selected-filter fix for release notes Constraint: Sourcebot requires every non-doc PR to add an Unreleased changelog entry that links to the PR. Confidence: high Scope-risk: narrow Directive: Keep future changelog entries under the matching Unreleased section and include the real PR link. Tested: Changelog format matched adjacent Unreleased entries. Not-tested: No code tests run for changelog-only commit. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0725af08..75cb7c169 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - 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) +- 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 From 58384ab19f74e57576040b0480342dd9372ebcb4 Mon Sep 17 00:00:00 2001 From: Divyam Talwar Date: Sun, 28 Jun 2026 06:23:16 +0530 Subject: [PATCH 3/3] Make search filter options keyboard accessible Replace the filter option click container with a native toggle button while preserving selected entries as clearable controls. Constraint: Frontend review found the existing filter row was mouse-only.\nRejected: Adding ARIA to a div | Native button semantics provide keyboard activation and focus behavior with less custom code.\nConfidence: high\nScope-risk: narrow\nDirective: Keep selected disabled entries clearable so users can remove incompatible filters.\nTested: node .yarn/releases/yarn-4.7.0.cjs workspace @sourcebot/web test 'src/app/(app)/search/components/filterPanel/filter.test.tsx'\nTested: node .yarn/releases/yarn-4.7.0.cjs workspace @sourcebot/web exec eslint 'src/app/(app)/search/components/filterPanel/filter.tsx' 'src/app/(app)/search/components/filterPanel/filter.test.tsx' 'src/app/(app)/search/components/filterPanel/entry.tsx'\nTested: git diff --check\nNot-tested: Full web build not rerun for this narrow component accessibility change. --- .../search/components/filterPanel/entry.tsx | 24 +++++++++------- .../components/filterPanel/filter.test.tsx | 28 ++++++++++++++++++- 2 files changed, 41 insertions(+), 11 deletions(-) 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 index 14e934419..cc6982799 100644 --- a/packages/web/src/app/(app)/search/components/filterPanel/filter.test.tsx +++ b/packages/web/src/app/(app)/search/components/filterPanel/filter.test.tsx @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, test } from 'vitest'; +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'; @@ -36,6 +36,32 @@ const entries: Entry[] = [ ]; 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(