Skip to content

Conversation

@faogustavo
Copy link
Collaborator

@faogustavo faogustavo commented Dec 24, 2025

  • Fixed index emission on SelectableLazyColumn when the selection changes by the speed search
  • Created 'EmptySpeedSearchMatcher' to easily identify when the filter text is empty
    • This matches is automatically returned in the SpeedSearchState.matcher when the text is empty
  • Added 'dismissOnLoseFocus' to SpeedSearchArea to keep the filter when the focus is left from the component
  • Added 'currentMatcher' to the 'SpeedSearchState', allowing the user use it for filtering purposes
  • Created convenience function on top of 'SpeedSearchMatcher' to check if the given text matches or not
  • Created convenience functions to support filtering collections based on the speed search matcher
  • Added a new page in the samples to test all SpeedSearch components and their variants
  • Added documentation for a bunch of public APIs that were missing

Evidences

Filter Speed Search
Screen Recording 2025-12-23 at 22 09 11

Release notes

⚠️ Important Changes

  • Added 'dismissOnLoseFocus' property to SpeedSearchArea, allowing users to choose if they want to keep the input visible on lose focus
    • This is important for cases that you want to use SpeedSearch as a filter

New features

  • Added support for filtering Collections using SpeedSearchMatcher.
    • Check the '.filter()' extension functions available in the 'org.jetbrains.jewel.foundation.search' package

Bug fixes

  • Fixed an issue that was not triggering the 'onSelectedIndexesChange' call when the selection gets changed by the SpeedSearch

Note

Introduces filtering-focused improvements and small fixes around speed search and selection.

  • SpeedSearchArea API: new overload accepting external SpeedSearchState, rememberSpeedSearchState, dismissOnLoseFocus flag, and SpeedSearchState.currentMatcher + visibility controls; internal batching/caching for better performance.
  • Matcher utilities: EmptySpeedSearchMatcher, SpeedSearchMatcher.doesMatch, and Iterable.filter(...) extensions (strings and generics).
  • Selection/scroll behavior: SpeedSearchableLazyColumnScrollEffect now reports selection via onSelectedIndexChange; SelectableLazyColumn emits indices when selection changes via SpeedSearch and syncs lastActiveItemIndex.
  • Samples/tests: new "Speed Search" showcase (tree, highlight, filter); added unit/UI tests for filtering and focus dismissal behavior; updated existing LazyColumn/Tree tests to cover dismissOnLoseFocus=false.

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

- Fixed index emission on SelectableLazyColumn when the selection changes by the speed search
- Created 'EmptySpeedSearchMatcher' to easily identify when the filter text is empty
  - This matches is automatically returned in the SpeedSearchState.matcher when the text is empty
- Added 'dismissOnLoseFocus' to SpeedSearchArea to keep the filter when the focus is left from the component
- Added 'currentMatcher' to the 'SpeedSearchState', allowing the user use it for filtering purposes
- Created convenience function on top of 'SpeedSearchMatcher' to check if the given text matches or not
- Created convenience functions to support filtering collections based on the speed search matcher
// The `state.lastActiveItemIndex` can also be changed by the SpeedSearch flow.
// If the last active index is not among the selected indices, the SpeedSearch
// has selected a different value and an update must be triggered.
if (indices != lastEmittedIndices || state.lastActiveItemIndex !in indices) {
Copy link

Choose a reason for hiding this comment

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

Condition causes repeated callback invocations when selection is empty

The condition state.lastActiveItemIndex !in indices always evaluates to true when indices is empty (since any value, including null, is not contained in an empty list). This means when there's no selection, the onSelectedIndexesChange callback fires every time the LaunchedEffect runs, even if nothing has actually changed. The second part of the condition needs to be guarded to only apply when indices is non-empty, for example: (indices.isNotEmpty() && state.lastActiveItemIndex !in indices).

Fix in Cursor Fix in Web

keys = currentStateToList.value.second,
onSelectedIndexChange = { currentOnSelectedIndexesChange(listOf(it)) },
dispatcher = dispatcher,
)
Copy link

Choose a reason for hiding this comment

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

Duplicate callback invocations when SpeedSearch selects an item

When SpeedSearch selects an item, the onSelectedIndexesChange callback can be invoked twice for the same selection event. SpeedSearchableLazyColumnScrollEffect sets selectedKeys and directly calls onSelectedIndexChange. This selectedKeys change then triggers the LaunchedEffect in SelectableLazyColumn, which also calls onSelectedIndexesChange because lastEmittedIndices wasn't updated by the first callback path. This could cause performance issues or unexpected behavior if the callback has side effects.

Additional Locations (1)

Fix in Cursor Fix in Web

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant