DEV: Introduce default 'auto' mode for glimmer header (#26467)

This will automatically enable the glimmer header when all installed themes/plugins are ready. This replaces the old group-based site setting.

In 'auto' mode, we check for calls to deprecated APIs (e.g. decorateWidget) which affect the old header. If any are present, we stick to the old header implementation and print a message to the console alongside the normal deprecation messages.

To override this automatic behavior, a new `glimmer_header_mode` site setting can be set to 'disabled' or 'enabled'.

This change also means that our test suite is running with the glimmer header. This unveiled a couple of small issues (e.g. some incorrect `aria-*` and `alt` text) which are now fixed. A number of selectors had to be updated to ensure the tests were clicking the actual `<button>` elements rather than the surrounding `<li>` elements.
This commit is contained in:
David Taylor 2024-04-10 14:35:54 +01:00 committed by GitHub
parent 5d0471ebe4
commit 3733db866c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 163 additions and 119 deletions

View File

@ -22,7 +22,10 @@ export default class Contents extends Component {
<div class="contents">
{{#if this.site.desktopView}}
{{#if @sidebarEnabled}}
<SidebarToggle @toggleHamburger={{@toggleHamburger}} />
<SidebarToggle
@toggleHamburger={{@toggleHamburger}}
@showSidebar={{@showSidebar}}
/>
{{/if}}
{{/if}}

View File

@ -2,6 +2,7 @@ import Component from "@glimmer/component";
import { hash } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { waitForPromise } from "@ember/test-waiters";
import { isDocumentRTL } from "discourse/lib/text-direction";
import { prefersReducedMotion } from "discourse/lib/utilities";
import { isTesting } from "discourse-common/config/environment";
@ -9,14 +10,13 @@ import discourseLater from "discourse-common/lib/later";
import closeOnClickOutside from "../../modifiers/close-on-click-outside";
import HamburgerDropdown from "../sidebar/hamburger-dropdown";
const CLOSE_ON_CLICK_SELECTORS =
"a[href], .sidebar-section-header-button, .sidebar-section-link-button, .sidebar-section-link";
export default class HamburgerDropdownWrapper extends Component {
@action
click(e) {
if (
e.target.closest(
".sidebar-section-header-button, .sidebar-section-link-button, .sidebar-section-link"
)
) {
if (e.target.closest(CLOSE_ON_CLICK_SELECTORS)) {
this.args.toggleHamburger();
}
}
@ -30,9 +30,9 @@ export default class HamburgerDropdownWrapper extends Component {
const panel = document.querySelector(".menu-panel");
const headerCloak = document.querySelector(".header-cloak");
const finishPosition = isDocumentRTL() ? "340px" : "-340px";
panel
const panelAnimatePromise = panel
.animate([{ transform: `translate3d(${finishPosition}, 0, 0)` }], {
duration: 200,
duration: isTesting() ? 0 : 200,
fill: "forwards",
easing: "ease-in",
})
@ -43,11 +43,13 @@ export default class HamburgerDropdownWrapper extends Component {
discourseLater(() => this.args.toggleHamburger());
}
});
headerCloak.animate([{ opacity: 0 }], {
duration: 200,
const cloakAnimatePromise = headerCloak.animate([{ opacity: 0 }], {
duration: isTesting() ? 0 : 200,
fill: "forwards",
easing: "ease-in",
});
}).finished;
waitForPromise(panelAnimatePromise);
waitForPromise(cloakAnimatePromise);
} else {
this.args.toggleHamburger();
}

View File

@ -1,4 +1,4 @@
import { hash } from "@ember/helper";
import { concat } from "@ember/helper";
import emoji from "discourse/helpers/emoji";
import I18n from "discourse-i18n";
@ -17,7 +17,8 @@ const UserStatusBubble = <template>
<div class="user-status-background">
{{emoji
@status.emoji
(hash title=(title @status.description @status.ends_at @timezone))
title=(title @status.description @status.ends_at @timezone)
alt=(concat ":" @status.emoji ":")
}}
</div>
</template>;

View File

@ -13,6 +13,7 @@ export default Controller.extend({
showTop: true,
router: service(),
footer: service(),
header: service(),
sidebarState: service(),
showSidebar: false,
sidebarDisabledRouteOverride: false,

View File

@ -24,6 +24,7 @@ export default {
applicationController.calculateShowSidebar()
);
applicationController.appEvents.trigger("site-header:force-refresh");
owner.lookup("service:header").hamburgerVisible = false;
}
}
});

View File

@ -203,19 +203,6 @@ function wrapWithErrorHandler(func, messageKey) {
};
}
function deprecatedHeaderWidgetOverride(widgetName, override) {
if (DEPRECATED_HEADER_WIDGETS.includes(widgetName)) {
deprecated(
`The ${widgetName} widget has been deprecated and ${override} is no longer a supported override.`,
{
since: "v3.3.0.beta1-dev",
id: "discourse.header-widget-overrides",
url: "https://meta.discourse.org/t/296544",
}
);
}
}
class PluginApi {
constructor(version, container) {
this.version = version;
@ -560,7 +547,7 @@ class PluginApi {
**/
decorateWidget(name, fn) {
const widgetName = name.split(":")[0];
deprecatedHeaderWidgetOverride(widgetName, "decorateWidget");
this.#deprecatedHeaderWidgetOverride(widgetName, "decorateWidget");
decorateWidget(name, fn);
}
@ -591,7 +578,7 @@ class PluginApi {
return;
}
deprecatedHeaderWidgetOverride(widget, "attachWidgetAction");
this.#deprecatedHeaderWidgetOverride(widget, "attachWidgetAction");
widgetClass.prototype[actionName] = fn;
}
@ -910,7 +897,7 @@ class PluginApi {
*
**/
changeWidgetSetting(widgetName, settingName, newValue) {
deprecatedHeaderWidgetOverride(widgetName, "changeWidgetSetting");
this.#deprecatedHeaderWidgetOverride(widgetName, "changeWidgetSetting");
changeSetting(widgetName, settingName, newValue);
}
@ -944,7 +931,7 @@ class PluginApi {
**/
reopenWidget(name, args) {
deprecatedHeaderWidgetOverride(name, "reopenWidget");
this.#deprecatedHeaderWidgetOverride(name, "reopenWidget");
return reopenWidget(name, args);
}
@ -979,6 +966,7 @@ class PluginApi {
url: "https://meta.discourse.org/t/296544",
}
);
this.container.lookup("service:header").anyWidgetHeaderOverrides = true;
attachAdditionalPanel(name, toggle, transformAttrs);
}
@ -2952,6 +2940,20 @@ class PluginApi {
registerAdminPluginConfigNav(pluginId, mode, links);
}
#deprecatedHeaderWidgetOverride(widgetName, override) {
if (DEPRECATED_HEADER_WIDGETS.includes(widgetName)) {
this.container.lookup("service:header").anyWidgetHeaderOverrides = true;
deprecated(
`The ${widgetName} widget has been deprecated and ${override} is no longer a supported override.`,
{
since: "v3.3.0.beta1-dev",
id: "discourse.header-widget-overrides",
url: "https://meta.discourse.org/t/296544",
}
);
}
}
}
// from http://stackoverflow.com/questions/6832596/how-to-compare-software-version-number-using-js-only-number

View File

@ -26,7 +26,7 @@ export default class CloseOnClickOutside extends Modifier {
}
if (
document.querySelector(this.targetSelector).contains(event.target) ||
document.querySelector(this.targetSelector)?.contains(event.target) ||
(this.secondaryTargetSelector &&
document
.querySelector(this.secondaryTargetSelector)

View File

@ -1,10 +1,32 @@
import { tracked } from "@glimmer/tracking";
import Service from "@ember/service";
import Service, { service } from "@ember/service";
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
@disableImplicitInjections
export default class Header extends Service {
@service siteSettings;
@tracked topic = null;
@tracked hamburgerVisible = false;
@tracked userVisible = false;
@tracked anyWidgetHeaderOverrides = false;
get useGlimmerHeader() {
if (this.siteSettings.glimmer_header_mode === "disabled") {
return false;
} else if (this.siteSettings.glimmer_header_mode === "enabled") {
return true;
} else {
// Auto
if (this.anyWidgetHeaderOverrides) {
// eslint-disable-next-line no-console
console.warn(
"Using legacy 'widget' header because themes and/or plugins are using deprecated APIs. https://meta.discourse.org/t/296544"
);
return false;
} else {
return true;
}
}
}
}

View File

@ -12,7 +12,7 @@
/>
{{#if this.showSiteHeader}}
{{#if this.currentUser.glimmer_header_enabled}}
{{#if this.header.useGlimmerHeader}}
<GlimmerSiteHeader
@canSignUp={{this.canSignUp}}
@showCreateAccount={{route-action "showCreateAccount"}}

View File

@ -27,7 +27,7 @@ acceptance("Do not disturb", function (needs) {
updateCurrentUser({ do_not_disturb_until: null });
await visit("/");
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
await click("#user-menu-button-profile");
await click("#quick-access-profile .do-not-disturb .btn");
@ -52,7 +52,7 @@ acceptance("Do not disturb", function (needs) {
updateCurrentUser({ do_not_disturb_until: null });
await visit("/");
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
await click("#user-menu-button-profile");
await click("#quick-access-profile .do-not-disturb .btn");
@ -94,7 +94,7 @@ acceptance("Do not disturb", function (needs) {
"The active dnd icon is shown"
);
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
await click("#user-menu-button-profile");
assert.strictEqual(
query(".do-not-disturb .relative-date").textContent.trim(),
@ -126,7 +126,7 @@ acceptance("Do not disturb", function (needs) {
this.siteSettings.enable_user_status = true;
await visit("/");
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
await click("#user-menu-button-profile");
await click("#quick-access-profile .do-not-disturb .btn");
@ -137,7 +137,7 @@ acceptance("Do not disturb", function (needs) {
updateCurrentUser({ do_not_disturb_until: DoNotDisturb.forever });
await visit("/");
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
await click("#user-menu-button-profile");
assert.dom(".do-not-disturb .relative-date").doesNotExist();

View File

@ -51,7 +51,7 @@ acceptance(
test("history modal is shown when navigating from a non-topic page", async function (assert) {
await visit("/");
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
await click(".notification.edited a");
const [v1, v2] = queryAll(".history-modal .revision-content");
@ -88,7 +88,7 @@ acceptance(
test("history modal is not shown when navigating from a non-topic page", async function (assert) {
await visit("/");
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
await click(".notification.edited a");
assert
.dom(".history-modal")
@ -117,7 +117,7 @@ acceptance(
test("history modal is not shown when navigating from a non-topic page", async function (assert) {
await visit("/");
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
await click(".notification.edited a");
assert
.dom(".history-modal")

View File

@ -8,6 +8,9 @@ import { acceptance } from "discourse/tests/helpers/qunit-helpers";
// header is the default.
acceptance("Header API - authenticated", function (needs) {
needs.user();
needs.settings({
glimmer_header_mode: "disabled",
});
test("can add buttons to the header", async function (assert) {
withPluginApi("1.29.0", (api) => {
@ -93,7 +96,7 @@ acceptance("Header API - anonymous", function () {
acceptance("Glimmer Header API - authenticated", function (needs) {
needs.user({ groups: AUTO_GROUPS.everyone });
needs.settings({
experimental_glimmer_header_groups: AUTO_GROUPS.everyone,
glimmer_header_mode: "enabled",
});
test("can add buttons to the header", async function (assert) {

View File

@ -64,8 +64,9 @@ async function triggerSwipeEnd({ x, y, touchTarget }) {
// new Touch() isn't available in Firefox, so this is skipped there
acceptance("Mobile - menu swipes", function (needs) {
needs.mobileView();
needs.user({
glimmer_header_enabled: true,
needs.user();
needs.settings({
glimmer_header_mode: "enabled",
});
chromeTest("swipe to close hamburger", async function (assert) {

View File

@ -2,6 +2,7 @@ import {
click,
currentURL,
fillIn,
triggerEvent,
triggerKeyEvent,
visit,
} from "@ember/test-helpers";
@ -19,6 +20,9 @@ import {
import selectKit from "discourse/tests/helpers/select-kit-helper";
import I18n from "discourse-i18n";
const clickOutside = () =>
triggerEvent(document.querySelector("header.d-header"), "pointerdown");
acceptance("Search - Anonymous", function (needs) {
needs.pretender((server, helper) => {
server.get("/search/query", (request) => {
@ -106,7 +110,7 @@ acceptance("Search - Anonymous", function (needs) {
await click("#search-button");
assert.ok(exists(".search-menu"));
await click(".d-header"); // click outside
await clickOutside();
assert.ok(!exists(".search-menu"));
await click("#search-button");
@ -1249,7 +1253,7 @@ acceptance("Search - assistant", function (needs) {
assert.notOk(exists(".btn.search-context"), "it removes the button");
await click(".d-header");
await clickOutside();
await click("#search-button");
assert.ok(
exists(".btn.search-context"),

View File

@ -30,7 +30,7 @@ acceptance("Sidebar - Anonymous User", function (needs) {
this.siteSettings.navigation_menu = "header dropdown";
await visit("/");
await click(".hamburger-dropdown");
await click(".hamburger-dropdown button");
assert.ok(
exists(".sidebar-hamburger-dropdown .sidebar-sections-anonymous"),

View File

@ -1,4 +1,4 @@
import { click, visit, waitFor } from "@ember/test-helpers";
import { click, triggerEvent, visit, waitFor } from "@ember/test-helpers";
import $ from "jquery";
import { test } from "qunit";
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
@ -38,7 +38,8 @@ acceptance("Sidebar - Narrow Desktop", function (needs) {
"cloak sidebar is displayed"
);
await click("#main-outlet");
await triggerEvent(document.querySelector(".header-cloak"), "pointerdown");
assert.ok(
!exists(".sidebar-hamburger-dropdown"),
"cloak sidebar is collapsed"
@ -66,7 +67,7 @@ acceptance("Sidebar - Narrow Desktop", function (needs) {
timeout: 5000,
});
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
$(".header-dropdown-toggle.current-user").click();
assert.ok(exists(".quick-access-panel"));

View File

@ -21,7 +21,7 @@ acceptance(
test("sections are collapsable", async function (assert) {
await visit("/");
await click(".hamburger-dropdown");
await click("#toggle-hamburger-menu");
assert.ok(
exists(".sidebar-section-header.sidebar-section-header-collapsable"),
@ -42,14 +42,14 @@ acceptance(
test("showing and hiding sidebar", async function (assert) {
await visit("/");
await click(".hamburger-dropdown");
await click("#toggle-hamburger-menu");
assert.ok(
exists(".sidebar-hamburger-dropdown"),
"displays the sidebar dropdown"
);
await click(".hamburger-dropdown");
await click("#toggle-hamburger-menu");
assert.notOk(
exists(".sidebar-hamburger-dropdown"),
@ -59,7 +59,7 @@ acceptance(
test("sections are not collapsable", async function (assert) {
await visit("/");
await click(".hamburger-dropdown");
await click("#toggle-hamburger-menu");
assert.notOk(
exists(".sidebar-section-header.sidebar-section-header-collapsable"),
@ -69,7 +69,7 @@ acceptance(
test("'more' dropdown should display as regular list items in header dropdown mode", async function (assert) {
await visit("/");
await click(".hamburger-dropdown");
await click("#toggle-hamburger-menu");
assert.ok(
exists("[data-link-name='admin']"),

View File

@ -3,6 +3,7 @@ import {
click,
currentRouteName,
currentURL,
triggerEvent,
triggerKeyEvent,
visit,
} from "@ember/test-helpers";
@ -60,7 +61,7 @@ acceptance("User menu", function (needs) {
test("notifications panel has a11y attributes", async function (assert) {
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
const panel = query("#quick-access-all-notifications");
assert.strictEqual(panel.getAttribute("tabindex"), "-1");
assert.strictEqual(
@ -71,7 +72,7 @@ acceptance("User menu", function (needs) {
test("replies notifications panel has a11y attributes", async function (assert) {
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-replies");
const panel = query("#quick-access-replies");
assert.strictEqual(panel.getAttribute("tabindex"), "-1");
@ -83,7 +84,7 @@ acceptance("User menu", function (needs) {
test("profile panel has a11y attributes", async function (assert) {
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-profile");
const panel = query("#quick-access-profile");
assert.strictEqual(panel.getAttribute("tabindex"), "-1");
@ -95,7 +96,7 @@ acceptance("User menu", function (needs) {
test("clicking on an unread notification", async function (assert) {
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
let repliesBadgeNotification = query(
"#user-menu-button-replies .badge-notification"
@ -114,7 +115,7 @@ acceptance("User menu", function (needs) {
"the Discourse-Clear-Notifications request header is set to the notification id in the next ajax request"
);
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
repliesBadgeNotification = query(
"#user-menu-button-replies .badge-notification"
);
@ -129,7 +130,7 @@ acceptance("User menu", function (needs) {
updateCurrentUser({ reviewable_count: 1 });
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-review-queue");
assert.strictEqual(
@ -152,7 +153,7 @@ acceptance("User menu", function (needs) {
"clicking on an item closes the menu after navigating"
);
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-review-queue");
await click("#quick-access-review-queue li.reviewable.pending a");
@ -216,7 +217,7 @@ acceptance("User menu", function (needs) {
"user-menu-button-profile": I18n.t("user_menu.tabs.profile"),
};
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
for (const [key, title] of Object.entries(expectedTitles)) {
assert.strictEqual(
query(`#${key}`).title,
@ -291,7 +292,7 @@ acceptance("User menu", function (needs) {
};
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
assert.ok(
exists("#user-menu-button-custom-tab-1"),
@ -377,7 +378,7 @@ acceptance("User menu", function (needs) {
});
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
const notifications = queryAll(
"#quick-access-all-notifications ul li.notification"
@ -404,7 +405,7 @@ acceptance("User menu", function (needs) {
});
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-bookmarks");
const bookmarks = queryAll("#quick-access-bookmarks ul li.bookmark");
@ -431,7 +432,7 @@ acceptance("User menu", function (needs) {
});
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-messages");
const messages = queryAll("#quick-access-messages ul li.message");
@ -442,9 +443,12 @@ acceptance("User menu", function (needs) {
});
test("the profile tab", async function (assert) {
const clickOutside = () =>
triggerEvent(document.querySelector("header.d-header"), "pointerdown");
updateCurrentUser({ draft_count: 13 });
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-profile");
const summaryLink = query("#quick-access-profile ul li.summary a");
@ -492,9 +496,10 @@ acceptance("User menu", function (needs) {
"invites link has the right icon"
);
await click("header.d-header"); // close the menu
await clickOutside();
updateCurrentUser({ can_invite_to_forum: false });
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-profile");
assert.notOk(
@ -548,11 +553,11 @@ acceptance("User menu", function (needs) {
"Do Not Disturb button has the right icon"
);
await click("header.d-header"); // close the menu
await clickOutside();
const date = new Date();
date.setHours(date.getHours() + 2);
updateCurrentUser({ do_not_disturb_until: date.toISOString() });
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-profile");
doNotDisturbButton = query(
@ -591,9 +596,9 @@ acceptance("User menu", function (needs) {
"toggle anonymous button has the right icon when the user isn't anonymous"
);
await click("header.d-header"); // close the menu
await clickOutside();
updateCurrentUser({ is_anonymous: true });
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-profile");
toggleAnonButton = query(
@ -612,7 +617,7 @@ acceptance("User menu", function (needs) {
"toggle anonymous button has the right icon when the user is anonymous"
);
await click("header.d-header"); // close the menu
await clickOutside();
updateCurrentUser({
is_anonymous: false,
can_post_anonymously: false,
@ -623,7 +628,7 @@ acceptance("User menu", function (needs) {
AUTO_GROUPS.trust_level_2,
],
});
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-profile");
assert.notOk(
@ -635,7 +640,7 @@ acceptance("User menu", function (needs) {
"toggle anon button isn't shown when the user can't use it"
);
await click("header.d-header"); // close the menu
await clickOutside();
updateCurrentUser({
is_anonymous: true,
trust_level: 2,
@ -646,7 +651,7 @@ acceptance("User menu", function (needs) {
AUTO_GROUPS.trust_level_2,
],
});
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-profile");
assert.ok(
@ -654,7 +659,7 @@ acceptance("User menu", function (needs) {
"toggle anon button is always shown if the user is anonymous"
);
await click("header.d-header"); // close the menu
await clickOutside();
updateCurrentUser({
is_anonymous: true,
can_post_anonymously: true,
@ -667,7 +672,7 @@ acceptance("User menu", function (needs) {
AUTO_GROUPS.trust_level_4,
],
});
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-profile");
assert.notOk(
@ -675,7 +680,7 @@ acceptance("User menu", function (needs) {
"toggle anon button is not shown if the allow_anonymous_posting setting is false"
);
await click("header.d-header"); // close the menu
await clickOutside();
updateCurrentUser({
is_anonymous: false,
can_post_anonymously: false,
@ -686,7 +691,7 @@ acceptance("User menu", function (needs) {
AUTO_GROUPS.trust_level_2,
],
});
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-profile");
assert.notOk(
@ -726,7 +731,7 @@ acceptance("User menu", function (needs) {
});
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-profile");
const item1 = query("#quick-access-profile ul li.test-1-item");
@ -792,7 +797,7 @@ acceptance("User menu", function (needs) {
});
});
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-all-notifications");
assert.strictEqual(
currentURL(),
@ -812,7 +817,7 @@ acceptance("User menu", function (needs) {
["#user-menu-button-profile", "/u/eviltrout/summary"],
];
for (const [id, expectedLink] of tabs) {
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click(id);
await click(id);
if (expectedLink) {
@ -837,7 +842,7 @@ acceptance("User menu", function (needs) {
test("tabs have hrefs and can be opened in new window/tab", async function (assert) {
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
assert
.dom("#user-menu-button-replies")
@ -865,7 +870,7 @@ acceptance("User menu", function (needs) {
test("tabs without hrefs can be visited with the keyboard", async function (assert) {
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await triggerKeyEvent(
"#user-menu-button-other-notifications",
@ -881,7 +886,7 @@ acceptance("User menu", function (needs) {
test("closes the menu when navigating away", async function (assert) {
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-profile");
await click(".quick-access-panel .preferences a");
@ -947,7 +952,7 @@ acceptance("User menu - Dismiss button", function (needs) {
test("shows confirmation modal for the all-notifications list", async function (assert) {
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click(".user-menu .notifications-dismiss");
assert.strictEqual(
@ -968,7 +973,7 @@ acceptance("User menu - Dismiss button", function (needs) {
test("shows confirmation modal for the bookmarks list", async function (assert) {
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
assert.strictEqual(
query("#user-menu-button-bookmarks .badge-notification").textContent,
@ -1024,7 +1029,7 @@ acceptance("User menu - Dismiss button", function (needs) {
test("shows confirmation modal for the messages list", async function (assert) {
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
assert.strictEqual(
query("#user-menu-button-messages .badge-notification").textContent,
@ -1080,7 +1085,7 @@ acceptance("User menu - Dismiss button", function (needs) {
test("doesn't show confirmation modal for the likes notifications list", async function (assert) {
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-likes");
await click(".user-menu .notifications-dismiss");
@ -1092,7 +1097,7 @@ acceptance("User menu - Dismiss button", function (needs) {
test("doesn't show confirmation modal for the other notifications list", async function (assert) {
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-other-notifications");
let othersBadgeNotification = query(
@ -1125,14 +1130,14 @@ acceptance("User menu - avatars", function (needs) {
test("It shows user avatars for various notifications on all notifications pane", async function (assert) {
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
assert.ok(exists("li.notification.edited .icon-avatar"));
assert.ok(exists("li.notification.replied .icon-avatar"));
});
test("It shows user avatars for messages", async function (assert) {
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-messages");
assert.ok(exists("li.notification.private-message .icon-avatar"));
@ -1141,7 +1146,7 @@ acceptance("User menu - avatars", function (needs) {
test("It shows user avatars for bookmark items and bookmark reminder notification items", async function (assert) {
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
await click("#user-menu-button-bookmarks");
assert.ok(exists("li.notification.bookmark-reminder .icon-avatar"));
@ -1150,7 +1155,7 @@ acceptance("User menu - avatars", function (needs) {
test("Icon avatars have correct class names based on system avatar usage", async function (assert) {
await visit("/");
await click(".d-header-icons .current-user");
await click(".d-header-icons .current-user button");
assert.ok(exists("li.group-message-summary .icon-avatar.system-avatar"));
assert.ok(exists("li.notification.replied .icon-avatar.user-avatar"));
});

View File

@ -9,7 +9,7 @@ import {
} from "discourse/tests/helpers/qunit-helpers";
async function openUserStatusModal() {
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
await click("#user-menu-button-profile");
await click(".set-user-status button");
}
@ -56,7 +56,7 @@ acceptance("User Status", function (needs) {
});
await visit("/");
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
await click("#user-menu-button-profile");
assert.equal(
@ -155,7 +155,7 @@ acceptance("User Status", function (needs) {
"shows user status emoji on the user avatar in the header"
);
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
await click("#user-menu-button-profile");
assert.equal(
query(
@ -184,7 +184,7 @@ acceptance("User Status", function (needs) {
await pickEmoji(userStatusEmoji);
await click(".btn-primary"); // save
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
await click("#user-menu-button-profile");
assert.equal(
query(
@ -222,7 +222,7 @@ acceptance("User Status", function (needs) {
await click("#tap_tile_one_hour");
await click(".btn-primary"); // save
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
await click("#user-menu-button-profile");
assert.equal(
@ -476,7 +476,7 @@ acceptance("User Status - user menu", function (needs) {
this.siteSettings.enable_user_status = false;
await visit("/");
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
await click("#user-menu-button-profile");
assert.notOk(exists("li.set-user-status"));
@ -486,7 +486,7 @@ acceptance("User Status - user menu", function (needs) {
this.siteSettings.enable_user_status = true;
await visit("/");
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
await click("#user-menu-button-profile");
assert.ok(exists("li.set-user-status .btn"), "shows the button");
@ -503,7 +503,7 @@ acceptance("User Status - user menu", function (needs) {
});
await visit("/");
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
await click("#user-menu-button-profile");
assert.equal(
@ -529,7 +529,7 @@ acceptance("User Status - user menu", function (needs) {
this.siteSettings.enable_user_status = true;
await visit("/");
await click(".header-dropdown-toggle.current-user");
await click(".header-dropdown-toggle.current-user button");
await click("#user-menu-button-profile");
await click(".set-user-status button");

View File

@ -98,13 +98,14 @@ export function performEmojiUnescape(string, opts) {
isReplaceableInlineEmoji(string, index, opts.inlineEmoji);
const title = opts.title ?? emojiVal;
const alt = opts.alt ?? opts.title ?? emojiVal;
const tabIndex = opts.tabIndex ? ` tabindex='${opts.tabIndex}'` : "";
return url && isReplacable
? `<img width="20" height="20" src='${url}' ${
opts.skipTitle ? "" : `title='${title}'`
} ${
opts.lazy ? "loading='lazy' " : ""
}alt='${title}' class='${classes}'${tabIndex}>`
}alt='${alt}' class='${classes}'${tabIndex}>`
: m;
};

View File

@ -1823,10 +1823,6 @@ class User < ActiveRecord::Base
in_any_groups?(SiteSetting.experimental_new_new_view_groups_map)
end
def glimmer_header_enabled?
in_any_groups?(SiteSetting.experimental_glimmer_header_groups_map)
end
def watched_precedence_over_muted
if user_option.watched_precedence_over_muted.nil?
SiteSetting.watched_precedence_over_muted

View File

@ -74,7 +74,6 @@ class CurrentUserSerializer < BasicUserSerializer
:new_new_view_enabled?,
:use_experimental_topic_bulk_actions?,
:use_admin_sidebar,
:glimmer_header_enabled?,
:can_view_raw_email
delegate :user_stat, to: :object, private: true

View File

@ -2620,7 +2620,7 @@ en:
experimental_topics_filter: "EXPERIMENTAL: Enables the experimental topics filter route at /filter"
enable_experimental_lightbox: "EXPERIMENTAL: Replace the default image lightbox with the revamped design."
enable_experimental_bookmark_redesign_groups: "EXPERIMENTAL: Show a quick access menu for bookmarks on posts and a new redesigned modal"
experimental_glimmer_header_groups: "EXPERIMENTAL: Render the site header as glimmer components."
glimmer_header_mode: "Control whether the new 'glimmer' header implementation is used. Defaults to 'auto', which will enable automatically once all your themes and plugins are ready. https://meta.discourse.org/t/296544"
experimental_form_templates: "EXPERIMENTAL: Enable the form templates feature. <b>After enabled,</b> manage the templates at <a href='%{base_path}/admin/customize/form-templates'>Customize / Templates</a>."
admin_sidebar_enabled_groups: "EXPERIMENTAL: Enable sidebar navigation for the admin UI for the specified groups, which replaces the top-level admin navigation buttons."
lazy_load_categories_groups: "EXPERIMENTAL: Lazy load category information only for users of these groups. This improves performance on sites with many categories."

View File

@ -2352,12 +2352,14 @@ developer:
instrument_gc_stat_per_request:
default: false
hidden: true
experimental_glimmer_header_groups:
glimmer_header_mode:
client: true
type: group_list
list_type: compact
default: ""
allow_any: false
type: enum
choices:
- disabled
- auto
- enabled
default: auto
admin_sidebar_enabled_groups:
type: group_list
list_type: compact

View File

@ -5,7 +5,7 @@ RSpec.describe "Glimmer Header", type: :system do
let(:search) { PageObjects::Pages::Search.new }
fab!(:current_user) { Fabricate(:user) }
fab!(:topic)
before { SiteSetting.experimental_glimmer_header_groups = Group::AUTO_GROUPS[:everyone] }
before { SiteSetting.glimmer_header_mode = "enabled" }
it "renders basics" do
visit "/"