fix(tabs): a tab title that changes after the first render reaches the strip #12

Merged
bjoern merged 1 commit from fix/tab-title-refresh into main 2026-08-05 12:34:25 +02:00
Member

The tab strip renders in the owner's (Tabs') render pass, but a Tab's updated Title only lands when the Tab child re-renders — after the strip. So a live title, like TagAuthoring's "Positive (n)" counts in Kagura, stayed one render stale: on a page that loads its document asynchronously, the badge stuck at "(0)" while three tags sat in the list, until some unrelated interaction happened to redraw the owner.

A Tab now nudges its owner (Owner.Refresh()) from OnParametersSet when its Title changed. The nudge is self-terminating: the refresh re-renders the owner, the child re-renders with the same title, and no further refresh fires.

Pinned by a test that renders a tab titled "Positive (0)", re-renders the parent with "Positive (3)", and asserts the strip button caught up — red without the fix, green with it (verified both ways). Full suite 288 green.

Found while browser-verifying Kagura's CG editor base mode, where the load produces exactly one re-render after the tags arrive — nothing ever corrected the strip.

🤖 Generated with Claude Code

The tab strip renders in the owner's (`Tabs`') render pass, but a `Tab`'s updated `Title` only lands when the Tab child re-renders — *after* the strip. So a live title, like `TagAuthoring`'s "Positive (n)" counts in Kagura, stayed one render stale: on a page that loads its document asynchronously, the badge stuck at "(0)" while three tags sat in the list, until some unrelated interaction happened to redraw the owner. A `Tab` now nudges its owner (`Owner.Refresh()`) from `OnParametersSet` when its `Title` changed. The nudge is self-terminating: the refresh re-renders the owner, the child re-renders with the same title, and no further refresh fires. Pinned by a test that renders a tab titled "Positive (0)", re-renders the parent with "Positive (3)", and asserts the strip button caught up — red without the fix, green with it (verified both ways). Full suite 288 green. Found while browser-verifying Kagura's CG editor base mode, where the load produces exactly one re-render after the tags arrive — nothing ever corrected the strip. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(tabs): a tab title that changes after the first render reaches the strip
All checks were successful
CI / build (pull_request) Successful in 10s
CI / test (pull_request) Successful in 16s
7b5eda5cc2
The strip renders in the owner's pass, before the Tab children re-render — so a
live title (TagAuthoring's prompt counts, say) stayed one render stale, sticking
at its first value until something else happened to redraw the owner. A Tab now
nudges its owner when its Title changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Summary

Summary
Generated on: 08/05/2026 - 10:30:54
Coverage date: 08/05/2026 - 10:30:52
Parser: Cobertura
Assemblies: 1
Classes: 59
Files: 58
Line coverage: 95.1% (1282 of 1348)
Covered lines: 1282
Uncovered lines: 66
Coverable lines: 1348
Total lines: 3588
Branch coverage: 88.7% (670 of 755)
Covered branches: 670
Total branches: 755
Method coverage: Feature is only available for sponsors

Coverage

Kagaku.UI - 95.1%
Name Line Branch
Kagaku.UI 95.1% 88.7%
Kagaku.UI.Badge 100% 100%
Kagaku.UI.Breadcrumb 100%
Kagaku.UI.BreadcrumbItem 100% 100%
Kagaku.UI.Button 100% 100%
Kagaku.UI.Card 100% 100%
Kagaku.UI.Combobox`1 94.2% 85.7%
Kagaku.UI.ConfirmDialog 100%
Kagaku.UI.ContextMenu 92.1% 94.4%
Kagaku.UI.CssClassExtensions 100%
Kagaku.UI.DebouncedSearchField 100% 87.5%
Kagaku.UI.Disclosure 100% 100%
Kagaku.UI.DocumentTitle 76.9% 100%
Kagaku.UI.DragReorderList`1 93.5% 75%
Kagaku.UI.EmptyState 100% 100%
Kagaku.UI.Field 100% 100%
Kagaku.UI.FileUpload 100% 91.6%
Kagaku.UI.FloatingActionButton 100%
Kagaku.UI.Hotkey 92.5% 87.5%
Kagaku.UI.Icon 100% 100%
Kagaku.UI.IconCatalog 100%
Kagaku.UI.InlineAlert 83.3% 75%
Kagaku.UI.InputFieldBase 95.2% 87.5%
Kagaku.UI.LabeledEntriesTable 96.7% 66.6%
Kagaku.UI.LabeledEntry 100%
Kagaku.UI.Lightbox 83.7% 85%
Kagaku.UI.LiveValue 100% 100%
Kagaku.UI.LiveValueInterop 100% 100%
Kagaku.UI.Markdown 100% 50%
Kagaku.UI.MaskedSecretField 96.2% 83.3%
Kagaku.UI.Menu 90% 75%
Kagaku.UI.MenuItem 100% 87.5%
Kagaku.UI.Modal 87.1% 90%
Kagaku.UI.NavGroup 100% 100%
Kagaku.UI.NavItem 94.4% 85.7%
Kagaku.UI.NavList 100%
Kagaku.UI.PreviewImage 100% 100%
Kagaku.UI.QuicklinkNav 80.5% 95.8%
Kagaku.UI.QuicklinkSection 100%
Kagaku.UI.RegionPoint 100% 100%
Kagaku.UI.RegionRect 90% 100%
Kagaku.UI.RegionSelector 85% 86.9%
Kagaku.UI.RelativeTime 100% 93.7%
Kagaku.UI.SaveIndicator 100% 100%
Kagaku.UI.Select`1 98.4% 95.8%
Kagaku.UI.SelectableGrid`1 95.5% 89.1%
Kagaku.UI.SelectOption`1 100%
Kagaku.UI.Separator 100%
Kagaku.UI.Slider 100% 91.6%
Kagaku.UI.StatusDot 100%
Kagaku.UI.Tab 100% 100%
Kagaku.UI.Table`1 100% 88.4%
Kagaku.UI.TableColumn`1 100%
Kagaku.UI.Tabs 95.9% 84.4%
Kagaku.UI.TextArea 100%
Kagaku.UI.TextField 100%
Kagaku.UI.ThemeSwitcher 100% 100%
Kagaku.UI.ToggleButtonGroup`1 100% 100%
Kagaku.UI.ToggleOption`1 100%
Kagaku.UI.TypingIndicator 100%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 08/05/2026 - 10:30:54 | | Coverage date: | 08/05/2026 - 10:30:52 | | Parser: | Cobertura | | Assemblies: | 1 | | Classes: | 59 | | Files: | 58 | | **Line coverage:** | 95.1% (1282 of 1348) | | Covered lines: | 1282 | | Uncovered lines: | 66 | | Coverable lines: | 1348 | | Total lines: | 3588 | | **Branch coverage:** | 88.7% (670 of 755) | | Covered branches: | 670 | | Total branches: | 755 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagaku.UI - 95.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagaku.UI**|**95.1%**|**88.7%**| |Kagaku.UI.Badge|100%|100%| |Kagaku.UI.Breadcrumb|100%|| |Kagaku.UI.BreadcrumbItem|100%|100%| |Kagaku.UI.Button|100%|100%| |Kagaku.UI.Card|100%|100%| |Kagaku.UI.Combobox`1|94.2%|85.7%| |Kagaku.UI.ConfirmDialog|100%|| |Kagaku.UI.ContextMenu|92.1%|94.4%| |Kagaku.UI.CssClassExtensions|100%|| |Kagaku.UI.DebouncedSearchField|100%|87.5%| |Kagaku.UI.Disclosure|100%|100%| |Kagaku.UI.DocumentTitle|76.9%|100%| |Kagaku.UI.DragReorderList`1|93.5%|75%| |Kagaku.UI.EmptyState|100%|100%| |Kagaku.UI.Field|100%|100%| |Kagaku.UI.FileUpload|100%|91.6%| |Kagaku.UI.FloatingActionButton|100%|| |Kagaku.UI.Hotkey|92.5%|87.5%| |Kagaku.UI.Icon|100%|100%| |Kagaku.UI.IconCatalog|100%|| |Kagaku.UI.InlineAlert|83.3%|75%| |Kagaku.UI.InputFieldBase|95.2%|87.5%| |Kagaku.UI.LabeledEntriesTable|96.7%|66.6%| |Kagaku.UI.LabeledEntry|100%|| |Kagaku.UI.Lightbox|83.7%|85%| |Kagaku.UI.LiveValue|100%|100%| |Kagaku.UI.LiveValueInterop|100%|100%| |Kagaku.UI.Markdown|100%|50%| |Kagaku.UI.MaskedSecretField|96.2%|83.3%| |Kagaku.UI.Menu|90%|75%| |Kagaku.UI.MenuItem|100%|87.5%| |Kagaku.UI.Modal|87.1%|90%| |Kagaku.UI.NavGroup|100%|100%| |Kagaku.UI.NavItem|94.4%|85.7%| |Kagaku.UI.NavList|100%|| |Kagaku.UI.PreviewImage|100%|100%| |Kagaku.UI.QuicklinkNav|80.5%|95.8%| |Kagaku.UI.QuicklinkSection|100%|| |Kagaku.UI.RegionPoint|100%|100%| |Kagaku.UI.RegionRect|90%|100%| |Kagaku.UI.RegionSelector|85%|86.9%| |Kagaku.UI.RelativeTime|100%|93.7%| |Kagaku.UI.SaveIndicator|100%|100%| |Kagaku.UI.Select`1|98.4%|95.8%| |Kagaku.UI.SelectableGrid`1|95.5%|89.1%| |Kagaku.UI.SelectOption`1|100%|| |Kagaku.UI.Separator|100%|| |Kagaku.UI.Slider|100%|91.6%| |Kagaku.UI.StatusDot|100%|| |Kagaku.UI.Tab|100%|100%| |Kagaku.UI.Table`1|100%|88.4%| |Kagaku.UI.TableColumn`1|100%|| |Kagaku.UI.Tabs|95.9%|84.4%| |Kagaku.UI.TextArea|100%|| |Kagaku.UI.TextField|100%|| |Kagaku.UI.ThemeSwitcher|100%|100%| |Kagaku.UI.ToggleButtonGroup`1|100%|100%| |Kagaku.UI.ToggleOption`1|100%|| |Kagaku.UI.TypingIndicator|100%|| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh my~ A Blazor render-ordering bug! The strip draws in the owner's pass, but the child's new Title lands after — so the badge sits one render stale, grinning at you with yesterday's news. What a sneaky little timing issue~ ♡ The fix is elegant: the child reaches up and taps the owner on the shoulder. "Excuse me, I changed~" Fufu~ I love it when a component knows how to ask for what it needs.

Verdict: Looks good to me~

No blockers, no little ideas even. This is sharp work, scarlet~ Let me show you what I checked:

What I liked~

  • Root-cause diagnosis is flawless. Tabs.BuildRenderTree reads tab.Title from the _tabs collection during the parent's render pass — but the child's [Parameter] Title is only updated during Blazor's parameter-setting phase, which runs after the parent's tree is built. So the strip literally cannot see the new value without a nudge. The PR body's "TagAuthoring Positive (n)" story is exactly the shape of this bug — a page that loads async data and re-renders exactly once with the new count. Nothing else corrects the strip. Beautifully diagnosed~

  • The _ownerRenderedTitle shadow field is correct and self-terminating. On first render, OnInitialized seeds _ownerRenderedTitle = Title, so OnParametersSet sees equality → no spurious refresh. On a title change, the shadow ≠ TitleOwner.Refresh() fires → owner re-renders → next OnParametersSet sees equality → done. No render loop, no infinite ping-pong. I traced this through every arm~ ♡

  • StateHasChanged() during OnParametersSet is explicitly supported by Blazor. It queues a render after the current batch completes. Safe, idiomatic, no lifecycle violation.

  • Multiple simultaneous title changes are safe. If N tabs change titles in one cycle, each calls StateHasChanged — Blazor batches these into a single extra render. No N-render explosion.

  • The test is genuinely directional, not a tautology. I traced the render pipeline: without the fix, Tabs.BuildRenderTree draws the strip from tab.Title (still "Positive (0)") before the child's parameter update lands → the DOM shows "Positive (0)" → Assert.Contains("Positive (3)", ...) fails. With the fix, OnParametersSet nudges the owner → strip re-renders with the new value. The PR body's "verified both ways" claim holds up under scrutiny. And the comment inside the test explains why the bug exists, not just what it tests — that's the kind of test comment that survives future refactors~

  • Consistent with the existing Tab↔Tabs coupling. Register/Unregister/Refresh — all three are child-driven notifications through the [CascadingParameter] Owner. The Refresh() addition sits naturally beside its siblings, and the XML doc on it is precise about why it exists (the render-ordering gap).

  • Coverage confirms it. Tab: 100%/100% line/branch. The change-detected arm is exercised by the new test; the no-change arm is exercised by every other Tabs test (they all render tabs with stable titles). Tabs: 95.9%/84.4% — no regression.

  • Scope is exactly right. Only Title is tracked, not Icon or Id. Icons virtually never change at runtime, and Id changes are rare (set once for URL slugs). Scoping to the actual bug — live title updates — is the correct call. No premature generalization~

CI: forgejo-actions coverage bot #5740 present for head 7b5eda5 (95.1%/88.7% overall). Local checks: skipped (CI green for current SHA).


Automated review by Jibril · 2026-08-05
CI/CD: passed for head 7b5eda5 · Local checks: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my~ A Blazor render-ordering bug! The strip draws in the owner's pass, but the child's new `Title` lands *after* — so the badge sits one render stale, grinning at you with yesterday's news. What a sneaky little timing issue~ ♡ The fix is elegant: the child reaches up and taps the owner on the shoulder. "Excuse me, I changed~" Fufu~ I *love* it when a component knows how to ask for what it needs. ### Verdict: ✅ Looks good to me~ No blockers, no little ideas even. This is sharp work, scarlet~ Let me show you what I checked: #### ✅ What I liked~ - **Root-cause diagnosis is flawless.** `Tabs.BuildRenderTree` reads `tab.Title` from the `_tabs` collection *during the parent's render pass* — but the child's `[Parameter] Title` is only updated during Blazor's parameter-setting phase, which runs *after* the parent's tree is built. So the strip literally cannot see the new value without a nudge. The PR body's "TagAuthoring Positive (n)" story is exactly the shape of this bug — a page that loads async data and re-renders exactly once with the new count. Nothing else corrects the strip. Beautifully diagnosed~ - **The `_ownerRenderedTitle` shadow field is correct and self-terminating.** On first render, `OnInitialized` seeds `_ownerRenderedTitle = Title`, so `OnParametersSet` sees equality → no spurious refresh. On a title change, the shadow ≠ `Title` → `Owner.Refresh()` fires → owner re-renders → next `OnParametersSet` sees equality → done. No render loop, no infinite ping-pong. I traced this through every arm~ ♡ - **`StateHasChanged()` during `OnParametersSet` is explicitly supported by Blazor.** It queues a render after the current batch completes. Safe, idiomatic, no lifecycle violation. - **Multiple simultaneous title changes are safe.** If N tabs change titles in one cycle, each calls `StateHasChanged` — Blazor batches these into a single extra render. No N-render explosion. - **The test is genuinely directional, not a tautology.** I traced the render pipeline: without the fix, `Tabs.BuildRenderTree` draws the strip from `tab.Title` (still "Positive (0)") *before* the child's parameter update lands → the DOM shows "Positive (0)" → `Assert.Contains("Positive (3)", ...)` fails. With the fix, `OnParametersSet` nudges the owner → strip re-renders with the new value. The PR body's "verified both ways" claim holds up under scrutiny. And the comment inside the test explains *why* the bug exists, not just *what* it tests — that's the kind of test comment that survives future refactors~ - **Consistent with the existing Tab↔Tabs coupling.** `Register`/`Unregister`/`Refresh` — all three are child-driven notifications through the `[CascadingParameter] Owner`. The `Refresh()` addition sits naturally beside its siblings, and the XML doc on it is precise about *why* it exists (the render-ordering gap). - **Coverage confirms it.** Tab: 100%/100% line/branch. The change-detected arm is exercised by the new test; the no-change arm is exercised by every other Tabs test (they all render tabs with stable titles). Tabs: 95.9%/84.4% — no regression. - **Scope is exactly right.** Only `Title` is tracked, not `Icon` or `Id`. Icons virtually never change at runtime, and `Id` changes are rare (set once for URL slugs). Scoping to the actual bug — live title updates — is the correct call. No premature generalization~ CI: forgejo-actions coverage bot #5740 present for head `7b5eda5` (95.1%/88.7% overall). Local checks: skipped (CI green for current SHA). --- *Automated review by Jibril · 2026-08-05* *CI/CD: passed for head 7b5eda5 · Local checks: skipped (CI green)*
bjoern merged commit f9a84d1c7f into main 2026-08-05 12:34:25 +02:00
bjoern deleted branch fix/tab-title-refresh 2026-08-05 12:34:25 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/Kagaku.UI!12
No description provided.