fix(plex): address S2 /code-review findings (typeahead focus + a11y + test)

- Decouple the typeahead's dismiss from real focus: a separate `dismissed`
  flag (set on Escape/pick, cleared on typing/refocus) replaces manually
  clearing `focused`, so closing the panel no longer wedges it shut while the
  input keeps DOM focus — typing again re-opens it (verified in-browser).
- Scroll the highlighted option into view during arrow-key nav (listRef +
  data-idx + scrollIntoView), instead of leaving listRef unused.
- Wire the input as a combobox for screen readers: role=combobox,
  aria-autocomplete/expanded/controls/activedescendant + per-option ids.
- Drop the redundant client-side .slice (the request already caps at limit=10).
- SearchBar gains an `inputProps` passthrough for the combobox ARIA.
- Add PlexSearchBox.dom.test.tsx (4 tests): per-scope pill options, suggestion
  render + keyboard pick→jumpTo, and the Escape→retype reopen regression.

Reviewer's scrollbar-blur concern was a false positive — the listbox container
is not focusable, so a mousedown on it never blurs the input.
This commit is contained in:
2026-08-16 08:21:29 +02:00
parent f9681deecf
commit c51df4e0b9
3 changed files with 220 additions and 11 deletions
+11 -1
View File
@@ -1,4 +1,9 @@
import { type FocusEventHandler, type KeyboardEventHandler, type ReactNode } from "react";
import {
type FocusEventHandler,
type InputHTMLAttributes,
type KeyboardEventHandler,
type ReactNode,
} from "react";
import { useTranslation } from "react-i18next";
import { Search, X } from "lucide-react";
@@ -26,6 +31,7 @@ export default function SearchBar({
onFocus,
onBlur,
onKeyDown,
inputProps,
}: {
value: string;
onChange: (q: string) => void;
@@ -41,6 +47,9 @@ export default function SearchBar({
onFocus?: FocusEventHandler<HTMLInputElement>;
onBlur?: FocusEventHandler<HTMLInputElement>;
onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
// Extra attributes spread onto the <input> (e.g. Plex's combobox ARIA). Spread FIRST so the
// component's own value/handlers always win.
inputProps?: InputHTMLAttributes<HTMLInputElement>;
}) {
const { t } = useTranslation();
const fire = () => {
@@ -51,6 +60,7 @@ export default function SearchBar({
<div className="h-11 pl-3 pr-2 rounded-2xl glass-menu flex items-center gap-2 focus-within:outline-2 focus-within:outline-accent/60 focus-within:outline-offset-2">
{leading ?? <Search className="w-4 h-4 shrink-0 text-muted" />}
<input
{...inputProps}
value={value}
onChange={(e) => onChange(e.target.value)}
onFocus={onFocus}