From fd4695f3d53d4e2655bfd110a17a3d7900f0156b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 20 Feb 2026 14:08:22 +0000 Subject: [PATCH] Update UserMenu theme toggle to use IconButton (#32591) * Update UserMenu theme toggle to use IconButton This lets it use the correct compound colour based on theme Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update screenshot Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../user-menu.spec.ts/user-menu-linux.png | Bin 14924 -> 14801 bytes res/css/structures/_UserMenu.pcss | 15 ++- res/themes/dark/css/_dark.pcss | 1 - .../legacy-light/css/_legacy-light.pcss | 2 - .../css/_light-high-contrast.pcss | 1 - res/themes/light/css/_light.pcss | 1 - src/components/structures/UserMenu.tsx | 121 ++++++++---------- src/settings/watchers/ThemeWatcher.ts | 13 +- .../components/structures/UserMenu-test.tsx | 25 +++- .../settings/watchers/ThemeWatcher-test.tsx | 16 +++ 10 files changed, 112 insertions(+), 83 deletions(-) diff --git a/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png b/playwright/snapshots/user-menu/user-menu.spec.ts/user-menu-linux.png index c210321017598604cbb99c83a26dce07fd4492f3..fcd039ba2689c73b86848a25014113887ec90078 100644 GIT binary patch literal 14801 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 U#@jzMJu!L z$X1{CI{ka2zszIb^HmzO_V%?9d84ic7KdLX4q#i&lmtKd`Td-qZx~3RJ(uT?`LI*% zGdG|HDI@qe{vwY*ierZYr&p*%3Sp{YD#yj4>=dt={9b&Z9km{pP^>cGYi@+;M=&`# zIZlCqvM*v$H&ic^lxXGQb#iEG`5ThE)O&3W(b1LcUQx;I`vX{$gH8BssQ2~F!V!lT zi-2JlOMT4;au$l13Mv*BJV$1m)aQO+18>hT8oL(o(a_Rru*R1FgHKdfP0}BCH=EgF zCK6Ck_^F~RqRJ&bv@qSyR)|41=aXiPZXT!c-@hMDtDyfjbX#Q>k&daVuVOQ-HsNj> z&ER7>9v|9UxWo?+4@bH{s+zmNt^GEUF#`2E6DEqaT`6=^4(qTudl|1fhkf)poGo^u z+hmb`;|=@t={yF!_$MN)L9_bivV-;de8QAEhJ^3tKy7%@_ss5bYW6)|)TZ`AW1~mX z!@F<7h?E0Eh(f?9fB&Aovi#f0kwssSii)c19I{wJyL-fA zvsgP7D1;A75hD5$r!D=NnJGcI#dq-dxx}t+V+Uf&l}V8Xrk~% zO|6HPk&f{rWyxBjTDa3(<=Pec85$CstVZS2n$W{uBa2N=LC66zDQWXcbGks(2W%{F z@Ve+y!=3w!LVuxgAi^)>4P0xH?EWGi`@#73}^2jz`tE+ip9$Up9ypVw$`0#K!HvR>Um8P^Dk(K7# z^}<>P`u;B6;CG!$7t+!QQ<;ULNb^EYH+sIrj2cKVNLsxe6u+LFHeQo7kv8V++9~Vn z$rjsun#q#nlvo~8E|2EyI>SQyqFw-BOp!|Iw8t!}CSU2ht6(vv|Do>raYL7g!`WTK z71Y%n+7E2ty}Z%pN$uxczO0+HR_5-)up-&$AqY{d!2d^fO=~5Cc9t#BCnY6icGo|F z&+?*{JgWh(MIQL!<0F)}6C49Fhp$1p^$PQq79eM#pgq%IKkyv zLQEh#9q5wNRCD??Z!wO=&N#uEe{0!Iuc2aberCgyLd{a5ZXb z2qb#0-uvOovgo;=f3WuU(bII7A)h)iYOcxU2xqKJUpIUsEm&b;-*|$wv@jKP5{e2q z^b@boC0~$T&BGN^tp;nYIdQFC>)YuL=EeriX7wbW<^}YUCUpu1+kYC!$O$u32{3TC z9&4^|Fu`kE5*9~1?TZwr6?2IT#uZU8gp(^@NbtRm{wDA@;;x>+a#;NVy9@(iM>(bW z7{uIWa*qW~keMsjhfhE%l^rK$(kBVfH!vw}r$k`N-k>5;S!(k!urXwXrzgpEalTsG zOjA*@JJ*rXzHctHSvcI-=XzQ4c2%kzYc5z{_`TPI?(_0DSaH8D;{-MAKUjd>yGXkIEhnuqsuVyv4_h(ZnF)$yBCxXTzP3ZM-KLMKu z0@DsPH8rLGI_z6v#CO&A1l-0Xdlvx+sa!?|JeD~j&d&%^Z)S;k>}q=2yze~+&wTww z$_zSqcFsWSQl^)ee}UD@%+y3C=-hCpgY~I1(PR)QDlQ&P$J5F1`AF)_`hdb?)u5?X4Vl~oD7`PXlL z-_PG0w74^T{D@HBpv?(xD6|28j3%;OY%%+@#Y>urx3I9#r+nAZI6cQ2F*$6X_5QJA z(cG{0i{ZSHa<=g0Ewl!*a`+eIK8a&-2hy71_c|36!4_r|TqMinur9c)jr)wviHx04 z5fKrwH)hy9cGf(0B(t-@eX{1;Z+Q%7?N0PD+QdYdjMtrDgPTc(Q;J6)FO$b&_Rj%} zev_+&H(oNuER?N1-HowH74`SH_gte^y;vBbH3I>l2N~sT61`ch ztEvjsI=Yw(LmV}H(7;697>*p_t{Jf%Kq5(vE=ROX)s3TuK|2&rpzFh#l|Pn4+YD0z z&flO`I~;X_59}daL6W!kABG2qGlZ7%tE&815rXQgzgJmZ^LO8|A&Bbh*4vnzjlDxM zma=^5q)DAk%VAt6rn-6)de&SX0eQSxQkb75_$1-}26�x5&h~%5d1%bM;&7%_qK_ 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 RSy31mIVmN{3UQ-={{gLL7?uD4 diff --git a/res/css/structures/_UserMenu.pcss b/res/css/structures/_UserMenu.pcss index 89b15e68b2..b6c5b73e93 100644 --- a/res/css/structures/_UserMenu.pcss +++ b/res/css/structures/_UserMenu.pcss @@ -86,14 +86,8 @@ Please see LICENSE files in the repository root for full details. } .mx_UserMenu_contextMenu_themeButton { - min-width: 32px; - max-width: 32px; - width: 32px; - height: 32px; + flex-shrink: 0; margin-left: 8px; - border-radius: 32px; - background-color: $theme-button-bg-color; - cursor: pointer; /* to make alignment easier, create flexbox for the image */ display: flex; @@ -102,6 +96,13 @@ Please see LICENSE files in the repository root for full details. /* For enhanced visibility under contrast control */ outline: 1px solid transparent; + + /* Compound overrides to match transitional designs */ + padding: var(--cpd-space-2x); + svg { + width: 16px; + height: 16px; + } } &.mx_UserMenu_contextMenu_guestPrompts { diff --git a/res/themes/dark/css/_dark.pcss b/res/themes/dark/css/_dark.pcss index dbd5176653..99fd31f371 100644 --- a/res/themes/dark/css/_dark.pcss +++ b/res/themes/dark/css/_dark.pcss @@ -250,7 +250,6 @@ $visual-bell-bg-color: #800; $event-timestamp-color: var(--cpd-color-text-secondary); $composer-shadow-color: rgba(0, 0, 0, 0.28); $breadcrumb-placeholder-bg-color: #272c35; -$theme-button-bg-color: #e3e8f0; $resend-button-divider-color: var(--cpd-color-gray-700); $inlinecode-border-color: $quinary-content; $inlinecode-background-color: $system; diff --git a/res/themes/legacy-light/css/_legacy-light.pcss b/res/themes/legacy-light/css/_legacy-light.pcss index 8c00393d41..c2b0bd5995 100644 --- a/res/themes/legacy-light/css/_legacy-light.pcss +++ b/res/themes/legacy-light/css/_legacy-light.pcss @@ -138,8 +138,6 @@ $room-icon-unread-color: var(--cpd-color-icon-tertiary); /* ******************** */ -$theme-button-bg-color: #e3e8f0; - $roomtile-default-badge-bg-color: #61708b; $roomtile-selected-bg-color: #fff; diff --git a/res/themes/light-high-contrast/css/_light-high-contrast.pcss b/res/themes/light-high-contrast/css/_light-high-contrast.pcss index ada87d7a33..483917fb0c 100644 --- a/res/themes/light-high-contrast/css/_light-high-contrast.pcss +++ b/res/themes/light-high-contrast/css/_light-high-contrast.pcss @@ -21,7 +21,6 @@ $input-darker-bg-color: $quinary-content; $input-darker-fg-color: $secondary-content; $resend-button-divider-color: $input-darker-bg-color; $icon-button-color: var(--cpd-color-icon-tertiary); -$theme-button-bg-color: $quinary-content; /* not using a compound color here for now as we want to have the same color in light and dark theme. Until we have a non-symetrical token for it, let's keep it hardcoded to the following value */ diff --git a/res/themes/light/css/_light.pcss b/res/themes/light/css/_light.pcss index fcab227283..3bd803c53e 100644 --- a/res/themes/light/css/_light.pcss +++ b/res/themes/light/css/_light.pcss @@ -321,7 +321,6 @@ $visual-bell-bg-color: #faa; $event-timestamp-color: var(--cpd-color-text-secondary); $composer-shadow-color: rgba(0, 0, 0, 0.04); $breadcrumb-placeholder-bg-color: #e8eef5; -$theme-button-bg-color: $quinary-content; $resend-button-divider-color: $input-darker-bg-color; $inlinecode-border-color: $quinary-content; $inlinecode-background-color: $system; diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 6f9b768588..b3f20a07ed 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, createRef, type ReactNode } from "react"; +import React, { type JSX, createRef, type ReactNode, useMemo } from "react"; import { type Room } from "matrix-js-sdk/src/matrix"; import { ChatSolidIcon, @@ -18,6 +18,7 @@ import { NotificationsSolidIcon, ThemeIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; +import { IconButton } from "@vector-im/compound-web"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import defaultDispatcher from "../../dispatcher/dispatcher"; @@ -31,8 +32,8 @@ import FeedbackDialog from "../views/dialogs/FeedbackDialog"; import Modal from "../../Modal"; import LogoutDialog, { shouldShowLogoutDialog } from "../views/dialogs/LogoutDialog"; import SettingsStore from "../../settings/SettingsStore"; -import { findHighContrastTheme, getCustomTheme, isHighContrastTheme } from "../../theme"; -import { RovingAccessibleButton } from "../../accessibility/RovingTabIndex"; +import { findHighContrastTheme, isHighContrastTheme } from "../../theme"; +import { useRovingTabIndex } from "../../accessibility/RovingTabIndex"; import AccessibleButton, { type ButtonEvent } from "../views/elements/AccessibleButton"; import SdkConfig from "../../SdkConfig"; import { getHomePageUrl } from "../../utils/pages"; @@ -52,6 +53,8 @@ import PosthogTrackers from "../../PosthogTrackers"; import { type ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload"; import { SDKContext } from "../../contexts/SDKContext"; import { shouldShowFeedback } from "../../utils/Feedback"; +import ThemeWatcher, { ThemeWatcherEvent } from "../../settings/watchers/ThemeWatcher.ts"; +import { useTypedEventEmitterState } from "../../hooks/useEventEmitter.ts"; interface IProps { isPanelCollapsed: boolean; @@ -62,8 +65,6 @@ type PartialDOMRect = Pick; interface IState { contextMenuPosition: PartialDOMRect | null; - isDarkTheme: boolean; - isHighContrast: boolean; selectedSpace?: Room | null; } @@ -83,6 +84,51 @@ const below = (rect: PartialDOMRect): MenuProps => { }; }; +const ThemeSwitchButton = (): JSX.Element => { + const [onFocus, isActive, ref] = useRovingTabIndex(); + const themeWatcher = useMemo(() => new ThemeWatcher(), []); + const [isHighContrast, isDark] = useTypedEventEmitterState( + themeWatcher, + ThemeWatcherEvent.Change, + (theme: string) => [isHighContrastTheme(theme), themeWatcher.isUserOnDarkTheme()], + ); + + const onSwitchThemeClick = (ev: ButtonEvent): void => { + ev.preventDefault(); + ev.stopPropagation(); + + PosthogTrackers.trackInteraction("WebUserMenuThemeToggleButton", ev); + + // Disable system theme matching if the user hits this button + SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false); + + let newTheme = isDark ? "light" : "dark"; + if (isHighContrast) { + const hcTheme = findHighContrastTheme(newTheme); + if (hcTheme) { + newTheme = hcTheme; + } + } + SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab + themeWatcher.recheck(newTheme); + }; + + return ( + + + + ); +}; + export default class UserMenu extends React.Component { public static contextType = SDKContext; declare public context: React.ContextType; @@ -97,8 +143,6 @@ export default class UserMenu extends React.Component { this.state = { contextMenuPosition: null, - isDarkTheme: this.isUserOnDarkTheme(), - isHighContrast: this.isUserOnHighContrastTheme(), selectedSpace: SpaceStore.instance.activeSpaceRoom, }; } @@ -111,7 +155,6 @@ export default class UserMenu extends React.Component { OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); this.dispatcherRef = defaultDispatcher.register(this.onAction); - this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged); } public componentWillUnmount(): void { @@ -122,30 +165,6 @@ export default class UserMenu extends React.Component { SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); } - private isUserOnDarkTheme(): boolean { - if (SettingsStore.getValue("use_system_theme")) { - return window.matchMedia("(prefers-color-scheme: dark)").matches; - } else { - const theme = SettingsStore.getValue("theme"); - if (theme.startsWith("custom-")) { - return !!getCustomTheme(theme.substring("custom-".length)).is_dark; - } - return theme === "dark"; - } - } - - private isUserOnHighContrastTheme(): boolean { - if (SettingsStore.getValue("use_system_theme")) { - return window.matchMedia("(prefers-contrast: more)").matches; - } else { - const theme = SettingsStore.getValue("theme"); - if (theme.startsWith("custom-")) { - return false; - } - return isHighContrastTheme(theme); - } - } - private onProfileUpdate = async (): Promise => { // the store triggered an update, so force a layout update. We don't // have any state to store here for that to magically happen. @@ -158,13 +177,6 @@ export default class UserMenu extends React.Component { }); }; - private onThemeChanged = (): void => { - this.setState({ - isDarkTheme: this.isUserOnDarkTheme(), - isHighContrast: this.isUserOnHighContrastTheme(), - }); - }; - private onAction = (payload: ActionPayload): void => { switch (payload.action) { case Action.ToggleUserMenu: @@ -200,25 +212,6 @@ export default class UserMenu extends React.Component { this.setState({ contextMenuPosition: null }); }; - private onSwitchThemeClick = (ev: ButtonEvent): void => { - ev.preventDefault(); - ev.stopPropagation(); - - PosthogTrackers.trackInteraction("WebUserMenuThemeToggleButton", ev); - - // Disable system theme matching if the user hits this button - SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false); - - let newTheme = this.state.isDarkTheme ? "light" : "dark"; - if (this.state.isHighContrast) { - const hcTheme = findHighContrastTheme(newTheme); - if (hcTheme) { - newTheme = hcTheme; - } - } - SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab - }; - private onSettingsOpen = (ev: ButtonEvent, tabId?: string, props?: Record): void => { ev.preventDefault(); ev.stopPropagation(); @@ -398,17 +391,7 @@ export default class UserMenu extends React.Component { - - - + {topSection} {primaryOptionList} diff --git a/src/settings/watchers/ThemeWatcher.ts b/src/settings/watchers/ThemeWatcher.ts index d56ee559c2..9876dcfe6b 100644 --- a/src/settings/watchers/ThemeWatcher.ts +++ b/src/settings/watchers/ThemeWatcher.ts @@ -13,7 +13,7 @@ import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; import SettingsStore from "../SettingsStore"; import dis from "../../dispatcher/dispatcher"; import { Action } from "../../dispatcher/actions"; -import { findHighContrastTheme } from "../../theme"; +import { findHighContrastTheme, getCustomTheme } from "../../theme"; import { type ActionPayload } from "../../dispatcher/payloads"; import { SettingLevel } from "../SettingLevel"; @@ -125,6 +125,17 @@ export default class ThemeWatcher extends TypedEventEmitter", () => { let client: MatrixClient; @@ -151,4 +152,26 @@ describe("", () => { }); }); }); + + it("should toggle theme on switcher click", async () => { + sdkContext.client = stubClient(); + const spy = jest.spyOn(SettingsStore, "setValue"); + + const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext); + render(); + + screen.getByRole("button", { name: /User menu/i }).click(); + + const themeSwitchButton = await screen.findByRole("button", { name: "Switch to dark mode" }); + expect(themeSwitchButton).toBeInTheDocument(); + + expect(spy).not.toHaveBeenCalled(); + fireEvent.click(themeSwitchButton); + expect(spy).toHaveBeenCalledWith("use_system_theme", null, "device", false); + expect(spy).toHaveBeenCalledWith("theme", null, "device", "dark"); + + fireEvent.click(themeSwitchButton); + expect(spy).toHaveBeenCalledWith("use_system_theme", null, "device", false); + expect(spy).toHaveBeenCalledWith("theme", null, "device", "light"); + }); }); diff --git a/test/unit-tests/settings/watchers/ThemeWatcher-test.tsx b/test/unit-tests/settings/watchers/ThemeWatcher-test.tsx index 3b98e504dd..325b55693c 100644 --- a/test/unit-tests/settings/watchers/ThemeWatcher-test.tsx +++ b/test/unit-tests/settings/watchers/ThemeWatcher-test.tsx @@ -189,4 +189,20 @@ describe("ThemeWatcher", function () { const themeWatcher = new ThemeWatcher(); expect(themeWatcher.getEffectiveTheme()).toBe("dark"); }); + + it("should identify custom dark themes as dark", () => { + SettingsStore.getValueAt = makeGetValueAt({ use_system_theme: false, theme: "custom-darkula" }); + SettingsStore.getValue = makeGetValue({ + custom_themes: [ + { + name: "darkula", + is_dark: true, + }, + ], + }); + + const themeWatcher = new ThemeWatcher(); + expect(themeWatcher.getEffectiveTheme()).toBe("custom-darkula"); + expect(themeWatcher.isUserOnDarkTheme()).toBe(true); + }); });