feat: Have theme follow system pref

This commit is contained in:
Tom Moor
2020-05-20 23:19:07 -07:00
parent 218b0ea76a
commit 672ffacc5b
5 changed files with 88 additions and 23 deletions

View File

@ -23,6 +23,7 @@ type Props = {
onClose?: () => void,
children?: Children,
className?: string,
hover?: boolean,
style?: Object,
position?: 'left' | 'right' | 'center',
};
@ -72,7 +73,7 @@ class DropdownMenu extends React.Component<Props> {
this.initPosition();
// attempt to keep only one flyout menu open at once
if (previousClosePortal) {
if (previousClosePortal && !this.props.hover) {
previousClosePortal();
}
previousClosePortal = closePortal;
@ -134,7 +135,7 @@ class DropdownMenu extends React.Component<Props> {
}
render() {
const { className, label, children } = this.props;
const { className, hover, label, children } = this.props;
return (
<div className={className}>
@ -146,7 +147,14 @@ class DropdownMenu extends React.Component<Props> {
>
{({ closePortal, openPortal, isOpen, portal }) => (
<React.Fragment>
<Label onClick={this.handleOpen(openPortal, closePortal)}>
<Label
onMouseOver={
hover ? this.handleOpen(openPortal, closePortal) : undefined
}
onClick={
hover ? undefined : this.handleOpen(openPortal, closePortal)
}
>
{label || (
<NudeButton
id={`${this.id}button`}
@ -216,6 +224,7 @@ const Position = styled.div`
z-index: 1000;
transform: ${props =>
props.position === 'center' ? 'translateX(-50%)' : 'initial'};
pointer-events: none;
`;
const Menu = styled.div`
@ -228,6 +237,7 @@ const Menu = styled.div`
overflow: hidden;
overflow-y: auto;
box-shadow: ${props => props.theme.menuShadow};
pointer-events: all;
@media print {
display: none;

View File

@ -1,14 +1,22 @@
// @flow
import * as React from 'react';
import { CheckmarkIcon } from 'outline-icons';
import styled from 'styled-components';
type Props = {
onClick?: (SyntheticEvent<>) => void | Promise<void>,
children?: React.Node,
selected?: boolean,
disabled?: boolean,
};
const DropdownMenuItem = ({ onClick, children, disabled, ...rest }: Props) => {
const DropdownMenuItem = ({
onClick,
children,
selected,
disabled,
...rest
}: Props) => {
return (
<MenuItem
onClick={disabled ? undefined : onClick}
@ -17,6 +25,9 @@ const DropdownMenuItem = ({ onClick, children, disabled, ...rest }: Props) => {
tabIndex="-1"
{...rest}
>
{selected !== undefined && (
<CheckmarkIcon color={selected === false ? 'transparent' : undefined} />
)}
{children}
</MenuItem>
);
@ -26,6 +37,7 @@ const MenuItem = styled.a`
display: flex;
margin: 0;
padding: 6px 12px;
width: 100%;
height: 32px;
color: ${props =>

View File

@ -13,7 +13,7 @@ type Props = {
function Theme({ children, ui }: Props) {
return (
<ThemeProvider theme={ui.theme === 'dark' ? dark : light}>
<ThemeProvider theme={ui.resolvedTheme === 'dark' ? dark : light}>
<React.Fragment>
<GlobalStyles />
{children}

View File

@ -3,7 +3,7 @@ import * as React from 'react';
import { Link } from 'react-router-dom';
import { observable } from 'mobx';
import { inject, observer } from 'mobx-react';
import { SunIcon, MoonIcon } from 'outline-icons';
import { SunIcon, MoonIcon, EyeIcon, CheckmarkIcon } from 'outline-icons';
import styled from 'styled-components';
import UiStore from 'stores/UiStore';
import AuthStore from 'stores/AuthStore';
@ -82,19 +82,42 @@ class AccountMenu extends React.Component<Props> {
Report a bug
</DropdownMenuItem>
<hr />
<DropdownMenuItem onClick={ui.toggleDarkMode}>
<DropdownMenu
hover
position="right"
style={{
left: 170,
position: 'relative',
top: -34,
}}
label={
<DropdownMenuItem>
<ChangeTheme justify="space-between">
{isLightTheme ? (
<React.Fragment>
Dark theme <MoonIcon />
</React.Fragment>
) : (
<React.Fragment>
Light theme <SunIcon />
</React.Fragment>
)}
Appearance
{ui.resolvedTheme === 'light' ? <SunIcon /> : <MoonIcon />}
</ChangeTheme>
</DropdownMenuItem>
}
>
<DropdownMenuItem
onClick={() => ui.setTheme('system')}
selected={ui.theme === 'system'}
>
System
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => ui.setTheme('light')}
selected={ui.theme === 'light'}
>
Light
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => ui.setTheme('dark')}
selected={ui.theme === 'dark'}
>
Dark
</DropdownMenuItem>
</DropdownMenu>
<hr />
<DropdownMenuItem onClick={this.handleLogout}>
Log out

View File

@ -9,7 +9,8 @@ import type { Toast } from '../types';
const UI_STORE = 'UI_STORE';
class UiStore {
@observable theme: 'light' | 'dark';
@observable theme: 'light' | 'dark' | 'system';
@observable systemTheme: 'light' | 'dark';
@observable activeModalName: ?string;
@observable activeModalProps: ?Object;
@observable activeDocumentId: ?string;
@ -29,9 +30,19 @@ class UiStore {
// no-op Safari private mode
}
let colorSchemeQueryList = window.matchMedia(
'(prefers-color-scheme: dark)'
);
const setSystemTheme = event => {
this.systemTheme = event.matches ? 'dark' : 'light';
};
setSystemTheme(colorSchemeQueryList);
colorSchemeQueryList.addListener(setSystemTheme);
// persisted keys
this.tocVisible = data.tocVisible;
this.theme = data.theme || 'light';
this.theme = data.theme || 'system';
autorun(() => {
try {
@ -43,8 +54,8 @@ class UiStore {
}
@action
toggleDarkMode = () => {
this.theme = this.theme === 'dark' ? 'light' : 'dark';
setTheme = (theme: 'light' | 'dark' | 'system') => {
this.theme = theme;
if (window.localStorage) {
window.localStorage.setItem('theme', this.theme);
@ -153,6 +164,15 @@ class UiStore {
this.toasts.delete(id);
};
@computed
get resolvedTheme(): 'dark' | 'light' {
if (this.theme === 'system') {
return this.systemTheme;
}
return this.theme;
}
@computed
get orderedToasts(): Toast[] {
return orderBy(Array.from(this.toasts.values()), 'createdAt', 'desc');