Skip to content

ENG-2111 Add keyboard-only node type filtering with tag chips - #1321

Merged
trangdoan982 merged 4 commits into
mainfrom
eng-2111-add-keyboard-only-node-type-filtering-with-tag-chips
Aug 22, 2026
Merged

ENG-2111 Add keyboard-only node type filtering with tag chips#1321
trangdoan982 merged 4 commits into
mainfrom
eng-2111-add-keyboard-only-node-type-filtering-with-tag-chips

Conversation

@trangdoan982

@trangdoan982 trangdoan982 commented Aug 20, 2026

Copy link
Copy Markdown
Member

https://www.loom.com/share/7eae3997668c43829896b093d005401a

What this does

Chips and the query caret now share one field, so a keyboard-first user can narrow by type without reaching for the mouse. Typing offers the best prefix match as ghost text with a hint; Tab commits it to a chip. Text left unconfirmed stays a plain keyword query, so filtering never happens by accident.

Roam's advanced search is the design and behaviour reference. Obsidian has no tag-input component at all, so this is a build rather than a port.

Notable decisions

  • State is ENG-2110's. selectedNodeTypeIds stays the single source of truth, so chips and the filter dropdown are two views of one state. No change to QueryEngine. The chips write the raw list and skip fromPanelSelectedIds' canonicalisation on purpose: collapsing a full selection to "no filter" would make a chip the user just added vanish.
  • Keys bubble instead of being forwarded. Arrow, Enter and Escape reach NodeSearch, whose handler is already an ancestor and already navigates and opens results. Roam threads them through an onSearchKeyDown prop; here that would only duplicate it.
  • Backspace highlights before it deletes. The ticket said "removes the most recent chip"; this follows Roam instead, so a stray keystroke cannot silently drop a filter. The chip-focus state it needs is the same state the arrow keys use.
  • Matching is prefix-only. The suggestion is drawn as a completion of what was typed, and a substring match has no suffix to render.
  • The Tab hint shows whenever Tab would commit — including once the query spells a type name in full and no suffix is left to ghost. Roam hides it there, which is exactly when the user is about to press it.
  • The query is a textarea that grows with its content, so a long query and many chips stay visible rather than scrolling out of the field. Modified Enter is suppressed there: the modal deliberately leaves Mod/Ctrl/Alt+Enter unhandled for the insert and dock actions, so the query would otherwise gain a line break.
  • Field chrome comes from Obsidian's own input variables rather than Tailwind tokens, so it matches a native field (border, --background-modifier-form-field, --input-radius). Two rules were dropped as dead: --input-shadow is none in Obsidian 1.13 and --input-shadow-active is undefined — native fields there are marked by border colour alone.
  • Tailwind's ring-* utilities are inert in this build. --tw-ring-shadow and its offset counterpart are never defined, so the composite box-shadow collapses to none and a focused chip rendered no state at all. The focused chip takes an outline in CSS instead — no variables needed, and unlike a border it does not shift the chips beside it as focus moves.
  • Chip remove buttons are tabIndex={-1}. Tab is the commit key, so focusable remove buttons would wedge themselves into the tab order ahead of the footer actions; keyboard users delete via the highlighted chip.

Known edge case, documented rather than fixed

Chipping every configured type makes selectedNodeTypeIds.length === allTypeIds.length, which hasActiveTypeFilter reads as "no filter" — the count badge disappears while the chips stay on screen. Results are identical either way, and the dropdown shows all rows checked, so the two surfaces do not disagree. Suppressing the last chip would be worse: it would silently refuse a type the user asked for.

Verification

  • pnpm check-types and pnpm lint clean in apps/obsidian (0 errors; pre-existing warnings elsewhere untouched).
  • 32/32 assertions driving the running Obsidian over CDP against a test vault, re-run after the rebase: ghost completion, Tab commit, immediate narrowing, two-chip union, unconfirmed text applying no filter, Backspace highlight-then-remove with the marking asserted on the rendered outline, arrow navigation across chips and back to the caret, results navigation with a chip focused, modified Enter inserting no newline, Tab fall-through to the filter trigger, field growth without clipping, and chip↔dropdown sync in both directions.
  • Field geometry measured: an empty field is 34px like a native input; a long query grows it 34→53px alone and 34→92px with five chips, nothing clipped; all ten types chipped keeps every chip visible.
  • No unit tests. apps/obsidian has no test harness — no test script, no vitest config, no test files — the same call ENG-2110 made. The matching rules were split into a pure util so they are ready for one.

🤖 Generated with Claude Code


Open in Devin Review

@linear-code

linear-code Bot commented Aug 20, 2026

Copy link
Copy Markdown

ENG-2111

@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
discourse-graph Skipped Skipped Aug 22, 2026 4:49am

Request Review

@graphite-app

graphite-app Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

PR size/scope check

This PR is over our review-size guideline.

  • Recommended: ~200 lines changed
  • Acceptable limit: up to 400 lines when well-scoped/self-contained
  • Preferred file count: fewer than 5 files

Please split this into smaller PRs unless there is a clear reason the changes need to land together.

If keeping it as one PR, please add a brief justification covering:

  • What single problem this PR solves
  • Why the files/changes are coupled

@supabase

supabase Bot commented Aug 20, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project zytfjzqyijgagqxrzbmz because there are no changes detected in packages/database/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment thread apps/obsidian/src/components/NodeTypeChipsSearchInput.tsx
Comment thread apps/obsidian/src/components/NodeTypeChipsSearchInput.tsx Outdated
Comment thread apps/obsidian/src/components/NodeTypeChipsSearchInput.tsx Outdated
@trangdoan982
trangdoan982 force-pushed the eng-2111-add-keyboard-only-node-type-filtering-with-tag-chips branch from ebfe6c2 to 9001408 Compare August 20, 2026 02:40
Comment thread apps/obsidian/src/components/NodeTypeChipsSearchInput.tsx Outdated
@trangdoan982
trangdoan982 force-pushed the eng-2111-add-keyboard-only-node-type-filtering-with-tag-chips branch from 9001408 to 64c376f Compare August 20, 2026 02:57
@trangdoan982
trangdoan982 force-pushed the eng-2111-add-keyboard-only-node-type-filtering-with-tag-chips branch from 64c376f to 620c623 Compare August 20, 2026 02:58
@trangdoan982
trangdoan982 requested a review from mdroidian August 20, 2026 20:27
Comment thread apps/obsidian/src/components/NodeTypeChipsSearchInput.tsx Outdated
Comment thread apps/obsidian/src/components/NodeTypeChipsSearchInput.tsx Outdated
@trangdoan982
trangdoan982 force-pushed the eng-2111-add-keyboard-only-node-type-filtering-with-tag-chips branch from 559baa4 to 57d6429 Compare August 22, 2026 04:27
@trangdoan982
trangdoan982 force-pushed the eng-2111-add-keyboard-only-node-type-filtering-with-tag-chips branch from 57d6429 to 0ec6373 Compare August 22, 2026 04:34
@trangdoan982
trangdoan982 force-pushed the eng-2111-add-keyboard-only-node-type-filtering-with-tag-chips branch from 0ec6373 to ee6066d Compare August 22, 2026 04:39
Base automatically changed from eng-2110-add-node-type-filter-dropdown-menu to main August 22, 2026 04:45
@trangdoan982
trangdoan982 force-pushed the eng-2111-add-keyboard-only-node-type-filtering-with-tag-chips branch from ee6066d to a7b1683 Compare August 22, 2026 04:45
Chips and the query caret now share one field, so a keyboard-first user can
narrow by type without reaching for the mouse. Typing offers the best prefix
match as ghost text and Tab commits it to a chip; text left unconfirmed stays a
plain keyword query, so filtering never happens by accident.

Roam's advanced search is the design and behaviour reference. Obsidian has no
tag-input component at all, so this is a build rather than a port: the filter
semantics carry over, the BlueprintJS structure does not.

- nodeTypeChipCompletion.ts holds the matching rules as pure functions, prefix
  only — the suggestion is drawn as a completion of what was typed, and a
  substring match has no suffix to render.
- NodeTypeChipsSearchInput writes the raw selectedNodeTypeIds ENG-2110 owns, so
  chips and the filter dropdown are two views of one state. It skips that
  module's canonicalisation on purpose: collapsing a full selection to "no
  filter" would make a chip the user just added vanish.
- Chips and the query are inline siblings in a block box, so they flow together
  left to right and top to bottom and a long query wraps beside the last chip
  rather than into a column of its own. That rules out an input or a textarea,
  both of which can only wrap inside their own box, so the query is an editable
  span. Its text is uncontrolled, since re-rendering it on every keystroke would
  move the caret.
- The Tab hint is inline after the caret rather than an overlay, so it follows
  the text and wraps with it. It shows whenever Tab would commit, including once
  the query spells a type name in full and no suffix is left to ghost — Roam
  hides it there, precisely when the user is about to press it.
- Arrow, Enter and Escape are left to bubble to NodeSearch, which already
  navigates and opens results. Roam forwards them through a prop instead.
  Enter's default is suppressed here so the editable gains no line break: the
  modal deliberately leaves modified Enter unhandled for the insert and dock
  actions.
- Backspace on an empty field highlights the last chip and only removes it on a
  second press, so a stray keystroke cannot silently drop a filter.
- Styling is Tailwind throughout and adds no CSS. Two rules this build makes
  non-obvious: `border-solid` is required because `@tailwind base` is omitted, so
  nothing sets a default border style and `border` alone renders nothing; and
  `ring-*` is inert for the same reason, so the focused chip takes an outline.
  The chip's remove button carries `clickable-icon` to dodge Obsidian's
  `button:not(.clickable-icon)` rule, which outranks a utility class and would
  otherwise paint a box behind the ×.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
trangdoan982 and others added 3 commits August 22, 2026 00:47
- Chip removal and keyboard bounds used positions in `selectedNodeTypeIds` while
  the index came from `chips`, the rendered list. The two cannot diverge today —
  the modal reads settings once with no subscription — but the assumption was
  load-bearing for nothing, so removal is by id and navigation is bounded by
  what is actually on screen.
- `max-w-40` replaces the inline max-width, per apps/obsidian/AGENTS.md:100. The
  chip's colours stay inline: they are computed per type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The field's click handler focused the query and forced the caret to the end on
every click, including clicks on the text itself — so clicking into the middle
of a query dragged the caret to the end. It now runs only for clicks on the
field's own padding, where there is no caret position to preserve.

Measured: clicking 60% into "hello world" leaves the caret at 7 rather than 11,
and clicking the blank padding still places it at the end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Keeps only what the code cannot say: the Obsidian and Tailwind quirks
(`clickable-icon` beating a utility class, `border-solid` with no preflight),
the deliberate choices (uncontrolled text so the caret survives a re-render,
Backspace highlighting before it deletes, Enter's default suppressed but still
bubbling), and the inline-flow layout that is easy to "tidy" into a flex row.
Drops the rest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@trangdoan982
trangdoan982 force-pushed the eng-2111-add-keyboard-only-node-type-filtering-with-tag-chips branch from a7b1683 to 92fa691 Compare August 22, 2026 04:49
@trangdoan982
trangdoan982 merged commit e88a0af into main Aug 22, 2026
8 checks passed
@trangdoan982
trangdoan982 deleted the eng-2111-add-keyboard-only-node-type-filtering-with-tag-chips branch August 22, 2026 04:53
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.

2 participants