Skip to content

fix: preserve active tab selection after spinner reruns#14023

Merged
sfc-gh-bnisco merged 1 commit intodevelopfrom
02-19-fix_tabs_interaction_with_spinner
Feb 19, 2026
Merged

fix: preserve active tab selection after spinner reruns#14023
sfc-gh-bnisco merged 1 commit intodevelopfrom
02-19-fix_tabs_interaction_with_spinner

Conversation

@sfc-gh-bnisco
Copy link
Copy Markdown
Collaborator

@sfc-gh-bnisco sfc-gh-bnisco commented Feb 19, 2026

Describe your changes

Fixed a bug where tabs would lose their selection state when rendered after a spinner context. The issue occurred because transient nodes (like spinners) weren't properly handling child inheritance during reruns.

The fix modifies the AppRoot class to check if a node is a TransientNode and use its anchor node for child inheritance when replacing blocks. This ensures that tab selection state is preserved across reruns when tabs are rendered after a spinner.

Screenshot or video (only for visual changes)

GitHub Issue Link (if applicable)

Fixes #14018

Testing Plan

  • Unit Tests (JS and/or Python)

    • Added a unit test in AppRoot.test.ts to verify that children are properly inherited when replacing a block wrapped in a transient node
  • E2E Tests

    • Added a regression test scenario in st_spinner.py that reproduces the issue with tabs after a spinner
    • Added a corresponding test in st_spinner_test.py that verifies tab selection is preserved after widget interaction

Contribution License Agreement

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

@sfc-gh-bnisco sfc-gh-bnisco added the change:bugfix PR contains bug fix implementation label Feb 19, 2026 — with Graphite App
Copy link
Copy Markdown
Collaborator Author

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

@sfc-gh-bnisco sfc-gh-bnisco added the impact:users PR changes affect end users label Feb 19, 2026 — with Graphite App
@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Feb 19, 2026

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

Status Scanner 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 Feb 19, 2026

✅ PR preview is ready!

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

@sfc-gh-bnisco sfc-gh-bnisco added the ai-review If applied to PR or issue will run AI review workflow label Feb 19, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Feb 19, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Summary

This PR fixes a bug (#14018) where tabs lose their selection state when rendered after a st.spinner context. The root cause is that AppRoot.addBlock looks up the existing node at a delta path to determine whether to inherit children (preserving widget/React state across reruns). When a spinner wraps a tab container, the existing node is a TransientNode, not a BlockNode, so the child-inheritance check (instanceof BlockNode) fails and children are discarded — resetting the tab selection.

The fix adds a 4-line unwrap in addBlock: if the node at the delta path is a TransientNode, it accesses the underlying anchor (the original BlockNode) before the inheritance check. The change is accompanied by a unit test in AppRoot.test.ts and a regression E2E test in st_spinner_test.py.

Code Quality

The fix is minimal, well-placed, and follows existing patterns in the codebase.

  • AppRoot.ts (lines 426–435): The variable rename from existingNode to existingNodeAtPath + the derived existingNode is clear and self-documenting. The comment on lines 430–431 concisely explains the "why." The nullish coalescing fallback (anchor ?? existingNodeAtPath) is a safe default: if anchor is undefined, the TransientNode is used as-is and will fail the instanceof BlockNode check, resulting in an empty children array — the correct behavior when there's no prior block to inherit from.

  • AppRoot.test.ts (lines 431–504): The unit test directly reproduces the scenario (tab container → transient wrap → new tab container) and validates that children are inherited. It also verifies the tab labels on the inherited children, confirming the inheritance was correct. Follows existing test patterns in the same file.

  • st_spinner.py / st_spinner_test.py: The E2E test app uses session_state to persist the spinner-before-tabs scenario across reruns, which is a realistic reproduction of the bug. The test uses role-based locators (get_by_role("tab", ...), get_by_role("spinbutton", ...)), expect for auto-wait assertions, and includes both positive and negative aria-selected checks.

Test Coverage

The changes are well-covered:

  • Unit test verifies the core logic: when addBlock encounters a TransientNode wrapping a BlockNode of the same type, the children are inherited correctly. The test covers the full flow — creating tabs, wrapping in a transient, then replacing with a new tab container in a different script run.

  • E2E test validates the end-to-end user experience: clicking a tab, interacting with a widget, triggering a rerun, and confirming the tab selection persists. This directly captures the user-reported regression.

  • Both tests include negative assertions (tab_one is not selected when tab_two should be), which aligns with the project's testing guidelines.

Backwards Compatibility

No breaking changes. The fix only changes behavior when a TransientNode wraps a BlockNode that matches the incoming block type — a scenario that previously lost children (bug behavior). Non-transient nodes are unaffected (the ternary passes them through as-is). The fallback to existingNodeAtPath when anchor is undefined preserves existing behavior for anchorless transient nodes.

Security & Risk

No security concerns. This is a client-side render-tree state management fix with no impact on data handling, authentication, or network communication. The change is narrow in scope and low risk.

Accessibility

The fix is a net positive for accessibility: tab selection state is now correctly preserved across reruns when tabs follow a spinner context. This means keyboard/screen-reader users who select a tab won't have their selection unexpectedly reset after a rerun.

Recommendations

No blocking issues. Minor observations:

  1. The E2E test uses time.sleep(1) in the app script (st_spinner.py line 108) which is necessary to reproduce the spinner scenario but contributes to test runtime. This is acceptable given the nature of the test.

  2. Consider whether arrowAddRows in AppRoot.ts (line 522) could be similarly affected if a transient node wraps an element targeted by arrowAddRows. This seems unlikely in practice (data elements aren't typically wrapped by spinners), but worth noting as a theoretical edge case for future reference.

Verdict

APPROVED: Clean, minimal fix for a real user-reported regression with strong unit and E2E test coverage. The change is safe, backwards-compatible, and follows existing codebase patterns.


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

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 fixes a regression introduced in Streamlit v1.53 where tab selection state was lost when tabs were rendered after a spinner context. The root cause was that TransientNode wrappers (used by spinners) weren't being properly unwrapped when checking for child inheritance during block replacement.

Changes:

  • Modified AppRoot.addBlock() to unwrap TransientNodes and use their anchor node for child inheritance decisions
  • Added comprehensive unit test verifying child inheritance when replacing blocks wrapped in transient nodes
  • Added E2E regression test confirming tab selection preservation after spinner completion

Reviewed changes

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

File Description
frontend/lib/src/render-tree/AppRoot.ts Fixed child inheritance logic to unwrap TransientNodes and use their anchor node when replacing blocks
frontend/lib/src/render-tree/AppRoot.test.ts Added unit test verifying that tab children are properly inherited when replacing a block wrapped in a transient node
e2e_playwright/st_spinner_test.py Added E2E test verifying tab selection and widget interaction work correctly after spinner completes
e2e_playwright/st_spinner.py Added test scenario reproducing the bug: spinner followed by tabs with widgets

@sfc-gh-bnisco sfc-gh-bnisco changed the title fix: Tabs interaction with Spinner fix: preserve active tab selection after spinner reruns Feb 19, 2026
@sfc-gh-bnisco sfc-gh-bnisco force-pushed the 02-19-fix_tabs_interaction_with_spinner branch from dfbdd0d to 63c3dd3 Compare February 19, 2026 17:35
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

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@sfc-gh-bnisco sfc-gh-bnisco marked this pull request as ready for review February 19, 2026 17:57
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

@sfc-gh-bnisco sfc-gh-bnisco merged commit 65887d2 into develop Feb 19, 2026
50 checks passed
@sfc-gh-bnisco sfc-gh-bnisco deleted the 02-19-fix_tabs_interaction_with_spinner branch February 19, 2026 21:18
lukasmasuch pushed a commit that referenced this pull request Feb 20, 2026
## Describe your changes

Fixed a bug where tabs would lose their selection state when rendered
after a spinner context. The issue occurred because transient nodes
(like spinners) weren't properly handling child inheritance during
reruns.

The fix modifies the AppRoot class to check if a node is a TransientNode
and use its anchor node for child inheritance when replacing blocks.
This ensures that tab selection state is preserved across reruns when
tabs are rendered after a spinner.

## Screenshot or video (only for visual changes)

## GitHub Issue Link (if applicable)

Fixes #14018

## Testing Plan

- Unit Tests (JS and/or Python)
- Added a unit test in AppRoot.test.ts to verify that children are
properly inherited when replacing a block wrapped in a transient node

- E2E Tests
- Added a regression test scenario in st_spinner.py that reproduces the
issue with tabs after a spinner
- Added a corresponding test in st_spinner_test.py that verifies tab
selection is preserved after widget interaction

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this
project are made under the Apache 2.0 license.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:bugfix PR contains bug fix implementation impact:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Widgets in tabs are destroyed/refreshed if placed after a Spinner, as of v1.53

3 participants