From 13e80d3ba71a05de58f8c55cfd90a1336de86d4c Mon Sep 17 00:00:00 2001 From: karl-bullock Date: Tue, 21 Jul 2026 20:37:21 -0400 Subject: [PATCH] fix: blank index title control when no nav item is active 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 #4819 --- extensions/tags/js/src/forum/addTagFilter.tsx | 10 ++++ .../js/src/forum/components/IndexSidebar.tsx | 6 ++ .../forum/components/IndexSidebar.test.ts | 58 +++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 framework/core/js/tests/integration/forum/components/IndexSidebar.test.ts diff --git a/extensions/tags/js/src/forum/addTagFilter.tsx b/extensions/tags/js/src/forum/addTagFilter.tsx index 13875b15e6..98430ce3f0 100644 --- a/extensions/tags/js/src/forum/addTagFilter.tsx +++ b/extensions/tags/js/src/forum/addTagFilter.tsx @@ -87,6 +87,16 @@ export default function addTagFilter() { } }); + // Name the current tag in the index title control. The nav dropdown only falls + // back to this label when none of its items is active, so it surfaces exactly + // when the current tag has no item in the sidebar nav, which is the case for + // any secondary tag outside the 3 most discussed ones. + extend(IndexPage.prototype, 'view', function () { + const tag = app.currentTag(); + + if (tag) app.current.set('titleControlLabel', tag.name()); + }); + // If currently viewing a tag, restyle the 'new discussion' button to use // the tag's color, and disable if the user isn't allowed to edit. extend(IndexSidebar.prototype, 'items', function (items) { diff --git a/framework/core/js/src/forum/components/IndexSidebar.tsx b/framework/core/js/src/forum/components/IndexSidebar.tsx index e89badd0dc..a1f383377f 100644 --- a/framework/core/js/src/forum/components/IndexSidebar.tsx +++ b/framework/core/js/src/forum/components/IndexSidebar.tsx @@ -47,12 +47,18 @@ export default class IndexSidebar ); + // `SelectDropdown` labels its button with the active child, and falls back to + // `defaultLabel` when none of them is active. That happens whenever a page + // filters the discussion list without contributing a nav item of its own, so + // such a page can name itself here by setting `titleControlLabel` on + // `app.current`. Otherwise we are on the unfiltered index. items.add( 'nav', {this.navItems().toArray()} diff --git a/framework/core/js/tests/integration/forum/components/IndexSidebar.test.ts b/framework/core/js/tests/integration/forum/components/IndexSidebar.test.ts new file mode 100644 index 0000000000..6d56c94f9b --- /dev/null +++ b/framework/core/js/tests/integration/forum/components/IndexSidebar.test.ts @@ -0,0 +1,58 @@ +import bootstrapForum from '@flarum/jest-config/src/bootstrap/forum'; +import IndexSidebar from '../../../../src/forum/components/IndexSidebar'; +import { app } from '../../../../src/forum'; +import mq from 'mithril-query'; +import m from 'mithril'; + +beforeAll(() => bootstrapForum()); + +const titleControlLabel = (sidebar: any): string | undefined => + sidebar.rootEl.querySelector('.App-titleControl > .Dropdown-toggle .Button-labelText')?.textContent; + +const navItemLabel = (sidebar: any, item: string): string | undefined => + sidebar.rootEl.querySelector(`.Dropdown-menu .item-${item} .Button-labelText`)?.textContent; + +describe('IndexSidebar', () => { + beforeAll(() => app.boot()); + + afterEach(() => app.current.set('titleControlLabel', undefined)); + + test('renders', () => { + const sidebar = mq(IndexSidebar, {}); + + expect(sidebar).toHaveElement('.App-titleControl'); + }); + + test('labels the title control with the index link when no nav item is active', () => { + const sidebar = mq(IndexSidebar, {}); + + expect(titleControlLabel(sidebar)).toBeTruthy(); + expect(titleControlLabel(sidebar)).toBe(navItemLabel(sidebar, 'allDiscussions')); + }); + + test('labels the title control from app.current when a page provides a label', () => { + app.current.set('titleControlLabel', 'Support'); + + const sidebar = mq(IndexSidebar, {}); + + expect(titleControlLabel(sidebar)).toBe('Support'); + }); + + test('prefers an active nav item over the label from app.current', () => { + class SidebarWithActiveItem extends IndexSidebar { + navItems() { + const items = super.navItems(); + + items.add('active', m('button', { active: true }, 'Active item'), 200); + + return items; + } + } + + app.current.set('titleControlLabel', 'Support'); + + const sidebar = mq(SidebarWithActiveItem, {}); + + expect(titleControlLabel(sidebar)).toBe('Active item'); + }); +});