feat(finder): make filters a toggle and flag hidden results

The filter badges wrote a keyword prefix into the search box, which put
syntax in front of the user for something the badge already expresses.
They are now toggles: the input holds only what is being searched for,
the active badge is highlighted, and pressing it again returns to
searching every field. Typing a keyword still works and lights up the
badge it refers to, so the documented syntax is not lost.

The result list said nothing when it had more matches than it rendered.
It now ends with a count of what is hidden, since the footer total is
easy to miss while scanning results.

Also stops the search shortcut toggling the dialog. Toggling on a key
that mounts and unmounts the dialog is unreliable, and browsers treat a
repeated find shortcut as "focus the search again". It now opens, and
selects the existing query when the finder is already up, leaving
Escape to close.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DzALEq9gGWwFmwTdgAiFcY
This commit is contained in:
Claude
2026-08-18 16:54:10 +00:00
parent cd09476858
commit 2fc072a9d3
5 changed files with 136 additions and 38 deletions
+25 -10
View File
@@ -4,7 +4,7 @@ import { KeyboardEvent, PointerEvent, useDeferredValue, useEffect, useRef, useSt
import Input from '../../../common/components/input/input/Input';
import Kbd from '../../../common/components/kbd/Kbd';
import Modal from '../../../common/components/modal/Modal';
import useFinder, { applyFilter, FilterableEntry } from './useFinder';
import useFinder, { FilterableEntry } from './useFinder';
import style from './Finder.module.scss';
@@ -15,6 +15,7 @@ interface FinderProps {
export default function Finder({ isOpen, onClose }: FinderProps) {
const [search, setSearch] = useState('');
const [filter, setFilter] = useState<MaybeString>(null);
const [selectedId, setSelectedId] = useState<MaybeString>(null);
/**
@@ -22,7 +23,7 @@ export default function Finder({ isOpen, onClose }: FinderProps) {
* The search itself is cheap, rendering the results is what costs.
*/
const deferredSearch = useDeferredValue(search);
const { select, results, error, total, filters } = useFinder(deferredSearch);
const { select, results, error, total, filters, appliedFilter } = useFinder(deferredSearch, filter);
const inputRef = useRef<HTMLInputElement>(null);
const activeRef = useRef<HTMLLIElement>(null);
@@ -44,6 +45,13 @@ export default function Finder({ isOpen, onClose }: FinderProps) {
}, [activeEntry?.id]);
const navigate = (event: KeyboardEvent<HTMLDivElement>) => {
// pressing the search shortcut again selects the query, ready to be replaced
if ((event.metaKey || event.ctrlKey) && event.key === 'f') {
event.preventDefault();
inputRef.current?.select();
return;
}
// all operations need results
if (results.length === 0) {
return;
@@ -79,13 +87,13 @@ export default function Finder({ isOpen, onClose }: FinderProps) {
setSelectedId(id);
};
/** Scopes the search to a single field, keeping whatever the user already typed */
/** Scopes the search to a single field, or back to all fields when tapped again */
const handleFilter = (filterKey: string) => {
setSearch(applyFilter(search, filters, filterKey));
setFilter((previous) => (previous === filterKey ? null : filterKey));
inputRef.current?.focus();
};
const hasOverflow = total > results.length;
const hiddenResults = total - results.length;
return (
<Modal
@@ -106,14 +114,16 @@ export default function Finder({ isOpen, onClose }: FinderProps) {
/>
<div className={style.filters} data-testid='finder-filters'>
<span className={style.filterLabel}>Filter by</span>
{filters.map((filter) => (
{filters.map((option) => (
<button
key={filter.key}
key={option.key}
type='button'
className={style.filterBadge}
onClick={() => handleFilter(filter.key)}
data-active={appliedFilter === option.key}
aria-pressed={appliedFilter === option.key}
onClick={() => handleFilter(option.key)}
>
{filter.label}
{option.label}
</button>
))}
</div>
@@ -154,6 +164,11 @@ export default function Finder({ isOpen, onClose }: FinderProps) {
</li>
);
})}
{hiddenResults > 0 && (
<li className={style.more} data-testid='finder-more'>
{hiddenResults} more {hiddenResults === 1 ? 'result' : 'results'} keep typing to narrow the search
</li>
)}
</ul>
</div>
}
@@ -176,7 +191,7 @@ export default function Finder({ isOpen, onClose }: FinderProps) {
</div>
{total > 0 && (
<div className={style.count} data-testid='finder-count'>
{hasOverflow ? `Showing ${results.length} of ${total}` : `${total} result${total === 1 ? '' : 's'}`}
{hiddenResults > 0 ? `Showing ${results.length} of ${total}` : `${total} result${total === 1 ? '' : 's'}`}
</div>
)}
</div>