feat: add system mode to the theme selector (#7853)

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Arnost Pleskot 2024-04-08 16:46:24 +02:00 committed by GitHub
parent 92bc08207c
commit cd50aa719f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 301 additions and 56 deletions

View file

@ -1014,7 +1014,7 @@ class App extends React.Component<AppProps, AppState> {
width: 100%;
height: 100%;
color: ${
this.state.theme === "dark" ? "white" : "black"
this.state.theme === THEME.DARK ? "white" : "black"
};
}
body {
@ -1281,7 +1281,7 @@ class App extends React.Component<AppProps, AppState> {
return null;
}
const isDarkTheme = this.state.theme === "dark";
const isDarkTheme = this.state.theme === THEME.DARK;
let frameIndex = 0;
let magicFrameIndex = 0;
@ -2730,7 +2730,7 @@ class App extends React.Component<AppProps, AppState> {
this.excalidrawContainerRef.current?.classList.toggle(
"theme--dark",
this.state.theme === "dark",
this.state.theme === THEME.DARK,
);
if (

View file

@ -14,7 +14,9 @@ export const DarkModeToggle = (props: {
}) => {
const title =
props.title ||
(props.value === "dark" ? t("buttons.lightMode") : t("buttons.darkMode"));
(props.value === THEME.DARK
? t("buttons.lightMode")
: t("buttons.darkMode"));
return (
<ToolButton

View file

@ -3,7 +3,8 @@ import "./RadioGroup.scss";
export type RadioGroupChoice<T> = {
value: T;
label: string;
label: React.ReactNode;
ariaLabel?: string;
};
export type RadioGroupProps<T> = {
@ -26,13 +27,15 @@ export const RadioGroup = function <T>({
className={clsx("RadioGroup__choice", {
active: choice.value === value,
})}
key={choice.label}
key={String(choice.value)}
title={choice.ariaLabel}
>
<input
name={name}
type="radio"
checked={choice.value === value}
onChange={() => onChange(choice.value)}
aria-label={choice.ariaLabel}
/>
{choice.label}
</div>

View file

@ -75,6 +75,12 @@
&__shortcut {
margin-inline-start: auto;
opacity: 0.5;
&--orphaned {
text-align: right;
font-size: 0.875rem;
padding: 0 0.625rem;
}
}
&:hover {
@ -94,6 +100,22 @@
}
}
.dropdown-menu-item-bare {
align-items: center;
height: 2rem;
justify-content: space-between;
@media screen and (min-width: 1921px) {
height: 2.25rem;
}
svg {
width: 1rem;
height: 1rem;
display: block;
}
}
.dropdown-menu-item-custom {
margin-top: 0.5rem;
}

View file

@ -0,0 +1,51 @@
import { useDevice } from "../App";
import { RadioGroup } from "../RadioGroup";
type Props<T> = {
value: T;
shortcut?: string;
choices: {
value: T;
label: React.ReactNode;
ariaLabel?: string;
}[];
onChange: (value: T) => void;
children: React.ReactNode;
name: string;
};
const DropdownMenuItemContentRadio = <T,>({
value,
shortcut,
onChange,
choices,
children,
name,
}: Props<T>) => {
const device = useDevice();
return (
<>
<div className="dropdown-menu-item-base dropdown-menu-item-bare">
<label className="dropdown-menu-item__text" htmlFor={name}>
{children}
</label>
<RadioGroup
name={name}
value={value}
onChange={onChange}
choices={choices}
/>
</div>
{shortcut && !device.editor.isMobile && (
<div className="dropdown-menu-item__shortcut dropdown-menu-item__shortcut--orphaned">
{shortcut}
</div>
)}
</>
);
};
DropdownMenuItemContentRadio.displayName = "DropdownMenuItemContentRadio";
export default DropdownMenuItemContentRadio;

View file

@ -433,15 +433,10 @@ export const MoonIcon = createIcon(
);
export const SunIcon = createIcon(
<g
stroke="currentColor"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
>
<g stroke="currentColor" strokeLinejoin="round">
<path d="M10 12.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM10 4.167V2.5M14.167 5.833l1.166-1.166M15.833 10H17.5M14.167 14.167l1.166 1.166M10 15.833V17.5M5.833 14.167l-1.166 1.166M5 10H3.333M5.833 5.833 4.667 4.667" />
</g>,
modifiedTablerIconProps,
{ ...modifiedTablerIconProps, strokeWidth: 1.5 },
);
export const HamburgerMenuIcon = createIcon(
@ -2092,3 +2087,11 @@ export const coffeeIcon = createIcon(
</g>,
tablerIconProps,
);
export const DeviceDesktopIcon = createIcon(
<g stroke="currentColor">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 5a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1h-16a1 1 0 0 1-1-1v-10zM7 20h10M9 16v4M15 16v4" />
</g>,
{ ...tablerIconProps, strokeWidth: 1.5 },
);

View file

@ -8,6 +8,7 @@ import {
} from "../App";
import {
boltIcon,
DeviceDesktopIcon,
ExportIcon,
ExportImageIcon,
HelpIcon,
@ -35,6 +36,9 @@ import { jotaiScope } from "../../jotai";
import { useUIAppState } from "../../context/ui-appState";
import { openConfirmModal } from "../OverwriteConfirm/OverwriteConfirmState";
import Trans from "../Trans";
import DropdownMenuItemContentRadio from "../dropdownMenu/DropdownMenuItemContentRadio";
import { THEME } from "../../constants";
import type { Theme } from "../../element/types";
import "./DefaultItems.scss";
@ -181,32 +185,80 @@ export const ClearCanvas = () => {
};
ClearCanvas.displayName = "ClearCanvas";
export const ToggleTheme = () => {
export const ToggleTheme = (
props:
| {
allowSystemTheme: true;
theme: Theme | "system";
onSelect: (theme: Theme | "system") => void;
}
| {
allowSystemTheme?: false;
onSelect?: (theme: Theme) => void;
},
) => {
const { t } = useI18n();
const appState = useUIAppState();
const actionManager = useExcalidrawActionManager();
const shortcut = getShortcutFromShortcutName("toggleTheme");
if (!actionManager.isActionEnabled(actionToggleTheme)) {
return null;
}
if (props?.allowSystemTheme) {
return (
<DropdownMenuItemContentRadio
name="theme"
value={props.theme}
onChange={(value: Theme | "system") => props.onSelect(value)}
choices={[
{
value: THEME.LIGHT,
label: SunIcon,
ariaLabel: `${t("buttons.lightMode")} - ${shortcut}`,
},
{
value: THEME.DARK,
label: MoonIcon,
ariaLabel: `${t("buttons.darkMode")} - ${shortcut}`,
},
{
value: "system",
label: DeviceDesktopIcon,
ariaLabel: t("buttons.systemMode"),
},
]}
>
{t("labels.theme")}
</DropdownMenuItemContentRadio>
);
}
return (
<DropdownMenuItem
onSelect={(event) => {
// do not close the menu when changing theme
event.preventDefault();
return actionManager.executeAction(actionToggleTheme);
if (props?.onSelect) {
props.onSelect(
appState.theme === THEME.DARK ? THEME.LIGHT : THEME.DARK,
);
} else {
return actionManager.executeAction(actionToggleTheme);
}
}}
icon={appState.theme === "dark" ? SunIcon : MoonIcon}
icon={appState.theme === THEME.DARK ? SunIcon : MoonIcon}
data-testid="toggle-dark-mode"
shortcut={getShortcutFromShortcutName("toggleTheme")}
shortcut={shortcut}
aria-label={
appState.theme === "dark"
appState.theme === THEME.DARK
? t("buttons.lightMode")
: t("buttons.darkMode")
}
>
{appState.theme === "dark"
{appState.theme === THEME.DARK
? t("buttons.lightMode")
: t("buttons.darkMode")}
</DropdownMenuItem>