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>
This commit is contained in:
committed by
GitHub
parent
c2ed88fbf0
commit
fd4695f3d5
@@ -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<DOMRect, "width" | "left" | "top" | "height">;
|
||||
|
||||
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 (
|
||||
<IconButton
|
||||
ref={ref}
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
className="mx_UserMenu_contextMenu_themeButton"
|
||||
onClick={onSwitchThemeClick}
|
||||
tooltip={isDark ? _t("user_menu|switch_theme_light") : _t("user_menu|switch_theme_dark")}
|
||||
size="32px"
|
||||
kind="secondary"
|
||||
>
|
||||
<ThemeIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default class UserMenu extends React.Component<IProps, IState> {
|
||||
public static contextType = SDKContext;
|
||||
declare public context: React.ContextType<typeof SDKContext>;
|
||||
@@ -97,8 +143,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||
|
||||
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<IProps, IState> {
|
||||
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<IProps, IState> {
|
||||
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<void> => {
|
||||
// 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<IProps, IState> {
|
||||
});
|
||||
};
|
||||
|
||||
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<IProps, IState> {
|
||||
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<string, any>): void => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
@@ -398,17 +391,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<RovingAccessibleButton
|
||||
className="mx_UserMenu_contextMenu_themeButton"
|
||||
onClick={this.onSwitchThemeClick}
|
||||
title={
|
||||
this.state.isDarkTheme
|
||||
? _t("user_menu|switch_theme_light")
|
||||
: _t("user_menu|switch_theme_dark")
|
||||
}
|
||||
>
|
||||
<ThemeIcon width="16px" height="16px" />
|
||||
</RovingAccessibleButton>
|
||||
<ThemeSwitchButton />
|
||||
</div>
|
||||
{topSection}
|
||||
{primaryOptionList}
|
||||
|
||||
@@ -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<ThemeWatcherEvent, T
|
||||
return SettingsStore.getValue("theme");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if user is on a dark theme, if false implies user is on a light theme
|
||||
*/
|
||||
public isUserOnDarkTheme(): boolean {
|
||||
const theme = this.currentTheme;
|
||||
if (theme.startsWith("custom-")) {
|
||||
return !!getCustomTheme(theme.substring("custom-".length)).is_dark;
|
||||
}
|
||||
return theme === "dark" || theme === "dark-hc";
|
||||
}
|
||||
|
||||
private themeBasedOnSystem(): string | undefined {
|
||||
let newTheme: string | undefined;
|
||||
if (this.preferDark.matches) {
|
||||
|
||||
Reference in New Issue
Block a user