feat(feed): compress the toolbar chip groups into labeled FilterSelect dropdowns
Adds a reusable FilterSelect: a "Label: value ▾" pill that opens a listbox (a checkmark
row for single-select, a checkbox row for multi), on the AnchoredPopover platform. It
replaces the two feed-toolbar chip walls:
- Watch state (single-select, default All) — picking a row replaces + closes.
- Content types (multi-select, default Normal; requireOne keeps at least one on) — rows
toggle and the panel stays open.
The trigger summarises multi picks as "First +N". The component is source-agnostic, meant
for reuse anywhere a small known option set currently renders as a chip group.
FilterSelect takes a `testId` → the trigger gets it, each option gets `${testId}-${value}`,
so the feed/channel E2Es keep stable selectors. They now open the dropdown and assert the
option's aria-selected instead of the old inline aria-pressed chips (new expectContentType/
toggleContentType helpers).
This commit is contained in:
@@ -40,7 +40,7 @@ import { LoadingState, StateMessage } from "./QueryState";
|
||||
import { PageToolbar } from "./PageShell";
|
||||
import { useWizard } from "./WizardProvider";
|
||||
import SortControl from "./SortControl";
|
||||
import FilterChipGroup, { filterChipClass } from "./FilterChipGroup";
|
||||
import FilterSelect from "./FilterSelect";
|
||||
import ViewSwitcher from "./ViewSwitcher";
|
||||
import VirtualFeed from "./VirtualFeed";
|
||||
// Lazy: the in-app YouTube player (IFrame API) loads only when a video is first opened.
|
||||
@@ -570,35 +570,27 @@ export default function Feed({
|
||||
const toolbar = (
|
||||
<div className="pb-3">
|
||||
<div className="flex items-center gap-1.5 gap-y-2 flex-wrap">
|
||||
<FilterChipGroup
|
||||
ariaLabel={t("feed.showLabel")}
|
||||
options={SHOW_IDS.map((id) => ({
|
||||
value: id,
|
||||
label: t("sidebar.show." + id),
|
||||
testId: `feed-show-${id}`,
|
||||
}))}
|
||||
value={filters.show}
|
||||
onChange={(show) => setFilters({ ...filters, show })}
|
||||
{/* Watch state is single-select (always exactly one); content types are an independent
|
||||
multi-select. Both compress to one labeled dropdown pill via FilterSelect. */}
|
||||
<FilterSelect
|
||||
label={t("feed.showLabel")}
|
||||
testId="feed-show"
|
||||
options={SHOW_IDS.map((id) => ({ value: id, label: t("sidebar.show." + id) }))}
|
||||
selected={[filters.show]}
|
||||
onChange={(next) => setFilters({ ...filters, show: next[0] as FeedFilters["show"] })}
|
||||
/>
|
||||
<FilterSelect
|
||||
label={t("feed.contentLabel")}
|
||||
testId="feed-content"
|
||||
multiple
|
||||
options={CONTENT.map((c) => ({ value: c.key, label: t(c.label) }))}
|
||||
selected={CONTENT.filter((c) => filters[c.key]).map((c) => c.key)}
|
||||
onChange={(next) => {
|
||||
const patch = { ...filters };
|
||||
for (const c of CONTENT) patch[c.key] = next.includes(c.key);
|
||||
setFilters(patch);
|
||||
}}
|
||||
/>
|
||||
<span className="mx-1 h-5 w-px bg-border" aria-hidden="true" />
|
||||
{/* Content-type toggles are MULTI-select (independent booleans) — same chip look via
|
||||
filterChipClass, but not a single-select group. */}
|
||||
<div className="flex flex-wrap gap-1.5" role="group" aria-label={t("feed.contentLabel")}>
|
||||
{CONTENT.map((c) => {
|
||||
const on = filters[c.key];
|
||||
return (
|
||||
<button
|
||||
key={c.key}
|
||||
data-testid={`feed-content-${c.key}`}
|
||||
onClick={() => setFilters({ ...filters, [c.key]: !on })}
|
||||
aria-pressed={on}
|
||||
className={filterChipClass}
|
||||
>
|
||||
{t(c.label)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* The Source filter (organic / include-search / search-only) is a global-catalog concept —
|
||||
it selects how videos ENTERED the library. On a channel-scoped view we already show all
|
||||
of this one channel's videos (librarySource is pinned to "all"), so the selector would
|
||||
|
||||
Reference in New Issue
Block a user