DEV: Convert experimental user menu tabs to links when they're active (#18158)

This PR restores a small feature which was present in the old menu and allowed users to click on the active tab in the menu to navigate to some page that showed the same items in the menu but with more details.

For example, if you switch to the PMs tab and then click on it again, currently nothing happens. However, with this change, clicking on the tab again will take you to your messages page at `/my/messages`.

Note: plugins that register custom tabs in the menu can provide a `linkWhenActive` property for their tab if they wish to mimic core's tabs, but it's optional; if they don't provide one, the tab will do nothing if the user clicks on it again.

Internal topic: t/73349.
This commit is contained in:
Osama Sayegh 2022-09-13 17:12:27 +03:00 committed by GitHub
parent d00cd3295e
commit 12ebdf0ff0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 168 additions and 24 deletions

View File

@ -0,0 +1,15 @@
<button
type="button"
role="tab"
class={{this.classNames}}
id={{this.id}}
tabindex={{this.tabIndex}}
aria-selected={{if this.isActive "true" "false"}}
aria-controls={{this.ariaControls}}
data-tab-number={{@tab.position}}
{{on "click" @onTabClick}}
{{!-- template-lint-disable require-context-role --}}
>
{{d-icon @tab.icon}}
{{yield}}
</button>

View File

@ -0,0 +1,27 @@
import Component from "@glimmer/component";
export default class UserMenuTab extends Component {
get isActive() {
return this.args.tab.id === this.args.currentTabId;
}
get classNames() {
const list = ["btn", "btn-flat", "btn-icon", "no-text", "user-menu-tab"];
if (this.isActive) {
list.push("active");
}
return list.join(" ");
}
get id() {
return `user-menu-button-${this.args.tab.id}`;
}
get tabIndex() {
return this.isActive ? "0" : "-1";
}
get ariaControls() {
return `quick-access-${this.args.tab.id}`;
}
}

View File

@ -11,23 +11,23 @@
<div class="menu-tabs-container" role="tablist" aria-orientation="vertical" aria-label={{i18n "user_menu.sr_menu_tabs"}}> <div class="menu-tabs-container" role="tablist" aria-orientation="vertical" aria-label={{i18n "user_menu.sr_menu_tabs"}}>
<div class="top-tabs tabs-list"> <div class="top-tabs tabs-list">
{{#each this.topTabs as |tab|}} {{#each this.topTabs as |tab|}}
<UserMenu::TabButton <UserMenu::MenuTab
@tab={{tab}} @tab={{tab}}
@currentTabId={{this.currentTabId}} @currentTabId={{this.currentTabId}}
@changeTabFunction={{fn this.changeTab tab}} @onTabClick={{fn this.handleTabClick tab}}
> >
{{#if tab.count}} {{#if tab.count}}
<span class="badge-notification">{{tab.count}}</span> <span class="badge-notification">{{tab.count}}</span>
{{/if}} {{/if}}
</UserMenu::TabButton> </UserMenu::MenuTab>
{{/each}} {{/each}}
</div> </div>
<div class="bottom-tabs tabs-list"> <div class="bottom-tabs tabs-list">
{{#each this.bottomTabs as |tab|}} {{#each this.bottomTabs as |tab|}}
<UserMenu::TabButton <UserMenu::MenuTab
@tab={{tab}} @tab={{tab}}
@currentTabId={{this.currentTabId}} @currentTabId={{this.currentTabId}}
@changeTabFunction={{fn this.changeTab tab}} @onTabClick={{fn this.handleTabClick tab}}
/> />
{{/each}} {{/each}}
</div> </div>

View File

@ -4,6 +4,8 @@ import { action } from "@ember/object";
import { NO_REMINDER_ICON } from "discourse/models/bookmark"; import { NO_REMINDER_ICON } from "discourse/models/bookmark";
import UserMenuTab, { CUSTOM_TABS_CLASSES } from "discourse/lib/user-menu/tab"; import UserMenuTab, { CUSTOM_TABS_CLASSES } from "discourse/lib/user-menu/tab";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import getUrl from "discourse-common/lib/get-url";
import DiscourseURL from "discourse/lib/url";
const DEFAULT_TAB_ID = "all-notifications"; const DEFAULT_TAB_ID = "all-notifications";
const DEFAULT_PANEL_COMPONENT = "user-menu/notifications-list"; const DEFAULT_PANEL_COMPONENT = "user-menu/notifications-list";
@ -23,6 +25,10 @@ const CORE_TOP_TABS = [
get panelComponent() { get panelComponent() {
return DEFAULT_PANEL_COMPONENT; return DEFAULT_PANEL_COMPONENT;
} }
get linkWhenActive() {
return `${this.currentUser.path}/notifications`;
}
}, },
class extends UserMenuTab { class extends UserMenuTab {
@ -45,6 +51,10 @@ const CORE_TOP_TABS = [
get notificationTypes() { get notificationTypes() {
return ["replied"]; return ["replied"];
} }
get linkWhenActive() {
return `${this.currentUser.path}/notifications/responses`;
}
}, },
class extends UserMenuTab { class extends UserMenuTab {
@ -67,6 +77,10 @@ const CORE_TOP_TABS = [
get notificationTypes() { get notificationTypes() {
return ["mentioned"]; return ["mentioned"];
} }
get linkWhenActive() {
return `${this.currentUser.path}/notifications/mentions`;
}
}, },
class extends UserMenuTab { class extends UserMenuTab {
@ -96,6 +110,10 @@ const CORE_TOP_TABS = [
get notificationTypes() { get notificationTypes() {
return ["liked", "liked_consolidated", "reaction"]; return ["liked", "liked_consolidated", "reaction"];
} }
get linkWhenActive() {
return `${this.currentUser.path}/notifications/likes-received`;
}
}, },
class extends UserMenuTab { class extends UserMenuTab {
@ -120,9 +138,14 @@ const CORE_TOP_TABS = [
this.siteSettings.enable_personal_messages || this.currentUser.staff this.siteSettings.enable_personal_messages || this.currentUser.staff
); );
} }
get notificationTypes() { get notificationTypes() {
return ["private_message"]; return ["private_message"];
} }
get linkWhenActive() {
return `${this.currentUser.path}/messages`;
}
}, },
class extends UserMenuTab { class extends UserMenuTab {
@ -145,6 +168,10 @@ const CORE_TOP_TABS = [
get notificationTypes() { get notificationTypes() {
return ["bookmark_reminder"]; return ["bookmark_reminder"];
} }
get linkWhenActive() {
return `${this.currentUser.path}/activity/bookmarks`;
}
}, },
class extends UserMenuTab { class extends UserMenuTab {
@ -167,6 +194,10 @@ const CORE_TOP_TABS = [
get count() { get count() {
return this.currentUser.get("reviewable_count"); return this.currentUser.get("reviewable_count");
} }
get linkWhenActive() {
return getUrl("/review");
}
}, },
]; ];
@ -183,6 +214,10 @@ const CORE_BOTTOM_TABS = [
get panelComponent() { get panelComponent() {
return "user-menu/profile-tab-content"; return "user-menu/profile-tab-content";
} }
get linkWhenActive() {
return `${this.currentUser.path}/summary`;
}
}, },
]; ];
@ -300,11 +335,13 @@ export default class UserMenu extends Component {
} }
@action @action
changeTab(tab) { handleTabClick(tab) {
if (this.currentTabId !== tab.id) { if (this.currentTabId !== tab.id) {
this.currentTabId = tab.id; this.currentTabId = tab.id;
this.currentPanelComponent = tab.panelComponent; this.currentPanelComponent = tab.panelComponent;
this.currentNotificationTypes = tab.notificationTypes; this.currentNotificationTypes = tab.notificationTypes;
} else if (tab.linkWhenActive) {
DiscourseURL.routeTo(tab.linkWhenActive);
} }
} }

View File

@ -1,15 +0,0 @@
<button
class={{concat "btn btn-flat btn-icon no-text" (if (eq @tab.id @currentTabId) " active")}}
type="button"
role="tab"
id={{concat "user-menu-button-" @tab.id}}
tabindex={{if (eq @tab.id @currentTabId) "0" "-1"}}
aria-selected={{if (eq @tab.id @currentTabId) "true" "false"}}
aria-controls={{concat "quick-access-" @tab.id}}
data-tab-number={{@tab.position}}
{{on "click" @changeTabFunction}}
{{!-- template-lint-disable require-context-role --}}
>
{{d-icon @tab.icon}}
{{yield}}
</button>

View File

@ -1,3 +0,0 @@
import templateOnly from "@ember/component/template-only";
export default templateOnly();

View File

@ -551,6 +551,89 @@ acceptance("User menu", function (needs) {
"logout button has the right icon" "logout button has the right icon"
); );
}); });
test("the active tab can be clicked again to navigate to a page", async function (assert) {
withPluginApi("0.1", (api) => {
api.registerUserMenuTab((UserMenuTab) => {
return class extends UserMenuTab {
get id() {
return "custom-tab-1";
}
get icon() {
return "wrench";
}
get panelComponent() {
return "d-button";
}
get linkWhenActive() {
return "/u/eviltrout/preferences";
}
};
});
api.registerUserMenuTab((UserMenuTab) => {
return class extends UserMenuTab {
get id() {
return "custom-tab-2";
}
get icon() {
return "plus";
}
get panelComponent() {
return "d-button";
}
};
});
});
await visit("/");
await click(".d-header-icons .current-user");
await click("#user-menu-button-all-notifications");
assert.strictEqual(
currentURL(),
"/u/eviltrout/notifications",
"clicking on active tab navigates to the page it links to"
);
assert.notOk(exists(".user-menu"), "user menu is closed after navigating");
const tabs = [
["#user-menu-button-custom-tab-1", "/u/eviltrout/preferences/account"],
["#user-menu-button-replies", "/u/eviltrout/notifications/responses"],
["#user-menu-button-messages", "/u/eviltrout/messages"],
["#user-menu-button-mentions", "/u/eviltrout/notifications/mentions"],
["#user-menu-button-bookmarks", "/u/eviltrout/activity/bookmarks"],
["#user-menu-button-likes", "/u/eviltrout/notifications/likes-received"],
["#user-menu-button-custom-tab-2", null],
["#user-menu-button-review-queue", "/review"],
["#user-menu-button-profile", "/u/eviltrout/summary"],
];
for (const [id, expectedLink] of tabs) {
await click(".d-header-icons .current-user");
await click(id);
await click(id);
if (expectedLink) {
assert.strictEqual(
currentURL(),
expectedLink,
`clicking on the ${id} tab navigates to ${expectedLink}`
);
assert.notOk(
exists(".user-menu"),
"user menu is closed after navigating"
);
} else {
assert.ok(
exists(".user-menu"),
"user menu remains open if tab doesn't link to anywhere"
);
}
await click("#site-logo");
}
});
}); });
acceptance("User menu - Dismiss button", function (needs) { acceptance("User menu - Dismiss button", function (needs) {