Skip to content

[feature] Add required parameter to st.pills and st.segmented_control#14414

Merged
lukasmasuch merged 6 commits intodevelopfrom
lukasmasuch/selectbox-required
Mar 23, 2026
Merged

[feature] Add required parameter to st.pills and st.segmented_control#14414
lukasmasuch merged 6 commits intodevelopfrom
lukasmasuch/selectbox-required

Conversation

@lukasmasuch
Copy link
Copy Markdown
Collaborator

@lukasmasuch lukasmasuch commented Mar 18, 2026

Describe your changes

Adds required: bool = False parameter to st.pills and st.segmented_control:

  • Prevents deselection in single-select mode when required=True (clicking already-selected option does nothing)

  • Raises StreamlitAPIException when required=True with selection_mode="multi"

  • Provides return type narrowing via @overload when required=True and default is set (returns V instead of V | None)

  • Product spec

GitHub Issue Link (if applicable)

Testing Plan

  • Unit Tests (Python): lib/tests/streamlit/elements/button_group_test.py — Tests required parameter behavior
  • Unit Tests (TypeScript): frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.test.tsx — Tests deselection prevention
  • Type Tests: lib/tests/streamlit/typing/pills_types.py, segmented_control_types.py — Tests return type narrowing
  • E2E Tests: e2e_playwright/st_pills_test.py, st_segmented_control_test.py — Tests required behavior

Contribution License Agreement

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.

Agent metrics
Type Name Count
skill checking-changes 1
skill finalizing-pr 1
skill implementing-feature 1
skill updating-internal-docs 1
subagent fixing-pr 1
subagent general-purpose 3
subagent reviewing-local-changes 1
subagent simplifying-local-changes 1

Adds `required: bool = False` parameter that prevents deselection in
single-select mode when True. Raises StreamlitAPIException when used
with multi-select mode. Includes type narrowing via @overload.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@lukasmasuch lukasmasuch added change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users labels Mar 18, 2026
Copilot AI review requested due to automatic review settings March 18, 2026 14:10
@lukasmasuch lukasmasuch added change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users labels Mar 18, 2026
@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Mar 18, 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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 18, 2026

✅ PR preview is ready!

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

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

Adds a new required parameter to Streamlit’s button-group widgets (st.pills, st.segmented_control) to support non-deselectable single-select behavior, with corresponding backend validation, proto wiring, typing overloads, and test coverage across Python, frontend, and E2E.

Changes:

  • Extend ButtonGroup protobuf + Python widget plumbing to carry required and reject required=True with selection_mode="multi".
  • Add Python typing overloads and typing tests to narrow return types when required=True and a non-None default is provided.
  • Update the frontend ButtonGroup widget behavior and add unit + E2E tests for required/no-deselect flows.

Reviewed changes

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

Show a summary per file
File Description
proto/streamlit/proto/ButtonGroup.proto Adds required field to the ButtonGroup proto so the flag can reach the frontend.
lib/streamlit/elements/widgets/button_group.py Threads required through widget creation, adds validation, and introduces overloads/doc updates for the public API.
lib/tests/streamlit/elements/button_group_test.py Adds Python unit tests verifying proto propagation and backend validation for required.
lib/tests/streamlit/typing/pills_types.py Adds mypy typing assertions for pills return-type narrowing with required.
lib/tests/streamlit/typing/segmented_control_types.py Adds mypy typing assertions for segmented_control return-type narrowing with required.
frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.tsx Implements required-mode no-deselect logic in the widget click handling.
frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.test.tsx Adds frontend unit tests covering required vs not-required single-select interactions.
e2e_playwright/st_pills.py Adds a pills demo section for required behavior to the E2E app script.
e2e_playwright/st_pills_test.py Adds E2E coverage for required pills behavior.
e2e_playwright/st_segmented_control.py Adds a segmented control demo section for required behavior to the E2E app script.
e2e_playwright/st_segmented_control_test.py Adds E2E coverage for required segmented control behavior.
Comments suppressed due to low confidence (2)

lib/streamlit/elements/widgets/button_group.py:333

  • The single-select overloads restrict required to Literal[True] / Literal[False]. That prevents valid calls like required=is_required where is_required: bool is not a literal, because no overload matches. Add an overload (or relax the existing one) that accepts required: bool = False for single-select so non-literal booleans type-check while still keeping the Literal[True]+default narrowing overload.
        label: str,
        options: OptionSequence[V],
        *,
        selection_mode: Literal["single"] = "single",
        default: V | None = None,
        required: Literal[False] = ...,
        format_func: Callable[[Any], str] | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | None = None,
        args: WidgetArgs | None = None,

lib/streamlit/elements/widgets/button_group.py:657

  • Same overload issue as pills: segmented_control single-select overloads only accept required as Literal[True]/Literal[False], so required=some_bool won’t match any overload. Add a required: bool = False single-select overload (returning V | None) to keep the API usable with non-literal booleans while preserving the narrowing case.
    def segmented_control(
        self,
        label: str,
        options: OptionSequence[V],
        *,
        selection_mode: Literal["single"] = "single",
        default: V | None = None,
        required: Literal[False] = ...,
        format_func: Callable[[Any], str] | None = None,
        key: str | int | None = None,
        help: str | None = None,
        on_change: WidgetCallback | None = None,
        args: WidgetArgs | None = None,

…uired=true

When required=true and the user clicks an already-selected option,
skip the setValueWithSource call to avoid triggering an unnecessary
backend rerun. The test is also updated to verify that no widget
update is sent in this case.

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 18, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Mar 18, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Summary

This PR adds a required parameter to st.pills and st.segmented_control. When required=True in single-select mode, deselection is prevented — clicking an already-selected option is a no-op. Using required=True with selection_mode="multi" raises a StreamlitAPIException. The PR also provides return type narrowing via @overload: when required=True and a non-None default is provided, the return type is narrowed from V | None to V.

Changes span all layers: protobuf definition, Python backend, TypeScript frontend, Python unit tests, TypeScript unit tests, mypy type tests, and E2E tests.

Code Quality

All three reviewers agree the implementation is clean, well-structured, and follows existing Streamlit codebase conventions.

  • Protobuf (proto/streamlit/proto/ButtonGroup.proto): Additive field bool required = 16 with correct next id: 17 comment. This is the correct compatibility-preserving approach.
  • Python backend (lib/streamlit/elements/widgets/button_group.py): The required parameter is cleanly threaded through the call chain (pills()_internal_button_group()_button_group()_build_proto()). Validation is placed early, before widget registration. required is correctly excluded from compute_and_register_element_id since it's a behavioral flag. The overload design (4 overloads per command) is thoughtful and handles all type narrowing scenarios correctly.
  • TypeScript frontend (frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.tsx): handleSelection() correctly returns the same array reference when deselection is prevented. The onClick callback uses reference equality to detect no-op clicks and returns early, avoiding unnecessary state updates and backend reruns. The required dependency is correctly included in useCallback.

No blocking code-quality issues found by any reviewer.

Test Coverage

All reviewers rate test coverage as excellent/comprehensive across all layers:

  • Python unit tests (lib/tests/streamlit/elements/button_group_test.py): Cover proto field propagation, default values, the StreamlitAPIException for multi-select, and that required=False with multi-select is allowed.
  • TypeScript unit tests (frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.test.tsx): Verify deselection allowed when required=false, deselection prevented when required=true, and selection change allowed when required=true.
  • Type tests (lib/tests/streamlit/typing/pills_types.py, segmented_control_types.py): Verify all four key overload scenarios for return type narrowing.
  • E2E tests (e2e_playwright/st_pills_test.py, st_segmented_control_test.py): Three tests each for pills and segmented_control covering: required with default, required without default, and not-required allowing deselection.

Backwards Compatibility

Unanimous agreement: Fully backwards compatible. The required parameter defaults to False, preserving existing behavior. The protobuf field defaults to false in proto3. No existing overloads or return types are changed.

Security & Risk

Unanimous agreement: No security concerns. Changes are confined to widget UI state management — no new endpoints, routes, auth, CORS, or external requests. No new dependencies added. Regression risk is low.

External test recommendation

  • Recommend external_test: No (unanimous)
  • Triggered categories: None
  • Evidence: Changes are limited to widget behavior (protobuf field, Python parameter, TypeScript click handler). No routing, auth, WebSocket, embedding, asset serving, CORS, security header, or runtime integration changes.
  • Confidence: High

Accessibility

Unanimous agreement: No accessibility regressions. The change modifies click behavior only and does not alter DOM structure, roles, labeling, or focus behavior. All reviewers agree existing accessibility posture is maintained.

Reviewer Agreement & Disagreements

Full agreement across all three reviewers:

  • Implementation is clean and follows existing patterns
  • Test coverage is comprehensive across all layers
  • Fully backwards compatible
  • No security concerns
  • No external test coverage needed
  • No accessibility regressions
  • Verdict: APPROVED

No disagreements identified

All three reviewers independently reached the same conclusions on all major assessment areas.

Complementary (non-conflicting) recommendations:

Recommendation Reviewer Priority
Consider Literal[False] for required in multi-select overload to catch invalid combos at type-check time gemini-3.1-pro Optional (acknowledged it may not work due to fallback overload)
Add typing negative test for required=True + selection_mode="multi" gpt-5.3-codex-high Optional follow-up
Consolidate E2E tests into aggregated scenarios to reduce browser loads opus-4.6-thinking Optional (non-blocking)
Add aria-required for screen readers as future enhancement opus-4.6-thinking Optional (future follow-up)

Recommendations

  1. Optional follow-up: Consider adding a typing negative test to explicitly document that required=True with selection_mode="multi" is invalid at runtime (gpt-5.3-codex-high, gemini-3.1-pro).
  2. Optional optimization: The three separate E2E test functions per widget could be combined into a single aggregated scenario test to reduce browser loads per E2E guidance (opus-4.6-thinking).
  3. Optional future enhancement: Consider adding aria-required="true" on the button group when required=True to improve the assistive technology experience (opus-4.6-thinking).

None of these are blocking.

Verdict

APPROVED: All three reviewers unanimously approve. The implementation is clean, well-tested across all layers (proto, Python, TypeScript, E2E, type tests), fully backwards compatible, and poses no security, accessibility, or regression risk. The type narrowing via overloads is a nice developer experience improvement.


This is a consolidated AI review by opus-4.6-thinking. Individual reviews from 3/3 expected models were received and synthesized.


📋 Review by `gemini-3.1-pro`

Summary

Adds a required parameter to st.pills and st.segmented_control. When required=True in single-select mode, users are prevented from deselecting an option once it is selected. It also raises an exception if required=True is used with selection_mode="multi", and provides proper type narrowing so that required=True with a default value returns V instead of V | None.

Code Quality

The code is well-structured and follows Streamlit's conventions. The frontend implementation correctly prevents unnecessary state updates and backend reruns when a user clicks an already-selected option with required=True (frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.tsx). The Python backend plumbing for the new parameter is clean and includes appropriate validation (lib/streamlit/elements/widgets/button_group.py).

Test Coverage

Test coverage is excellent:

  • Python unit tests cover the proto field assignment and the exception for multi-select (lib/tests/streamlit/elements/button_group_test.py).
  • TypeScript unit tests verify that deselection is prevented when required=True (frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.test.tsx).
  • E2E tests cover the behaviour for both st.pills and st.segmented_control with and without default values (e2e_playwright/st_pills_test.py, e2e_playwright/st_segmented_control_test.py).
  • Type tests correctly verify the return type narrowing (lib/tests/streamlit/typing/pills_types.py, lib/tests/streamlit/typing/segmented_control_types.py).

Backwards Compatibility

The changes are fully backwards compatible. The required parameter defaults to False, which preserves the existing behaviour where options can be deselected.

Security & Risk

No security concerns or regression risks identified. The changes are confined to the widget's UI state management and do not interact with sensitive areas like routing, auth, or cross-origin behaviour.

External test recommendation

  • Recommend external_test: No
  • Triggered categories: None
  • Evidence:
    • The changes only modify the internal state management of the ButtonGroup widget and add a new protobuf field.
    • No changes to routing, auth, websocket transport, or asset serving.
  • Suggested external_test focus areas: N/A
  • Confidence: High
  • Assumptions and gaps: None

Accessibility

The changes do not negatively impact accessibility. While standard HTML form validation (aria-required) is not currently implemented for these widgets, the visual and interactive behaviour aligns with the expected behaviour of the required parameter in Streamlit.

Recommendations

No required changes. The implementation is solid.

One minor observation: In the multi-select overload for both pills and segmented_control (lib/streamlit/elements/widgets/button_group.py), required: bool = False is used. While this allows required=True to pass type checking (only to fail at runtime), changing it to required: Literal[False] = ... could potentially catch this at the type-checking level. However, given the fallback overload, mypy would likely just fall back to returning list[V] | V | None anyway, so the current approach is acceptable and the runtime exception provides clear feedback.

Verdict

APPROVED: The implementation is clean, well-tested, and backwards compatible.


This is an automated AI review by gemini-3.1-pro.

📋 Review by `gpt-5.3-codex-high`

Summary

This PR adds a new required parameter to st.pills and st.segmented_control with end-to-end support across protobuf, backend API/runtime validation, frontend interaction logic, and tests.
Behavior is implemented so single-select widgets can prevent deselection when required=True, while required=True with selection_mode="multi" now raises a StreamlitAPIException.

Code Quality

The implementation is clean and consistent with existing patterns:

  • Backend API and docs are updated in lib/streamlit/elements/widgets/button_group.py with explicit parameter plumbing and runtime validation (forbidden required=True + multi-select combination).
  • Frontend behavior is implemented in frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.tsx with a focused selection handler update and a no-op guard that avoids unnecessary reruns when selection does not change.
  • Proto evolution is additive (required = 16) in proto/streamlit/proto/ButtonGroup.proto, which is the correct compatibility-preserving approach.

No blocking code-quality issues found.

Test Coverage

Coverage is strong and appropriately layered for this change:

  • Python unit tests in lib/tests/streamlit/elements/button_group_test.py validate proto field propagation and invalid required=True + selection_mode="multi" behavior.
  • Frontend unit tests in frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.test.tsx cover deselection behavior for required/non-required single-select modes.
  • Typing tests in lib/tests/streamlit/typing/pills_types.py and lib/tests/streamlit/typing/segmented_control_types.py cover return-type narrowing scenarios.
  • E2E tests in e2e_playwright/st_pills_test.py and e2e_playwright/st_segmented_control_test.py validate user-visible behavior in real app flows.

I did not run tests in this review (per instruction), but test additions are comprehensive for the changed behavior.

Backwards Compatibility

Backwards compatibility looks good:

  • required is optional and defaults to False, preserving existing behavior.
  • The protobuf change is additive and does not repurpose existing fields.
  • Existing call sites without required are unaffected.

No breaking API changes identified.

Security & Risk

No security-sensitive surfaces were modified (no auth/session/cookie/CORS/routing/upload/path handling changes).
Risk is low and primarily functional (widget selection behavior), with runtime guardrails added in backend validation.

External test recommendation

  • Recommend external_test: No
  • Triggered categories: None
  • Key evidence from changed files:
    • lib/streamlit/elements/widgets/button_group.py: widget-level parameter validation and proto plumbing only.
    • frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.tsx: local selection state behavior only.
    • proto/streamlit/proto/ButtonGroup.proto: additive widget proto field, no routing/network/auth/embed behavior.
  • Suggested external test focus areas: N/A for this PR (no external-hosting or embedding boundary changes detected).
  • Confidence: High
  • Assumptions/gaps: Assessment is based on the diff for this PR only; no hidden cross-PR infrastructure changes were considered.

Accessibility

No accessibility regressions identified from this change:

  • The frontend update modifies selection state logic only and does not change interactive semantics, roles, labeling, or focus behavior.
  • Existing accessibility posture of ButtonGroup remains intact.

Recommendations

  1. Optional follow-up: add a typing negative test/assertion to explicitly document that required=True with selection_mode="multi" is invalid at runtime (to keep runtime/type expectations tightly aligned over time).

Verdict

APPROVED: The implementation is coherent, well-tested across layers, backward compatible, and ready to merge.


This is an automated AI review by gpt-5.3-codex-high.

📋 Review by `opus-4.6-thinking`

Summary

This PR adds a required: bool = False parameter to st.pills and st.segmented_control. When required=True in single-select mode, the widget prevents deselection — clicking an already-selected option is a no-op. Using required=True with selection_mode="multi" raises a StreamlitAPIException. The PR also provides return type narrowing via @overload: when required=True and a non-None default is provided, the return type is narrowed from V | None to V.

Changes span all layers: protobuf definition, Python backend, TypeScript frontend, Python unit tests, TypeScript unit tests, mypy type tests, and E2E tests.

Code Quality

The implementation is clean, well-structured, and follows existing patterns in the codebase.

Protobuf (proto/streamlit/proto/ButtonGroup.proto): Field bool required = 16 is correctly added with next id: 17 updated. The comment clearly describes the semantics.

Python backend (lib/streamlit/elements/widgets/button_group.py):

  • The required parameter is threaded cleanly through pills()_internal_button_group()_button_group()_build_proto().
  • Validation is placed early in _internal_button_group() (line 952), preventing invalid combinations before any widget registration occurs.
  • required is correctly excluded from compute_and_register_element_id since it's a behavioral flag (like disabled) that shouldn't affect widget identity.
  • The overload design is thoughtful: 4 overloads per command handle required=True with default (→ V), required=True without default (→ V | None), default case (→ V | None), and multi-select (→ list[V]). The use of Literal[False] = ... for the default overload is correct mypy convention.
  • Docstrings follow NumPy style and clearly describe the behavior and constraints.

TypeScript frontend (frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.tsx):

  • handleSelection() (line 117) cleanly handles the required flag by returning the same reference (currentSelection) when deselection is prevented.
  • The onClick callback (line 353) uses reference equality (newSelected === value) to detect no-op clicks and returns early, which correctly avoids unnecessary state updates and backend reruns.
  • required is properly included in the useCallback dependency array (line 369).

Test Coverage

Test coverage is thorough across all layers:

Python unit tests (lib/tests/streamlit/elements/button_group_test.py):

  • Tests default value (required defaults to False), proto field setting for both True/False, required with a default value, the StreamlitAPIException for multi-select, and that required=False with multi-select is allowed.
  • Uses @parameterized.expand to test both st.pills and st.segmented_control — consistent with the legacy class-based test style in this file.

TypeScript unit tests (frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.test.tsx):

  • Tests deselection allowed when required=false, deselection prevented when required=true (asserting no additional setStringArrayValue call), and selection change allowed when required=true.

Type tests (lib/tests/streamlit/typing/pills_types.py, segmented_control_types.py):

  • Verify all four key overload scenarios: required=FalseV | None, default=V, required=TrueV, required=True (no default) → V | None, default=None, required=TrueV | None.

E2E tests (e2e_playwright/st_pills_test.py, st_segmented_control_test.py):

  • Three tests each cover: required with default (prevents deselection, allows changing), required without default (allows initial selection, prevents deselection), and not-required allows deselection.
  • Tests use get_element_by_key and get_pill_button/get_segment_button locators, following the recommended locator priority from the E2E AGENTS.md.

Minor observation: The E2E AGENTS.md recommends preferring aggregated scenario tests over micro-tests when they share the same page state. The three tests per widget (test_required_*_behavior, test_required_*_without_default, test_not_required_*_allows_deselection) interact with different widgets on the same page and could be merged into a single scenario test to reduce browser loads. This is a minor optimization opportunity, not a blocker.

Backwards Compatibility

Fully backwards compatible. The required parameter defaults to False, preserving existing behavior for all current users. The new protobuf field bool required = 16 defaults to false in proto3, so existing messages are unaffected. No existing overloads or return types are changed.

Security & Risk

No security concerns identified. The changes are:

  • A new boolean parameter on existing widgets, no new endpoints or routes.
  • Frontend logic is purely a click handler guard — no DOM manipulation, no eval, no external requests.
  • No new dependencies added.
  • Protobuf field is a simple boolean with no trust implications.

Regression risk is low. The required=False default ensures all existing code paths are unaffected. The early-return optimization in the onClick handler (reference equality check) is safe because handleSelection returns the same array reference only when deselection is prevented.

External test recommendation

  • Recommend external_test: No
  • Triggered categories: None
  • Evidence:
    • Changes are limited to widget behavior (protobuf field, Python parameter, TypeScript click handler).
    • No routing, auth, WebSocket, embedding, asset serving, CORS, security header, or runtime integration changes.
  • Suggested external_test focus areas: N/A
  • Confidence: High
  • Assumptions and gaps: None. This is a pure widget feature addition with no network, auth, embedding, storage, or cross-origin impact.

Accessibility

No accessibility concerns. The required parameter does not change the visual appearance or DOM structure of the widget. It only modifies click behavior (preventing deselection). The buttons remain fully keyboard-accessible and screen-reader-compatible. No aria-* attributes need updating since required in this context means "cannot deselect" rather than the HTML form validation concept of required.

Recommendations

  1. Consider consolidating E2E tests (minor): The three separate E2E test functions per widget (test_required_pills_behavior, test_required_pills_without_default, test_not_required_pills_allows_deselection) could be combined into a single aggregated scenario test to reduce browser loads, per E2E AGENTS.md guidance. They all operate on different widgets within the same page and don't conflict. Same applies to st_segmented_control_test.py. This is optional and non-blocking.

  2. Consider adding aria-required for screen readers (optional, future enhancement): While not strictly necessary for the current "prevent deselection" semantics, setting aria-required="true" on the button group when required=True and a default is set could improve the accessibility experience by signaling to assistive technology that a selection is expected. This could be done in a follow-up.

Verdict

APPROVED: Clean, well-tested feature addition that is fully backwards compatible, follows existing codebase patterns, and poses no security or regression risk. The type narrowing via overloads is a nice developer experience improvement.


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

lukasmasuch and others added 2 commits March 18, 2026 16:04
- Add typing comments documenting runtime validation for required=True
  with selection_mode="multi"
- Consolidate E2E tests into aggregated scenario tests to reduce browser loads
- Add aria-required attribute for screen reader accessibility

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 19, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Mar 19, 2026
@streamlit streamlit deleted a comment from github-actions bot Mar 20, 2026
@lukasmasuch lukasmasuch added the ai-review If applied to PR or issue will run AI review workflow label Mar 20, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Mar 20, 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.

Consolidated Review — required parameter for st.pills and st.segmented_control

Summary

This PR adds a required: bool = False parameter to st.pills and st.segmented_control that prevents deselection in single-select mode. The implementation spans backend (Python), frontend (TypeScript/React), protobuf, type overloads, and comprehensive tests across all layers (Python unit, TypeScript unit, typing, and E2E).

Reviewer Consensus

Reviewer Verdict Key Observations
gemini-3.1-pro APPROVED Clean implementation, excellent test coverage, proper accessibility
gpt-5.3-codex-high CHANGES REQUESTED Raised one correctness concern about clearable=True + bind="query-params" + required=True type contract
opus-4.6-thinking APPROVED Thorough coverage, good frontend optimization, no blocking issues

All three reviewers agreed on:

  • Code quality is high and follows existing Streamlit patterns
  • Test coverage is thorough across all layers (Python unit, TS unit, typing, E2E)
  • Backwards compatibility is fully preserved (required defaults to False)
  • No security concerns or regression risks
  • Accessibility is handled correctly (aria-required attribute)
  • No external test coverage needed
  • The @overload signatures are well-structured with correct precedence

Point of disagreement:
One reviewer (gpt-5.3-codex-high) raised a concern about the -> V type guarantee being potentially violated when bind="query-params" is used alongside required=True and a non-None default. The widget's _button_group method unconditionally passes clearable=True to register_widget, meaning an empty query parameter could theoretically seed the widget to None at registration time, breaking the type contract. The other two reviewers did not flag this.

Resolution: After independent verification, this concern is valid but not blocking. The clearable=True is pre-existing code (not introduced by this PR), and the scenario requires a specific three-way parameter combination (required=True + default=V + bind="query-params") combined with URL manipulation. The established codebase pattern (used by selectbox, radio, etc.) is to conditionally set clearable based on whether the widget can be in an empty state. This should be addressed — either in this PR or as a tracked follow-up — but it does not block the core feature, which works correctly for all standard usage patterns. This is captured as an inline comment.

Code Quality

The implementation is clean and idiomatic:

  • The required parameter is correctly excluded from compute_and_register_element_id, consistent with how disabled is handled.
  • The frontend optimization of skipping state updates via reference equality (returning the same array from handleSelection) prevents unnecessary backend reruns.
  • The runtime validation for required=True + selection_mode="multi" provides a clear, actionable error message.
  • Docstrings follow the established NumPy-style convention.

Test Coverage

Test coverage is comprehensive and follows repository guidelines:

  • Python unit tests: RequiredParameterTest covers default values, proto field assignment, required-with-default, multi-select rejection, and required=False+multi-select allowance. Uses @parameterized.expand for both st.pills and st.segmented_control.
  • TypeScript unit tests: Cover deselection prevention, selection changes, deselection when not required, and aria-required attribute presence/absence. Includes negative assertion (toHaveBeenCalledTimes(1)) confirming setStringArrayValue is not called unnecessarily.
  • Type tests: Verify return type narrowing for all relevant combinations. Document the known limitation that required=True + selection_mode="multi" cannot be caught at type-check time.
  • E2E tests: Aggregated scenario tests reduce browser loads, covering required-with-default, required-without-default, and not-required baselines for both widgets.

Verdict

APPROVED — Well-implemented feature with thorough test coverage, correct accessibility handling, full backwards compatibility, and no security concerns. The clearable interaction with bind="query-params" is a valid edge case noted inline that should be tracked for follow-up.


Consolidated review by opus-4.6-thinking from 3 of 3 expected models (gemini-3.1-pro, gpt-5.3-codex-high, opus-4.6-thinking). No models failed to complete review.

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

The spec document is no longer needed as part of this feature branch.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@lukasmasuch lukasmasuch merged commit fe2d0cf into develop Mar 23, 2026
43 checks passed
@lukasmasuch lukasmasuch deleted the lukasmasuch/selectbox-required branch March 23, 2026 20:02
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.

pills and segmented_control with a required value

3 participants