Skip to content

FE-512: Create FilterableListSubView with keyboard navigation and search#8532

Open
kube wants to merge 31 commits intocf/h-6324-migrate-petrinaut-to-oxlint-and-verify-react-compilerfrom
cf/fe-512-create-filterablelistsubview-component
Open

FE-512: Create FilterableListSubView with keyboard navigation and search#8532
kube wants to merge 31 commits intocf/h-6324-migrate-petrinaut-to-oxlint-and-verify-react-compilerfrom
cf/fe-512-create-filterablelistsubview-component

Conversation

@kube
Copy link
Collaborator

@kube kube commented Mar 12, 2026

Kapture.2026-03-13.at.11.01.04.mp4

🌟 What is the purpose of this PR?

Introduces the FilterableListSubView component and a global search panel for the Petrinaut editor sidebar. This provides a reusable, keyboard-navigable list pattern for all sidebar sections, along with fuzzy search across all entities.

🔗 Related links

🔍 What does this change?

FilterableListSubView

  • Add createFilterableListSubView factory for reusable sidebar list sections
  • Keyboard navigation (Arrow Up/Down), range selection (Shift+Click, Shift+Arrow), multi-select (Ctrl/Cmd+Click)
  • Row ellipsis menu with context actions
  • Focus management with visual focus indicator and blur reset

Search panel

  • Global search triggered via Ctrl/Cmd+F or Search button in sidebar headers
  • Fuzzy matching using fuzzysort with highlighted match results
  • Keyboard navigation between search input and results (ArrowDown enters list, ArrowUp on first item returns to input)
  • Escape closes search; empty query shows no results

VerticalSubViewsContainer improvements

  • Collapsible header border hidden when collapsed
  • renderTitle support for custom header content (used by search input)
  • Token-based header height and icon sizing

Other

  • Centralized entity icons
  • Keyboard focus highlighting on Menu items and submenu triggers
  • TopBar title input fills available space
  • Fix infinite render loop in EditorProvider by removing manual useCallback/useMemo (React Compiler handles memoization)

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • modifies an npm-publishable library and I have added a changeset file(s)

📜 Does this require a change to the docs?

The changes in this PR:

  • are internal and do not require a docs change

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

❓ How to test this?

  1. Checkout the branch and run yarn dev in libs/@hashintel/petrinaut
  2. Open the sidebar — verify lists are keyboard navigable (Arrow keys, Shift+Click range, Ctrl+Click toggle)
  3. Press Ctrl/Cmd+F — search panel appears with animated transition
  4. Type to fuzzy-search entities — matches are highlighted
  5. Use Arrow Down to navigate into results, Arrow Up on first result returns to input
  6. Press Escape to close search

@github-actions github-actions bot added area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team labels Mar 12, 2026
@vercel
Copy link

vercel bot commented Mar 12, 2026

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

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview, Comment Mar 14, 2026 7:15pm
petrinaut Ready Ready Preview Mar 14, 2026 7:15pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
hashdotdesign Ignored Ignored Preview Mar 14, 2026 7:15pm
hashdotdesign-tokens Ignored Ignored Preview Mar 14, 2026 7:15pm

Copy link
Collaborator Author

kube commented Mar 12, 2026

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@cursor
Copy link

cursor bot commented Mar 13, 2026

PR Summary

Medium Risk
Moderate UI/UX change that refactors left sidebar list implementations and adds new keyboard shortcut-driven search state, which could introduce selection/focus regressions but does not touch security- or data-critical logic.

Overview
Adds a reusable createFilterableListSubView pattern for left-sidebar entity lists, including keyboard navigation, range/multi-select behaviors, and per-row overflow menus for actions like delete.

Introduces a sidebar-wide fuzzy search panel (powered by fuzzysort) that can be opened via header search buttons or Ctrl/Cmd+F, with focus management via new EditorContext search state/ref and an animated swap between normal sidebar content and search view.

Updates subview/header plumbing and styling (SubView now supports icon, renderTitle, and alwaysShowHeaderAction), applies icons across properties subviews, and includes small visual tweaks (menu highlighted state, tooltip icon variants, layout/padding adjustments, and editor container overflow).

Written by Cursor Bugbot for commit 981a51a. This will update automatically on new commits. Configure here.

@augmentcode
Copy link

augmentcode bot commented Mar 13, 2026

🤖 Augment PR Summary

Summary: Refines Petrinaut’s vertical subview headers to better match updated UI behavior and adds reusable list subviews.

Changes:

  • Updated VerticalSubViewsContainer header styling: header actions/tooltip reveal on hover/focus and chevron visibility behavior.
  • Extended SubView to support optional header icons and an alwaysShowHeaderAction flag.
  • Introduced centralized entity icon exports (entity-icons.tsx) and applied them to PropertiesPanel headers.
  • Refactored left sidebar lists (nodes/types/parameters/differential equations) onto a new createFilterableListSubView factory with consistent row styling and per-row menus.
  • Adjusted menu/tooltip/section styling tokens for updated design-system alignment (highlight states, borders, radii, spacing).

Technical Notes: New filterable list subviews add keyboard navigation and multi-select range selection, and move destructive actions into an ellipsis menu using the shared Menu component.

🤖 Was this summary useful? React with 👍 or 👎

Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

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

Review completed. 3 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

<item.icon size={LIST_ITEM_ICON_SIZE} />
</span>
)}
<span className={listItemNameStyle}>
Copy link

Choose a reason for hiding this comment

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

renderItem() is wrapped in a <span>, but some callers (e.g. parameters) return a <div> which creates invalid HTML (div inside span) and can lead to layout/hydration warnings. Consider using a block wrapper or constraining renderItem to inline content.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed — changed the <span> wrapper to <div> so block-level renderItem content doesn't create invalid HTML.

[items, getSelectionItem, setSelection],
);

const handleKeyDown = useCallback(
Copy link

Choose a reason for hiding this comment

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

The listbox-level onKeyDown will also fire when focus is on nested controls (e.g. the row menu IconButton), so Enter/Space/arrow keys can interfere with opening/navigating menus. Consider ignoring key handling unless the event originated from the container itself.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed — added event.target \!== event.currentTarget guard so the listbox-level onKeyDown ignores events bubbling from nested controls like menu buttons.

@kube kube force-pushed the graphite-base/8532 branch from 0b08c25 to 44fe283 Compare March 13, 2026 01:28
@kube kube force-pushed the cf/fe-512-create-filterablelistsubview-component branch from 39823b5 to ae532f6 Compare March 13, 2026 01:28
@kube kube changed the base branch from graphite-base/8532 to main March 13, 2026 01:28
@github-actions github-actions bot added the area/deps Relates to third-party dependencies (area) label Mar 13, 2026
@kube kube changed the title Show chevron on hover in VerticalSubViewsContainer Header FE-512: Create FilterableListSubView with keyboard navigation and search Mar 13, 2026
},
emptyMessage: "No global parameters yet",
renderHeaderAction: () => <ParametersHeaderAction />,
});
Copy link

Choose a reason for hiding this comment

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

Simulation parameter value editing feature lost in refactor

Medium Severity

The refactor from a custom component to createFilterableListSubView removed the inline NumberInput that allowed changing parameter values during simulation mode. The old code used SimulationContext (parameterValues, setParameterValue) to render an editable input for each parameter when globalMode === "simulate". The new code only hides the delete menu item in simulation mode but provides no way to edit parameter values inline.

Fix in Cursor Fix in Web

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Valid observation — the inline NumberInput for editing parameter values during simulation mode was removed when migrating to the createFilterableListSubView pattern. This will be addressed in a follow-up since it requires a per-item renderAction slot that doesn't fit the current generic list abstraction.

kube and others added 15 commits March 14, 2026 14:55
…e entity icons

Clicking in the list container but not on an item now calls clearSelection
to deselect all. Also update Parameter/TokenType/DifferentialEquation icons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Increase default panel content horizontal padding to 4 and use negative
margin on FilterableListSubView to reduce its effective padding to 3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…terable list and menu

- Add Arrow Up/Down keyboard navigation with focused item tracking
- Support Shift+Click and Shift+Arrow range selection via selectRange helper
- Ctrl/Cmd+Click toggles multi-selection with anchor tracking
- Escape clears selection and resets focus state
- Clamp focus/anchor indices when item list shrinks
- Auto-scroll focused items into view
- Add ARIA listbox/option roles for accessibility
- Suppress default browser focus outline on list container
- Add _highlighted styles to Menu items and submenu triggers for keyboard focus

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Convert headerRowStyle to cva with isCollapsed variant that sets
borderBottomColor to transparent, avoiding visual double-borders
between collapsed sections.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace raw [44px] with token value for consistent sizing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Clear focusedIndex and anchorIndex on blur when focus moves outside
the list container, so stale keyboard state doesn't persist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace fixed minWidth with flex: 1 so the title input expands
to use all remaining space in the left section.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add isSearchOpen state and searchInputRef to EditorContext
- Create SearchPanel subview with input in header via renderTitle
- Add renderTitle to SubView type for custom main header content
- Swap between normal subviews and search panel with CSS transition
- Ctrl/Cmd+F opens search or focuses input if already open
- Escape closes search globally via keyboard shortcuts hook
- Wire Search button in FilterableListSubView header to open search

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Search all sidebar items (nodes, types, equations, parameters) with
fuzzy matching via fuzzysort. Matched characters are highlighted in
results. Shows match count at top, all items when query is empty.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… call

ArrowDown from the search input moves focus to the first result. ArrowUp/Down
navigates through results, selecting each item. ArrowUp on the first result
returns focus to the input. Also fixes the fuzzysort highlight call to use the
instance method (result.highlight) instead of the non-existent static method.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Hide the Filter and Sort IconButtons from FilterHeaderAction (not yet
implemented). Remove all useCallback and useMemo wrappers from changed
files so the React Compiler can optimize the components. Also fix search
panel to show empty state when no query and prevent onFocus render loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… handler

Rename getMenuItems → useMenuItems so the linter recognizes these
callbacks as hooks (they call use() and useIsReadOnly() inside RowMenu's
render). Add onKeyDown to the row div to satisfy the
click-events-have-key-events a11y rule.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
kube and others added 3 commits March 14, 2026 19:30
- Fix invalid HTML: change <span> to <div> for listItemNameStyle wrapper
  so block-level renderItem content (e.g. parameters) nests correctly
- Guard listbox onKeyDown to ignore events bubbling from nested controls
- Prevent Ctrl+F from intercepting inside Monaco/inputs by checking
  isInputFocused before handling the shortcut
- Restrict Escape-to-close-search to only fire when search input is focused
- Remove duplicate onKeyDown on search result rows (parent listbox handles
  navigation; rows now only handle Enter/Space for a11y)
- Add alwaysShowHeaderAction to multi-selection panel for consistency
- Remove commented-out CSS in tooltip.tsx
- Update stale doc comment (getMenuItems → useMenuItems)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The compiler flagged useMenuItems and useItems as dynamic hook props.
Fix by:
- Replacing useMenuItems callback with renderRowMenu component prop,
  so each consumer defines a proper React component (DiffEqRowMenu,
  ParameterRowMenu, TypeRowMenu) that calls hooks safely
- Moving useItems() call from FilterableListContent into the factory's
  Component, passing plain items data down instead of a hook prop
- Exporting RowMenu helper for consumers to render shared menu chrome

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds overflow: hidden to editorRootStyle so portalled menus and
tooltips don't render outside the component boundary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

const listRef = useRef<HTMLDivElement>(null);
const rowRefs = useRef<(HTMLDivElement | null)[]>([]);

const { searchInputRef } = use(EditorContext);
Copy link

Choose a reason for hiding this comment

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

Duplicate use(EditorContext) call in SearchContent component

Low Severity

SearchContent calls use(EditorContext) twice — once at line 205 to destructure isSelected and selectItem, and again at line 212 to destructure searchInputRef. These could be a single use() call with a combined destructuring.

Fix in Cursor Fix in Web

Comment on lines +127 to +149
<div
className={contentLayerStyle({
active: !isSearchOpen,
direction: "forward",
})}
>
<VerticalSubViewsContainer
name="left-sidebar"
subViews={LEFT_SIDEBAR_SUBVIEWS}
/>
</div>
<div
className={contentLayerStyle({
active: isSearchOpen,
direction: "backward",
})}
>
<VerticalSubViewsContainer
name="left-sidebar-search"
subViews={searchSubViews}
/>
</div>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

Both content layers (main sidebar and search) are rendered simultaneously with only opacity and pointerEvents controlling visibility. The pointerEvents: 'none' prevents mouse interaction but does not prevent keyboard focus. Users can still Tab into invisible/inactive elements in the hidden layer, causing confusing navigation.

Fix: Add visibility: 'hidden' or conditionally render the inactive layer:

compoundVariants: [
  {
    active: false,
    css: { 
      transform: "translateX(-8px)",
      visibility: "hidden" // Prevents keyboard focus
    }
  }
]
Suggested change
<div
className={contentLayerStyle({
active: !isSearchOpen,
direction: "forward",
})}
>
<VerticalSubViewsContainer
name="left-sidebar"
subViews={LEFT_SIDEBAR_SUBVIEWS}
/>
</div>
<div
className={contentLayerStyle({
active: isSearchOpen,
direction: "backward",
})}
>
<VerticalSubViewsContainer
name="left-sidebar-search"
subViews={searchSubViews}
/>
</div>
</div>
{!isSearchOpen ? (
<div
className={contentLayerStyle({
active: true,
direction: "forward",
})}
>
<VerticalSubViewsContainer
name="left-sidebar"
subViews={LEFT_SIDEBAR_SUBVIEWS}
/>
</div>
) : (
<div
className={contentLayerStyle({
active: true,
direction: "backward",
})}
>
<VerticalSubViewsContainer
name="left-sidebar-search"
subViews={searchSubViews}
/>
</div>
)}
</div>

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/deps Relates to third-party dependencies (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

1 participant