mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-25 08:59:13 +00:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user