Skip to content

feat(dropdown-menu,select): persistent items, a search query hook, and one search implementation - #166

Merged
tmccoy14 merged 4 commits into
mainfrom
feat/dropdown-search-persistent-items
Sep 25, 2026
Merged

tmccoy14 merged 4 commits into
mainfrom
feat/dropdown-search-persistent-items

Conversation

@tmccoy14

@tmccoy14 tmccoy14 commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

What

Follow-up to #163, from a review of Guardian Studio's adoption of 4.3.x. It adds what Studio was hand-rolling around in-menu search, and closes the gaps left between DropdownMenu and Select search.

Added

  • A persistent prop on DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuSeparator, SelectItem and SelectSeparator. A persistent item stays visible while searching but is never a result. It doesn't count towards the empty state or the result count, and Enter in the search box skips it.
  • useDropdownMenuSearchQuery(), which returns { query, isSearching, matches } to components inside a DropdownMenu. Studio's project picker uses it so "Select all" selects only the projects a search shows.
  • useSelectSearchQuery(), the same hook for components inside a Select.

Changed

  • DropdownMenuSearch shows on open by default, as SelectSearch does. alwaysVisible={false} keeps reveal-on-typing.
  • A searchable FilterDropdown keeps "Clear all" visible during a search, as a persistent item. Previously the footer's padding stayed behind as an empty strip.

Fixed

  • Side flip during search. A searchable DropdownMenu, and so FilterDropdown and RadioDropdown, flipped sides mid-search, the same bug fix(select,filter-dropdown): keep Clear all's menu open and a searching Select's side #163 fixed for Select. In Studio at 1280×560, the Activity Logs project filter opened above its trigger, dropped below on a query with no matches, and went back on clear.
  • Backspace. With a DropdownMenu item focused, Backspace now edits the query, as in Select.
  • Dialog focus. Opening inside a Dialog now reliably focuses the search. The 4.3.1 fix only reacted to the menu's first focus event, and the input could claim that before the dialog's focus trap pulled focus back out. In Studio's Manage Roles dialog it failed 2–4 of every 20 opens. It now pulls focus back whenever the menu itself is focused before any interaction, as SelectContent does.

Structure

  • The side lock (useSearchSideLock), the interacted-since-open flag, the query hook (useSearchQuery) and the key handling on list items (handleListKeyDown) now live in list-search.ts, so Select and DropdownMenu share one implementation of each. resetForOpen clears the lock and the flag. DropdownMenuContent and SelectContent now have the same shape.
  • useFilterableItem and useSearchSideLock take options objects rather than trailing positional arguments.
  • Resetting the flag after a controlled open happens in an effect rather than during render, to satisfy react-hooks/refs.
  • data-persistent is set after {...props} on every item, so consumer props can't override it.
  • Each hook is exported from its component's file in its own export { … } under an eslint-disable-next-line react-refresh/only-export-components, as form.tsx exports useFormField, so the barrel stays export *.
  • The redundant alwaysVisible props are gone from FilterDropdown and RadioDropdown. The docs VersionSwitcher keeps it: @eqtylab/docs declares a ^4.1.0 peer range, and before 4.4.0 the default is reveal-on-typing.

Docs

  • The DropdownMenu and Select pages have Persistent items sections, each with a live demo and a snippet that matches it, and the props tables are updated in Slots order. The hooks are documented in prose, as useFormField is, including that they throw outside their root.
  • The DropdownMenu demo variants are renamed to match Select (with-search is the default, with-search-reveal reveals on typing).
  • The empty state now sits directly after the search box in every example, the docs VersionSwitcher and the test fixtures.
  • Removed a stale claim on the Select page that only Select shows its search by default. Fixed the FilterDropdown page, which still said "Clear all" closes the menu.
  • Added a row to the skill's name translation table for a footer action, "Select all" or "Other" that should stay visible while searching.

Tests

63 passing (was 40). New coverage:

  • persistent items and separators, for both components, including the result count and Enter;
  • useDropdownMenuSearchQuery and useSelectSearchQuery, including throwing outside their root;
  • DropdownMenu reveal-on-keystroke with alwaysVisible={false};
  • Backspace from a focused item, for both components;
  • ↑ from the first DropdownMenu item back to the search box;
  • the side lock, including release on resetForOpen, via renderHook, since jsdom has no layout;
  • interaction tracking, including its reset when a controlled open becomes true;
  • focus recovery when the menu itself is focused before any interaction. This test fails against the 4.3.1 first-focus logic.

Verification

  • pnpm format:check, pnpm lint, pnpm test, tsc --noEmit, and the ui and demo builds.
  • In Studio with this build (eqtylab/guardian#275): 46 affected e2e specs passed, and a full suite run passed 227 of 228. The one failure was a histogram test unrelated to dropdowns, which passed on its own. Menus inside dialogs focused their search in 30 of 30 opens.

4.4.0, with a CHANGELOG entry.

🤖 Generated with Claude Code

tmccoy14 and others added 4 commits September 25, 2026 08:41
…d one search implementation

Adds what consumers were hand-rolling around in-menu search, and closes the
remaining gaps between DropdownMenu and Select search:

- A persistent prop on DropdownMenu items and separators, SelectItem and
  SelectSeparator: visible while searching, never a result, skipped by Enter
- useDropdownMenuSearchQuery, for components inside the menu that act on the
  matches, such as a "Select all" scoped to the search
- DropdownMenuSearch shows on open by default, as SelectSearch does
- A searchable FilterDropdown keeps "Clear all" through a search, as a
  persistent item, instead of leaving an empty strip
- DropdownMenu keeps the side it opened on while searching; the side lock and
  the interacted-since-open flag move into list-search, so Select and
  DropdownMenu share one implementation of each
- Backspace on a focused DropdownMenu item edits the query, as in Select
- Opening inside a Dialog reliably focuses the search: 4.3.1 recovered focus
  only on the menu's first focus event, which the input could claim first

The search context and hook live in their own module so dropdown-menu.tsx
exports only components. Docs, demos and the skill's name translation table
cover the new API. 4.4.0, with a CHANGELOG entry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…stray semicolon

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review follow-up for the persistent items branch:

- VersionSwitcher keeps alwaysVisible, so @eqtylab/docs behaves the same
  on the ^4.1.0 peer range it declares
- useDropdownMenuSearchQuery moves into dropdown-menu.tsx, exported as
  form.tsx exports useFormField, and the barrel is plain export * again
- useSelectSearchQuery, so Select has the same query hook; both wrap one
  useSearchQuery in list-search
- One handleListKeyDown in list-search replaces the key handling that
  DropdownMenuContent and SelectContent each duplicated; the two content
  components now share one shape
- useFilterableItem and useSearchSideLock take options objects rather than
  trailing positional arguments
- Docs: the persistent items snippet matches its demo, the hooks are
  documented in prose like useFormField, and separator props follow the
  Slots order
- Tests: ArrowUp to the search in DropdownMenu, Backspace in Select, the
  controlled-open interaction reset, and both hooks throwing outside
  their root; fixtures and helpers mirror each other across the two files
- Comments trimmed to warnings; the skill table gains one row without
  re-padding

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@tmccoy14 tmccoy14 self-assigned this Sep 25, 2026
@tmccoy14
tmccoy14 merged commit 27506ac into main Sep 25, 2026
1 check passed
@tmccoy14
tmccoy14 deleted the feat/dropdown-search-persistent-items branch September 25, 2026 13:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant