Skip to content

[2.x] fix: blank index title control when no nav item is active - #4821

Open
karl-bullock wants to merge 1 commit into
flarum:2.xfrom
karl-bullock:fix/tag-nav-current-tag
Open

[2.x] fix: blank index title control when no nav item is active#4821
karl-bullock wants to merge 1 commit into
flarum:2.xfrom
karl-bullock:fix/tag-nav-current-tag

Conversation

@karl-bullock

@karl-bullock karl-bullock commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Fixes #4819

What / why

On phone-width viewports the index toolbar title (the nav SelectDropdown, App-titleControl) renders blank on tag pages whenever the current tag has no item in the index sidebar nav. In practice that means any secondary tag that is not among the 3 most discussed secondary tags, so the set of affected tags shifts as discussion counts change and the bug looks random from an admin's point of view.

Root cause

SelectDropdown.getButtonContent() uses the label of the first active child and falls back to attrs.defaultLabel. defaultLabel is declared required on ISelectDropdownAttrs, but the nav dropdown built in IndexSidebar.items() never passed one. When no child is active the label collapses to nothing and only the sort caret is left.

Tags is what exposes it: addTagList adds primary tags plus the top 3 secondary tags by discussionCount(), so a tag outside those filters contributes no nav item and nothing is active.

Fix

Two parts, following the approach agreed in review:

Core. IndexSidebar now passes a defaultLabel to the nav SelectDropdown, read from the current page with the index's own link as the fallback:

defaultLabel={app.current.get('titleControlLabel') ?? app.translator.trans('core.forum.index.all_discussions_link')}

This resolves the empty-label case for any consumer, not just tags. A page that filters the discussion list without contributing a nav item can name itself by setting titleControlLabel on app.current, which is the existing channel a page uses to hand context to other components. Anything that does not falls back to "All Discussions".

Tags. On a tag page, set app.current.set('titleControlLabel', tag.name()). Because getButtonContent() only reaches for defaultLabel when no child is active, this surfaces the tag name precisely in the unlisted-tag case, while a listed tag still wins through its own active item.

The change is label-only, so nothing is added to the sidebar, and it needs no override(). addTagList is untouched.

On ordering: the tags side sets the label from an extend on IndexPage.prototype.view. That runs after the original view() returns, but view() only returns a PageStructure vnode holding a bound sidebar callback, so IndexSidebar.view() (and therefore items()) does not run until later in the diff. The label is always set before it is read. Stale labels are not a concern either, since DefaultResolver.makeKey() keys on route name plus route params, so any tag-to-tag or tag-to-index navigation re-creates the page and with it app.current.

Testing

New framework/core/js/tests/integration/forum/components/IndexSidebar.test.ts covers the title control: that it renders, that it falls back to the index link when no nav item is active, that it uses titleControlLabel when a page sets one, and that an active nav item still beats titleControlLabel. The two behavioural cases fail on 2.x without the IndexSidebar change and pass with it.

Also verified by hand on a 2.x-dev install with 4 secondary tags at descending discussion counts (4/3/2/1), Chromium at 390x844:

  • Before: the 4th-ranked tag's page shows an empty toolbar title with only the sort caret.
  • After: it shows the tag name. A top-3 tag page renders byte-identically to before, and the index still shows "All Discussions".
  • At 1280px the tag page and the index render byte-identically before and after, confirming the sidebar is unchanged: the unlisted tag is still absent from it.

Client-side navigation was checked separately, since the ordering argument above only matters on a route change rather than a fresh load. Driving the router through listed tag, unlisted tag, listed tag, unlisted tag, index, unlisted tag, listed tag gives the correct title at every step, and the unlisted tag never gains a nav item.

No js/dist in the diff, per the contributing docs.

@karl-bullock
karl-bullock requested a review from a team as a code owner July 22, 2026 00:38
@imorland imorland added this to the 2.0.0-rc.6 milestone Jul 23, 2026

@imorland imorland left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the thorough writeup — the root-cause analysis here is spot on, and I verified it: SelectDropdown.getButtonContent() derives the label purely from the active child and the nav dropdown passes no defaultLabel, so an unlisted current tag → no active child → blank title. That part's not in question.

Where I'd push back is the layer this is fixed at. Adding the current tag to the nav list doesn't only restore the title — it makes the current tag appear in the index sidebar (desktop and mobile) whenever you're on its page, when it otherwise wouldn't be listed. That's a visible behaviour change to the sidebar beyond the reported bug, and it's the kind of thing that should be a deliberate product decision rather than a side effect of a title fix.

Digging into alternatives, they all felt like they were compensating for the same underlying thing rather than fixing it:

  • Passing a defaultLabel from the tags extension — the natural "show the tag name" approach — isn't cleanly reachable: the nav SelectDropdown is built inline in IndexSidebar.items() with no hook to supply a per-page label, and extend() can't customise a scalar return, so it'd force an override().
  • A hardcoded core defaultLabel ("All Discussions") fixes the blank but shows a slightly-wrong title on tag pages, and still leaves the door open on other consumers.
  • Making the label an ItemList so it's extend()-able is really just a single value wearing a list costume — a smell of its own.

The common thread is that they're all working around the title control having nothing to show when no item is active, rather than addressing why it ends up with nothing to show — and the tags list is just what happens to expose it.

Could you take another pass at this? The constraints I'd want a fix to meet: it shouldn't change what appears in the sidebar, it should hold for any consumer that hits the empty-label case (not just tags), and it should avoid an override(). Worth agreeing the approach here before you rebuild it, rather than reworking the current one.

@imorland imorland removed this from the 2.0.0-rc.6 milestone Jul 23, 2026
@karl-bullock

Copy link
Copy Markdown
Contributor Author

Thanks, this is fair. I agree the sidebar behaviour change is the real problem with the current patch: adding the tag to the list to recover the title conflates two separate things.

Working within your three constraints (no sidebar change, holds for any consumer, no override()), I think the pieces are already in place:

  • SelectDropdown already accepts a defaultLabel, and getButtonContent() falls back to it when no child is active. The nav dropdown in IndexSidebar.items() simply never passes one.
  • app.current (the PageState) is the existing channel a page uses to hand context to other components (DiscussionPage sets discussion/stream, UserPage sets user), and tags already writes noTagsList to it.

So the fix I would propose:

1. CoreIndexSidebar passes a defaultLabel to the nav SelectDropdown, read from the current page with the index's own label as the fallback:

defaultLabel={app.current.get('title') ?? app.translator.trans('core.forum.index.all_discussions_link')}

That resolves the empty-label case for any consumer, adds nothing to the sidebar, and needs no override.

2. Tags — on a tag page, app.current.set('title', currentTag.name()). Because getButtonContent() only reaches for defaultLabel when no child is active, this surfaces the tag name precisely in the unlisted-tag case, while a listed tag still wins through its own active item.

Net result: an unlisted tag page shows the tag name, the plain index shows "All Discussions", and any other extension caught in the same empty-label spot can set its own page title or fall back to the default. No sidebar change, no override().

The one thing I would want your call on before I rebuild is the PageState key. I used title above for readability, but something more specific like sidebarTitle may be safer against collisions. Happy to go with whatever you prefer.

@imorland

Copy link
Copy Markdown
Member

This looks great — thanks for taking it back to the cause rather than working around it. The defaultLabel + app.current approach hits all three constraints cleanly: label-only so the sidebar is untouched, works for any consumer via the all_discussions_link fallback in core, and no override(). It also tidies up the fact that IndexSidebar never passed the (required) defaultLabel in the first place.

On the key: agreed that title is too generic for a shared, untyped PageState bag — it wouldn't collide today, but it's the kind of name something else will reach for eventually. I'd lean towards a key of titleControlLabel — it names what it actually feeds (the App-titleControl label) rather than a page concept, so there's no ambiguity about what setting it does.

Go ahead and rebuild on that. 👍

The index toolbar title (the nav SelectDropdown, App-titleControl) renders
blank on any page that filters the discussion list without contributing a
nav item of its own. SelectDropdown labels its button with the first active
child and falls back to defaultLabel, but IndexSidebar never passed the
(required) defaultLabel, so with no active child the label collapsed to
nothing and only the sort caret was left.

Pass a defaultLabel from IndexSidebar, read from the current page via
app.current with the index's own link as the fallback. A page in the
empty-label case can now name itself by setting titleControlLabel, and
anything else falls back to "All Discussions". This is label-only, so the
sidebar is untouched, and it needs no override().

Use that from tags, which is where the bug was reported: addTagList lists
primary tags plus only the 3 most discussed secondary tags, so the current
tag has no nav item whenever it falls outside those filters, and the set of
affected tags shifts as discussion counts change. On a tag page we now set
titleControlLabel to the tag name, which surfaces precisely when no item is
active while a listed tag still wins through its own active item.

Fixes flarum#4819
@karl-bullock
karl-bullock force-pushed the fix/tag-nav-current-tag branch from a5784ab to 13e80d3 Compare July 30, 2026 01:27
@karl-bullock karl-bullock changed the title [2.x] fix(tags): blank mobile toolbar title when the current tag has no nav item [2.x] fix: blank index title control when no nav item is active Jul 30, 2026
@karl-bullock

Copy link
Copy Markdown
Contributor Author

Rebuilt on titleControlLabel as agreed. addTagList is back to stock, so the whole fix is now the defaultLabel in IndexSidebar plus the tags side setting the label on app.current. I have rewritten the description to match.

On the three constraints:

  • No sidebar change. The tag page and the index render byte-identically to the stock build at desktop width, and the unlisted tag stays out of the sidebar.
  • Any consumer. The fallback lives in core and terminates at all_discussions_link, so nothing can land on blank, and a page can name itself by setting the key.
  • No override().

Added IndexSidebar.test.ts covering the fallback, the app.current label, and an active item still winning over it. The two behavioural cases fail on 2.x without the change.

One thing worth your eye, since it is the only call I had to make that had not already come up: the tags side sets the label from an extend on IndexPage.prototype.view. That works because view() only returns a PageStructure vnode holding a bound sidebar callback, so IndexSidebar.items() does not run until later in the diff and the label is always set before it is read. Happy to move it if you would rather it sat somewhere more explicit.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[2.x] Mobile toolbar title is blank on tag pages when the current tag has no sidebar nav item

2 participants