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

View File

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

View File

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

View File

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

View File

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