Skip to content

[feature] Add filter mode to select widgets#14537

Merged
lukasmasuch merged 9 commits intodevelopfrom
lukasmasuch/filter-mode-impl
Mar 27, 2026
Merged

[feature] Add filter mode to select widgets#14537
lukasmasuch merged 9 commits intodevelopfrom
lukasmasuch/filter-mode-impl

Conversation

@lukasmasuch
Copy link
Copy Markdown
Collaborator

@lukasmasuch lukasmasuch commented Mar 26, 2026

Describe your changes

  • Adds filter_mode to st.selectbox and st.multiselect with "fuzzy", "contains", "prefix", and None modes.
  • Keeps fuzzy matching as the default while letting apps use substring or prefix matching, or disable typing entirely for short option lists.
  • Wires the setting through protobuf and shared frontend filtering so widget identity, bulk-select behavior, and docs stay consistent across both widgets.
  • Adds focused Python, typing, frontend, and E2E coverage for the new filtering modes.

GitHub Issue Link (if applicable)

Testing Plan

  • make all
  • make check
  • E2E_CHECK=true make check
  • uv run pytest lib/tests/streamlit/elements/selectbox_test.py lib/tests/streamlit/elements/multiselect_test.py
  • cd frontend && yarn test lib/src/util/fuzzyFilterSelectOptions.test.ts lib/src/components/shared/Dropdown/Selectbox.test.tsx lib/src/components/widgets/Multiselect/Multiselect.test.tsx lib/src/components/widgets/Selectbox/Selectbox.test.tsx
  • make frontend-fast
  • make run-e2e-test e2e_playwright/st_selectbox_test.py with PYTEST_ADDOPTS='-k "widget_rendering or has_correct_initial_values or prefix_filter_mode or contains_filter_mode or filter_mode_none"'
  • make run-e2e-test e2e_playwright/st_multiselect_test.py with PYTEST_ADDOPTS='-k "initial_value or prefix_filter_mode or contains_filter_mode or filter_mode_none"'
  • Any manual testing needed?
Agent metrics
Type Name Count
skill implementing-feature 1
subagent Explore 1

Implement configurable dropdown filtering so selectbox and multiselect can use fuzzy, contains, prefix, or disabled matching. This gives apps more precise search behavior for IDs and codes, and lets them turn off typing when filtering is unnecessary.
Copilot AI review requested due to automatic review settings March 26, 2026 15:34
@lukasmasuch lukasmasuch added change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users ai-review If applied to PR or issue will run AI review workflow labels Mar 26, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 26, 2026

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-14537/streamlit-1.55.0-py3-none-any.whl
📦 @streamlit/component-v2-lib Download from artifacts
🕹️ Preview app pr-14537.streamlit.app (☁️ Deploy here if not accessible)

@lukasmasuch
Copy link
Copy Markdown
Collaborator Author

Added Agent Docs

@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Mar 26, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Copy link
Copy Markdown

@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.

Fix All in Cursor

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new filter_mode option for st.selectbox and st.multiselect, wiring it end-to-end (Python API → protobuf → frontend filtering) to support "fuzzy" (default), "contains", "prefix", and disabling filtering/typing (None).

Changes:

  • Add filter_mode to Selectbox/MultiSelect protos and Python widget implementations (including validation + docs).
  • Add shared frontend filtering utilities + hook support to apply the selected filter mode consistently.
  • Add/extend typing, unit, frontend, and Playwright E2E coverage for the new modes.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
proto/streamlit/proto/Selectbox.proto Adds filter_mode field to Selectbox protobuf message.
proto/streamlit/proto/MultiSelect.proto Adds filter_mode field to MultiSelect protobuf message.
lib/streamlit/elements/widgets/selectbox.py Adds filter_mode param, validation, stable-ID behavior note, and proto wiring for st.selectbox.
lib/streamlit/elements/widgets/multiselect.py Adds filter_mode param, validation, stable-ID behavior note, and proto wiring for st.multiselect.
lib/tests/streamlit/elements/selectbox_test.py Adds unit coverage for default/none/invalid filter_mode and stable-ID whitelist behavior.
lib/tests/streamlit/elements/multiselect_test.py Adds unit coverage for default/none/invalid filter_mode and stable-ID whitelist behavior.
lib/tests/streamlit/typing/selectbox_types.py Adds typing assertions for filter_mode usage in selectbox.
lib/tests/streamlit/typing/multiselect_types.py Adds typing assertions for filter_mode usage in multiselect.
frontend/lib/src/util/fuzzyFilterSelectOptions.ts Adds normalization + new contains/prefix/none filtering behavior.
frontend/lib/src/util/fuzzyFilterSelectOptions.test.ts Adds unit tests for new filter modes and normalization fallback.
frontend/lib/src/hooks/useSelectCommon.ts Plumbs filterMode through shared select hook; disables typing when mode is none.
frontend/lib/src/components/shared/Dropdown/Selectbox.tsx Passes filterMode into shared dropdown Selectbox implementation.
frontend/lib/src/components/shared/Dropdown/Selectbox.test.tsx Adds tests for contains/prefix/none filtering behavior in shared Selectbox.
frontend/lib/src/components/widgets/Selectbox/Selectbox.tsx Passes backend filterMode through to shared dropdown Selectbox.
frontend/lib/src/components/widgets/Multiselect/Multiselect.tsx Passes backend filterMode into useSelectCommon for multiselect behavior.
frontend/lib/src/components/widgets/Multiselect/Multiselect.test.tsx Adds tests for contains filtering + none disabling typing/keeping options visible.
frontend/knip.json Updates Knip configuration.
e2e_playwright/st_selectbox.py Adds selectbox examples for prefix/contains/none modes.
e2e_playwright/st_selectbox_test.py Adds E2E tests for prefix/contains/none modes and updates widget count/initial expectations.
e2e_playwright/st_multiselect.py Adds multiselect examples for prefix/contains/none modes.
e2e_playwright/st_multiselect_test.py Adds E2E tests for prefix/contains/none modes and updates widget count/initial expectations.

@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Mar 26, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Summary

This PR adds a filter_mode parameter to st.selectbox and st.multiselect with four modes: "fuzzy" (default, preserving existing behavior), "contains" (case-insensitive substring), "prefix" (case-insensitive prefix), and None (disables typing/filtering). The implementation spans protobuf definitions, Python backend validation/serialization, a new centralized filterSelectOptions frontend function, the shared useSelectCommon hook, and both widget components. The feature closes #7238 and #6160.

Reviewer consensus: All three reviewers (claude-4.6-opus-high-thinking, gemini-3.1-pro, gpt-5.3-codex-high) approved the PR unanimously with no blocking issues. Feedback across reviewers was highly consistent.

Code Quality

The implementation is well-structured and follows existing codebase patterns. All three reviewers independently noted the clean architecture:

  • Centralized filtering logic in fuzzyFilterSelectOptions.ts with a filterSelectOptions dispatcher that delegates to the appropriate strategy. The normalizeSelectFilterMode function provides a safe default fallback for unknown values.
  • Proper widget identity management: filter_mode is correctly included in key_as_main_identity so the widget resets when the mode changes, consistent with how accept_new_options is handled.
  • Render-time derivation: normalizedFilterMode is computed at the top of the useSelectCommon hook (not in a useEffect), and isFilteringDisabled is derived inline — following React's "You Might Not Need an Effect" guidance.
  • useMemo dependency array for createFilterOptions correctly includes [normalizedFilterMode].
  • Validation logic in Python is clean and correctly handles the incompatibility between filter_mode=None and accept_new_options=True.

Minor observations (agreed by 2 of 3 reviewers):

  • The _validate_filter_mode function is nearly identical in selectbox.py and multiselect.py. This could be extracted to a shared utility, though the duplication is small.
  • The knip.json change (removing ignoreUnresolved for proto imports) appears unrelated to the filter_mode feature and would be cleaner verified or separated.

Test Coverage

All three reviewers agreed the test coverage is comprehensive and well-layered:

  • Python unit tests: Default value assertion, explicit mode setting, None serialization to "none", invalid mode rejection, incompatibility with accept_new_options=True, and widget identity changes via parameterized tests for both widgets.
  • TypeScript unit tests: contains, prefix, and none modes tested in both Selectbox.test.tsx and Multiselect.test.tsx, including readonly input assertion and negative assertions for non-matching options.
  • Frontend utility tests: Direct testing of filterSelectOptions for all four modes, plus normalizeSelectFilterMode for unknown values.
  • Typing tests: assert_type coverage for filter_mode parameter in both selectbox_types.py and multiselect_types.py.
  • E2E tests: Three new tests per widget covering prefix-only matching, contains matching with negative assertions, and None mode (readonly + dropdown still works). Element count constants updated correctly.

Backwards Compatibility

All three reviewers confirmed full backwards compatibility:

  • Default value is "fuzzy", preserving existing behavior for all users.
  • The protobuf field is optional string, so older frontends ignore it, and older backends that don't send it result in the frontend defaulting to "fuzzy" via normalizeSelectFilterMode.
  • No existing API signatures are changed; filter_mode is added as a new keyword-only argument with a default.

Security & Risk

All reviewers agreed: no security concerns identified. This is a purely UI/UX feature with no changes to routing, authentication, session management, WebSocket handling, dependencies, external requests, or dynamic code execution. The frontend gracefully falls back to "fuzzy" for unknown filter modes, improving mixed-version resilience.

External test recommendation

  • Recommend external_test: No
  • Triggered categories: None
  • Evidence: All changes are confined to widget filtering behavior (proto field addition, Python validation, frontend filter logic, and component props). No routing, auth, WebSocket, embedding, asset serving, CORS, security header, or runtime changes.
  • Confidence: High (unanimous across all reviewers)
  • Assumptions and gaps: None — the change surface is entirely within widget UI logic.

Accessibility

All reviewers confirmed good accessibility handling:

  • When filter_mode=None, the input correctly receives a readonly attribute, preventing typing while keeping the combobox role and ARIA label intact.
  • The dropdown remains navigable via keyboard with filter_mode=None (readonly on the input doesn't prevent dropdown interaction).
  • The select widget retains its accessible name (aria-label) regardless of filter mode.
  • No new interactive elements are introduced that would need additional ARIA attributes.
  • This is also a UX improvement for mobile, where filter_mode=None prevents the keyboard from popping up for short option lists.

Recommendations

  1. Consider extracting _validate_filter_mode into a shared utility (e.g., in streamlit/elements/lib/) to reduce near-identical duplication between selectbox.py and multiselect.py. The widget name for the error message could be passed as a parameter. (Flagged by 2 of 3 reviewers; non-blocking.)
  2. Verify the removal of "ignoreUnresolved" in frontend/knip.json was intentional and doesn't cause knip to fail. If unrelated, consider separating it into its own commit. (Flagged by 2 of 3 reviewers; non-blocking.)

Verdict

APPROVED: A clean, well-tested, and backwards-compatible feature addition with unanimous approval from all three reviewers. The filter mode is properly wired through all layers (proto, backend, frontend) with comprehensive test coverage, no security risks, and no regression concerns.


Consolidated AI review by claude-4.6-opus-high-thinking from 3 of 3 expected reviews (claude-4.6-opus-high-thinking, gemini-3.1-pro, gpt-5.3-codex-high). All expected models completed their reviews successfully.

This review also includes 2 inline comment(s) on specific code lines.

Centralize select widget filter_mode validation in shared option utilities and sync the related spec and feature-guide docs. This keeps the selectbox and multiselect code paths aligned while preserving the shipped behavior.
@lukasmasuch lukasmasuch added the ai-review If applied to PR or issue will run AI review workflow label Mar 26, 2026
- Align knip.json with develop so generated protobuf imports do not fail Knip on a clean tree.
- Use locale-invariant toLowerCase() for select option filter normalization (review feedback).
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Mar 26, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Summary

This PR adds a filter_mode parameter to st.selectbox and st.multiselect with four modes: "fuzzy" (default, preserving current behavior), "contains" (case-insensitive substring), "prefix" (case-insensitive prefix), and None (disables typing/filtering entirely). The implementation spans protobuf definitions, Python backend validation, frontend filtering logic in a shared hook, and comprehensive test coverage at all layers.

Reviewer agreement: All three reviewers (claude-4.6-opus-high-thinking, gemini-3.1-pro, gpt-5.3-codex-high) independently approved this PR, with strong consensus on code quality, test coverage, backwards compatibility, and absence of security concerns.

Code Quality

The implementation is clean and well-structured, following established Streamlit patterns:

  • Shared utilities: Backend validation is centralized in validate_select_widget_filter_mode in options_selector_utils.py. Frontend filtering is centralized in filterSelectOptions in fuzzyFilterSelectOptions.ts, and the shared useSelectCommon hook handles normalization and read-only state for both widgets.
  • Defensive normalization: normalizeSelectFilterMode falls back to "fuzzy" for unknown or missing values, ensuring forward/backward compatibility across Streamlit versions.
  • Widget identity: filter_mode is correctly added to key_as_main_identity for both widgets, with docstrings noting the state-reset behavior.
  • Unrelated changes: The knip.json change (removing ignoreUnresolved for proto imports) and wiki/new-feature-guide.md fix (yarn vitestyarn test) appear to be drive-by cleanups. The knip.json change should be verified to not cause regressions (one reviewer flagged this).

Test Coverage

All three reviewers agreed that test coverage is thorough and well-layered:

  • Python unit tests: Default value assertion, non-default modes, None"none" serialization, invalid mode rejection, None + accept_new_options=True incompatibility, and stable key kwargs.
  • Typing tests: filter_mode="contains" and filter_mode=None for both widgets.
  • Frontend unit tests: All four modes tested for Selectbox; Multiselect covers contains and none modes. One reviewer optionally suggested adding a prefix mode unit test for Multiselect for parity (covered in E2E).
  • E2E tests: Prefix, contains, and none modes tested for both widgets with good negative assertions.

Backwards Compatibility

No breaking changes. All three reviewers confirmed full backwards compatibility:

  • Default filter_mode="fuzzy" preserves current behavior exactly.
  • The parameter is keyword-only, so existing positional call signatures are unaffected.
  • Protobuf fields are additive optional, safely ignored by older frontends and normalized by newer ones.
  • The intentional widget-state reset on filter_mode change is documented in docstrings.

Security & Risk

No security concerns identified by any reviewer. All changes are scoped to widget-level filtering logic — no new dependencies, external requests, auth/session changes, or injection risks. The readonly attribute for filter_mode=None uses standard HTML input behavior.

External test recommendation

  • Recommend external_test: No
  • Triggered categories: None
  • Evidence: All changes are confined to widget-level filtering (protobuf fields, Python API parameters, frontend filter functions, shared hook). No routing, auth, WebSocket transport, embedding, asset serving, cross-origin behavior, security headers, or runtime integrations are modified.
  • Confidence: High (all three reviewers independently reached the same conclusion)
  • Assumptions and gaps: None

Accessibility

All three reviewers confirmed accessibility is maintained:

  • The readonly attribute on the input when filter_mode=None is semantically correct and properly communicated to assistive technology.
  • The dropdown remains fully accessible via click/keyboard interaction when filtering is disabled.
  • No changes to ARIA attributes, roles, or focus management. Existing accessibility patterns are preserved.

Recommendations

  1. Verify knip.json change: Confirm that removing the ignoreUnresolved pattern for proto imports doesn't cause make frontend-knip regressions, since this change appears unrelated to the feature. (Raised by 1 of 3 reviewers)
  2. Docstring formatting (optional): Consider using bullet points for the filter_mode literal options in both selectbox.py and multiselect.py per lib/streamlit/AGENTS.md guidelines. The current prose style matches the existing label_visibility pattern, so this is a low-priority consistency improvement. (Raised by 1 of 3 reviewers)
  3. Multiselect prefix unit test (optional): Adding a filterMode="prefix" unit test in Multiselect.test.tsx would provide parity with the contains-mode unit test, though this behavior is already covered in E2E tests. (Raised by 1 of 3 reviewers)

Verdict

APPROVED: This is a well-implemented, thoroughly tested, backwards-compatible feature addition that follows established Streamlit patterns. All three reviewers approved unanimously with no critical or blocking issues identified. The minor recommendations above are optional improvements.


Consolidated AI review by claude-4.6-opus-high-thinking from 3 individual reviews (claude-4.6-opus-high-thinking, gemini-3.1-pro, gpt-5.3-codex-high). All expected models completed their reviews successfully.

This review also includes 4 inline comment(s) on specific code lines.

@lukasmasuch lukasmasuch added the ai-review If applied to PR or issue will run AI review workflow label Mar 26, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Mar 26, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Summary

This PR adds a filter_mode parameter to st.selectbox and st.multiselect, supporting four modes: "fuzzy" (default, preserving current behavior), "contains" (case-insensitive substring match), "prefix" (case-insensitive prefix match), and None (disables typing/filtering entirely). The feature is implemented end-to-end across protobuf definitions, Python backend, and React frontend, with shared filtering logic centralized in useSelectCommon and fuzzyFilterSelectOptions.ts.

All three reviewers (claude-4.6-opus-high-thinking, gemini-3.1-pro, gpt-5.3-codex-high) completed their reviews. Two approved outright; one requested changes based on a single inline comment (see below).

Code Quality

Reviewers unanimously agreed the implementation is clean, well-structured, and follows existing codebase patterns:

  • Backend: Validation is centralized in validate_select_widget_filter_mode within options_selector_utils.py, keeping both widget modules thin. The SelectWidgetFilterMode type alias is well-defined and shared.
  • Frontend: normalizeSelectFilterMode in fuzzyFilterSelectOptions.ts provides a clean normalization boundary. The filterSelectOptions dispatcher delegates to strategy-specific logic, and useSelectCommon integrates filter mode with proper memoization. The inputReadOnly derivation is computed during render rather than via useEffect, correctly following codebase guidelines.
  • Protobuf: New optional string filter_mode fields with correct field numbers and updated // Next: comments.
  • Docstrings: Thorough, NumPy-style, and clearly document the filter_mode=None / accept_new_options=True incompatibility.

The filter_mode is included in key_as_main_identity for widget ID computation, correctly resetting the widget when the filter mode changes.

One inline comment was raised by gpt-5.3-codex-high regarding a TypeError when unhashable types (e.g., list, dict) are passed to filter_mode validation. This is technically valid — frozenset.__contains__ calls __hash__ — but is a low-impact robustness edge case: the parameter is typed as Literal["fuzzy", "contains", "prefix"] | None, so type checkers catch misuse, and even at runtime the user still receives an error (just TypeError instead of StreamlitAPIException). This is not a blocking issue but is retained as a suggestion in the inline comments.

Test Coverage

All reviewers agreed test coverage is thorough across all layers:

  • Python unit tests: Default value, each non-default mode, None"none" serialization, invalid mode rejection, None + accept_new_options=True error, and widget key identity changes via parameterized tests.
  • Typing tests: Verify filter_mode="contains" and filter_mode=None produce correct return types.
  • Frontend unit tests: Cover contains, prefix, and none modes for both Selectbox and Multiselect. The none mode tests verify readonly attribute and full option visibility. normalizeSelectFilterMode fallback behavior is tested for undefined and unknown values.
  • E2E tests: Cover prefix-only matching, contains substring matching, and filter_mode=None (readonly input + dropdown) for both widgets. Widget counts are properly incremented.

Minor observation (non-blocking): explicit test cases for normalizeSelectFilterMode("fuzzy") and normalizeSelectFilterMode(null) would improve documentation, though both fall through to the default case correctly.

Backwards Compatibility

All reviewers unanimously confirmed full backward compatibility:

  • filter_mode="fuzzy" is the default, producing identical behavior to current release.
  • Protobuf field is optional string — old frontends ignore it and continue fuzzy filtering; old backends that don't send it result in undefined, which normalizeSelectFilterMode maps to "fuzzy".
  • No breaking API changes; parameter is keyword-only with a sensible default.
  • Existing widgets without filter_mode continue to have the same element ID.

Security & Risk

No security concerns identified by any reviewer. Changes are purely UI filtering behavior — no impact on authentication, session management, WebSocket handling, file serving, CORS, or external networking. No new dependencies, no eval/exec, no HTML rendering changes.

External test recommendation

  • Recommend external_test: No
  • Triggered categories: None
  • Evidence:
    • All changes are confined to widget filtering behavior (protobuf field additions, Python parameter plumbing, frontend filter logic).
    • No changes to routing, authentication, WebSocket handling, embedding, asset serving, CORS, security headers, or storage.
    • Protobuf additions are additive optional fields with no impact on message compatibility.
  • Suggested external_test focus areas: N/A
  • Confidence: High (all three reviewers concur)
  • Assumptions and gaps: None

Accessibility

All reviewers agreed the accessibility impact is minimal and positive:

  • filter_mode=None uses readonly on the input, correctly preventing typing while keeping the combobox focusable and operable via keyboard (Enter/Space to open dropdown).
  • No changes to ARIA attributes, focus management, or keyboard navigation patterns.
  • Screen readers will continue to announce the combobox role and options correctly.

Recommendations

  1. (Optional) Guard against unhashable filter_mode inputs in validate_select_widget_filter_mode with a try/except TypeError wrapper around the in check, converting to StreamlitAPIException for a cleaner user-facing error. This is a minor robustness improvement, not a blocking concern.
  2. (Optional) Add explicit test cases for normalizeSelectFilterMode("fuzzy") and normalizeSelectFilterMode(null) in fuzzyFilterSelectOptions.test.ts to document common-path behavior.
  3. (Optional) Consider adding runnable Examples sections for filter_mode in the docstrings of both selectbox.py and multiselect.py.

Verdict

APPROVED: This is a well-designed, thoroughly tested, and fully backward-compatible feature addition. All three reviewers agree the implementation is clean and follows existing patterns. The single inline comment is a minor robustness suggestion (not blocking), and two of three reviewers approved outright. No critical or blocking issues were identified.


Consolidated AI review by claude-4.6-opus-high-thinking. Individual reviews: claude-4.6-opus-high-thinking (APPROVED), gemini-3.1-pro (APPROVED), gpt-5.3-codex-high (CHANGES_REQUESTED). Please verify the feedback and use your judgment.

This review also includes 1 inline comment(s) on specific code lines.

lukasmasuch and others added 5 commits March 27, 2026 09:35
Tighten filter_mode validation to return a Streamlit API error for unhashable inputs and strengthen the shared Selectbox tests with explicit negative assertions. This addresses the remaining actionable review feedback without changing widget behavior.
The filter_mode feature is now implemented, so remove it as an example
of a hypothetical feature addition from the spec documentation.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@lukasmasuch lukasmasuch added the ai-review If applied to PR or issue will run AI review workflow label Mar 27, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Mar 27, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Summary

This PR adds a filter_mode parameter to st.selectbox and st.multiselect, supporting four modes: "fuzzy" (default, existing behavior), "contains" (case-insensitive substring), "prefix" (case-insensitive prefix), and None (disables typing/filtering entirely). The implementation spans all layers: a new shared protobuf enum (SelectWidgetFilterMode), backend validation and serialization in Python, frontend filtering logic, and comprehensive test coverage across unit tests, typing tests, and E2E tests.

Code Quality

The implementation is clean and well-structured:

  • Protobuf: FILTER_MODE_FUZZY = 0 as the zero-value default is the correct choice for backwards compatibility. The enum is extracted into its own shared proto file and imported by both Selectbox.proto and MultiSelect.proto.
  • Backend: Validation is centralized in validate_select_widget_filter_mode() with clear error messages. The validation correctly handles unhashable inputs via TypeError catch and enforces the logical constraint that filter_mode=None is incompatible with accept_new_options=True. The type alias SelectWidgetFilterMode = Literal["fuzzy", "contains", "prefix"] | None provides good IDE support.
  • Frontend: The filtering logic is cleanly extracted into filterSelectOptions() which dispatches to the appropriate strategy. The useSelectCommon hook properly normalizes the filter mode and memoizes the filter function with normalizedFilterMode in the dependency array. The readonly input behavior for FILTER_MODE_NONE is implemented at the shared hook level, benefiting both widgets.
  • Docstrings: Parameter documentation follows the existing NumPy-style conventions and clearly describes each mode's behavior.

Minor documentation fix in wiki/new-feature-guide.md (yarn vitestyarn test) is a nice bonus.

Test Coverage

Test coverage is thorough across all layers:

  • Python unit tests: Cover default mode assertion, each non-default mode, invalid mode rejection, and the filter_mode=None + accept_new_options=True incompatibility for both widgets. Stable ID tests verify filter_mode is included in element identity but not in the key_as_main_identity whitelist. Typing tests (selectbox_types.py, multiselect_types.py) verify type inference.
  • Validation unit tests: Cover known mode validation and unhashable input rejection.
  • Frontend unit tests: Cover contains mode, prefix mode, and none mode (readonly input + all options visible) for both Selectbox and Multiselect components, plus the shared filterSelectOptions utility.
  • E2E tests: Cover prefix-only matching, contains-only matching, and none mode (readonly + selection still works) for both widgets. Tests include negative assertions (excluded options are absent).

Backwards Compatibility

This change is fully backwards compatible:

  • The default value of filter_mode is "fuzzy", preserving existing behavior for all current users.
  • The protobuf enum uses FILTER_MODE_FUZZY = 0 as the zero value, so messages from older backends (without the field) default to fuzzy mode on the frontend.
  • The frontend getSelectFilterMode() helper defaults null/undefined to FILTER_MODE_FUZZY, handling any edge cases with missing proto fields.
  • The parameter is keyword-only (added after *) in the public API, so it doesn't affect positional argument ordering.

Security & Risk

No security concerns identified:

  • No new dependencies (frontend or backend).
  • No external assets or network requests.
  • No eval/exec/Function() usage.
  • Filter logic is pure, in-memory string matching.
  • No changes to routing, auth, WebSocket, CORS, or CSP.
  • No changes to file serving or path handling.
  • The readonly input attribute for filter_mode=None is a standard HTML attribute with no security implications.

External test recommendation

  • Recommend external_test: No
  • Triggered categories: None
  • Evidence:
    • All changes are scoped to widget filtering behavior (selectbox/multiselect options filtering UI)
    • No changes to routing, auth, cookies, WebSocket, embedding, asset serving, CORS, security headers, or storage
    • No changes to lib/streamlit/web/, lib/streamlit/runtime/, or connection handling
  • Suggested external_test focus areas: N/A
  • Confidence: High
  • Assumptions and gaps: None — the change is entirely within widget rendering and filtering logic, with no external integration surface.

Accessibility

  • The readonly attribute on the input when filter_mode=None is the correct approach: the input remains focusable, screen readers still announce it as a combobox, and keyboard navigation continues to work. Only character input is suppressed.
  • No new interactive elements, ARIA attributes, or focus management changes are introduced.
  • The existing keyboard and screen reader accessibility of the selectbox/multiselect widgets is preserved.

Recommendations

  1. (Low priority) In filterSelectOptions(), the final return normalizedLabel.startsWith(normalizedPattern) acts as a catch-all for both FILTER_MODE_PREFIX and any unknown future enum values. Consider an explicit FILTER_MODE_PREFIX check for defensive clarity.
  2. (Low priority) test_validates_known_modes in options_selector_utils_test.py covers 2 of 4 modes. Consider @pytest.mark.parametrize to cover all four modes explicitly for completeness.

Verdict

APPROVED: A well-implemented, fully backwards-compatible feature addition with thorough test coverage across all layers. The code follows established patterns, protobuf design is sound, and no security or compatibility risks were identified.


This is an automated AI review by claude-4.6-opus-high-thinking.

This review also includes 2 inline comment(s) on specific code lines.

Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Summary

This PR adds a filter_mode parameter to st.selectbox and st.multiselect, supporting four modes: "fuzzy" (default, preserves existing behavior), "contains" (case-insensitive substring), "prefix" (case-insensitive prefix), and None (disables typing/filtering entirely). The implementation spans all layers: a new shared protobuf enum (SelectWidgetFilterMode), backend validation and serialization in Python, frontend filtering logic, and comprehensive test coverage across unit tests, typing tests, and E2E tests.

All three reviewers (claude-4.6-opus-high-thinking, gemini-3.1-pro, gpt-5.3-codex-high) approved the PR unanimously, with only minor, non-blocking suggestions.

Code Quality

Reviewers agree the implementation is clean, well-structured, and follows established codebase patterns across all layers:

  • Protobuf: FILTER_MODE_FUZZY = 0 as the zero-value default is the correct choice for backwards compatibility. The enum is extracted into its own shared proto file and imported by both Selectbox.proto and MultiSelect.proto.
  • Backend: Validation is centralized in validate_select_widget_filter_mode() with robust error handling (including unhashable inputs via TypeError catch) and the logical constraint that filter_mode=None is incompatible with accept_new_options=True.
  • Frontend: Filtering logic is cleanly extracted into filterSelectOptions() which dispatches to the appropriate strategy. The useSelectCommon hook properly normalizes the filter mode and the readonly input behavior for FILTER_MODE_NONE benefits both widgets at the shared hook level.
  • Docstrings: Follow existing NumPy-style conventions and clearly describe each mode's behavior.

Two reviewers (claude-4.6-opus-high-thinking, gpt-5.3-codex-high) noted that the filterSelectOptions() function falls through to prefix-matching behavior for unknown enum values. An explicit check for FILTER_MODE_PREFIX with a safe default would be more defensive for future-proofing (see inline comment).

Test Coverage

All reviewers agree that test coverage is thorough and well-layered:

  • Python unit tests: Cover default mode assertion, each non-default mode, invalid mode rejection, unhashable input rejection, and filter_mode=None + accept_new_options=True incompatibility for both widgets. Stable ID tests verify filter_mode is included in element identity.
  • Typing tests: assert_type checks for the new parameter in both selectbox_types.py and multiselect_types.py.
  • Frontend unit tests: Cover contains, prefix, and none mode (readonly input + all options visible) for both Selectbox and Multiselect components, plus the shared filterSelectOptions utility.
  • E2E tests: Cover prefix-only matching, contains-only matching, and none mode for both widgets. Tests include negative assertions (excluded options are absent).

One minor gap noted: test_validates_known_modes covers only 2 of 4 modes at the validation-function level (see inline comment).

Backwards Compatibility

All reviewers agree the change is fully backwards compatible:

  • Default filter_mode is "fuzzy", preserving existing behavior.
  • Protobuf zero value is FILTER_MODE_FUZZY, so messages from older backends default correctly.
  • The frontend getSelectFilterMode() helper defaults null/undefined to FILTER_MODE_FUZZY.
  • The parameter is keyword-only in the public API, so positional argument ordering is unaffected.

One additional observation (gpt-5.3-codex-high only): Unkeyed widget IDs now include filter_mode in identity inputs via compute_and_register_element_id, which could cause a one-time state reset for keyless select widgets after upgrade. This is a minor compatibility caveat worth noting in release notes, though it is standard practice for new parameters.

Security & Risk

All reviewers agree there are no security concerns:

  • No new dependencies, external requests, or risky runtime execution.
  • No changes to routing, auth, WebSocket, CORS, CSP, or file serving.
  • Filter logic is pure, in-memory string matching.
  • The readonly attribute for filter_mode=None is a standard HTML attribute.

External test recommendation

  • Recommend external_test: No
  • Triggered categories: None
  • Evidence: All changes are scoped to widget filtering behavior (selectbox/multiselect option filtering UI). No changes to routing, auth, cookies, WebSocket, embedding, asset serving, CORS, security headers, or storage.
  • Suggested external_test focus areas: N/A
  • Confidence: High (unanimous across all three reviewers)
  • Assumptions and gaps: None — the change is entirely within widget rendering and filtering logic.

Accessibility

All reviewers agree the approach is correct: the readonly attribute on the input when filter_mode=None preserves focusability, screen reader announcements, and keyboard navigation while suppressing only character input. No new interactive elements, ARIA attributes, or focus management changes are introduced.

Recommendations

  1. (Low) Consider an explicit FILTER_MODE_PREFIX case in filterSelectOptions() so unknown enum values don't silently degrade to prefix matching. Two of three reviewers flagged this.
  2. (Low) Consider using @pytest.mark.parametrize to cover all four modes in test_validates_known_modes for completeness.
  3. (Low) Consider a brief release note mentioning the potential one-time state reset for keyless select widgets due to identity input changes (raised by gpt-5.3-codex-high).

Verdict

APPROVED: All three reviewers unanimously approved this PR. The feature is well-implemented with clean code, sound protobuf design, thorough cross-layer test coverage, full backwards compatibility, and no security or accessibility concerns. Only minor, non-blocking improvements were suggested.


Consolidated AI review by claude-4.6-opus-high-thinking from 3/3 expected models (claude-4.6-opus-high-thinking, gemini-3.1-pro, gpt-5.3-codex-high). All models completed their reviews successfully.

This review also includes 2 inline comment(s) on specific code lines.

Copy link
Copy Markdown
Contributor

@sfc-gh-nbellante sfc-gh-nbellante left a comment

Choose a reason for hiding this comment

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

LGTM 👍

@lukasmasuch lukasmasuch merged commit b2c9ca4 into develop Mar 27, 2026
43 checks passed
@lukasmasuch lukasmasuch deleted the lukasmasuch/filter-mode-impl branch March 27, 2026 16:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

st.selectbox and st.multiselect strict matching to user input Add parameter to enable/disable text search in st.selectbox

3 participants