This repository was archived by the owner on Apr 26, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1k
feat: Enhanced Dark Mode prototype #352
Merged
amiller-gh
merged 8 commits into
nodejs:staging
from
SMotaal:feature-enhanced-dark-mode
Oct 28, 2019
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
037e80f
feat: Enhanced Dark Mode prototype
9aca8eb
style: Lint
8b8a398
fix: Resolve build-ci bugs
cbda851
fix: Subtle CSS aspects
fa888c8
style: Lint
654dc9e
test: Artifacts from update-snapshots
e4cdb66
refactor: Rewire Enhanced Dark Mode feature into existing button
d3449b4
test: Artifacts from update-snapshots
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import React from 'react'; | ||
| import { css } from '@emotion/core'; | ||
| import DarkModeController from '../util/DarkModeController'; | ||
|
|
||
| const controlsStyles = { | ||
| header: css/* scss */ ` | ||
| position: fixed; | ||
| padding: 0.25ch 1ch; | ||
| margin-bottom: 1ch; | ||
| margin-top: -1ch; | ||
| right: 0; | ||
| z-index: 999; | ||
| box-sizing: border-box; | ||
| will-change: transform; | ||
|
|
||
| display: grid; | ||
| grid-auto-flow: column dense; | ||
| grid-gap: 1ch; | ||
| align-items: center; | ||
|
|
||
| opacity: 0.9; | ||
| color: var(--color-text-accent, #999); | ||
| background-color: var(--black9, #9993); | ||
| border-top-left-radius: 1ch; | ||
| border-bottom-left-radius: 1ch; | ||
|
|
||
| min-width: max-content; | ||
| width: 0; | ||
| white-space: normal; | ||
| text-size-adjust: 100%; | ||
| text-shadow: #333f46 0px 0.875px 0px; | ||
| user-select: none; | ||
| `, | ||
| button: css/* scss */ ` | ||
| color: inherit; | ||
| border: none; | ||
| width: max-content; | ||
| display: contents; | ||
| `, | ||
| controls: css/* scss */ ` | ||
| color: inherit; | ||
| `, | ||
| }; | ||
|
|
||
| interface Props { | ||
| lightModeIcon?: string; | ||
| darkModeIcon?: string; | ||
| controller?: DarkModeController; | ||
| } | ||
|
|
||
| const Controls = ({ | ||
| lightModeIcon = 'wb_sunny', | ||
| darkModeIcon = 'nights_stay', | ||
| controller = new DarkModeController(), | ||
| }: Props) => ( | ||
| <header css={controlsStyles.header}> | ||
| <div id="controls" css={controlsStyles.controls}> | ||
| <span> | ||
| <button | ||
| type="button" | ||
| css={controlsStyles.button} | ||
| id="contrast" | ||
| title="Dark/Light" | ||
| onPointerDown={(): void => { | ||
| controller.onPointerDown(); | ||
| }} | ||
| onPointerUp={(): void => { | ||
| controller.onPointerUp(); | ||
| }} | ||
| > | ||
| <span className="sr-only">Toggle Dark Mode</span> | ||
| <i className="material-icons light-mode-only">{darkModeIcon}</i> | ||
| <i className="material-icons dark-mode-only">{lightModeIcon}</i> | ||
| </button> | ||
| </span> | ||
| </div> | ||
| </header> | ||
| ); | ||
|
|
||
| export default Controls; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| // @ts-check | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. May I recommend you publish this as its own module? :) I'm sure many other projects would enjoy a lightweight dark mode management util. |
||
|
|
||
| /*eslint-disable */ | ||
|
|
||
| export default class DarkModeController { | ||
| static get timeout() { | ||
| const value = Symbol.for('dark-mode.toggler.timeout'); | ||
| Object.defineProperty(this, 'timeout', { value, writable: false }); | ||
| return value; | ||
| } | ||
|
|
||
| static get resetting() { | ||
| const value = Symbol.for('dark-mode.toggler.resetting'); | ||
| Object.defineProperty(this, 'resetting', { value, writable: false }); | ||
| return value; | ||
| } | ||
|
|
||
| static get prefersLightMode() { | ||
| const value = | ||
| (typeof matchMedia === 'function' && | ||
| matchMedia('(prefers-color-scheme: light)')) || | ||
| undefined; | ||
| Object.defineProperty(this, 'prefersLightMode', { value, writable: false }); | ||
| return value; | ||
| } | ||
|
|
||
| static get prefersDarkMode() { | ||
| const value = | ||
| (typeof matchMedia === 'function' && | ||
| matchMedia('(prefers-color-scheme: dark)')) || | ||
| undefined; | ||
| Object.defineProperty(this, 'prefersDarkMode', { value, writable: false }); | ||
| return value; | ||
| } | ||
|
|
||
| /** @param {HTMLElement} [target] */ | ||
| constructor(target) { | ||
| Object.defineProperties(this, { | ||
| target: { | ||
| value: | ||
| /** @type {HTMLElement|undefined} */ (target || | ||
| (typeof document === 'object' && document.body) || | ||
| undefined), | ||
| writable: false, | ||
| }, | ||
| [DarkModeController.timeout]: { | ||
| value: /** @type {number|undefined} */ (undefined), | ||
| writable: true, | ||
| }, | ||
| [DarkModeController.resetting]: { | ||
| value: /** @type {boolean|undefined} */ (undefined), | ||
| writable: true, | ||
| }, | ||
| state: { | ||
| value: /** @type {DarkModeState|undefined} */ (undefined), | ||
| writable: true, | ||
| }, | ||
| prefers: { | ||
| value: /** @type {PrefersColorSchemes|undefined} */ (undefined), | ||
| writable: true, | ||
| }, | ||
| enable: { value: this.enable.bind(this), writable: false }, | ||
| disable: { value: this.disable.bind(this), writable: false }, | ||
| toggle: { value: this.toggle.bind(this), writable: false }, | ||
| onPointerDown: { value: this.onPointerDown.bind(this), writable: false }, | ||
| onPointerUp: { value: this.onPointerUp.bind(this), writable: false }, | ||
| }); | ||
|
|
||
| ((prefersDarkMode, prefersLightMode, localStorage) => { | ||
| if (!localStorage || !prefersDarkMode || !prefersLightMode) return; | ||
| localStorage.darkMode === 'enabled' | ||
| ? ((this.state = 'enabled'), this.enable()) | ||
| : localStorage.darkMode === 'disabled' | ||
| ? ((this.state = 'disabled'), this.disable()) | ||
| : this.toggle( | ||
| prefersDarkMode.matches === true || | ||
| prefersLightMode.matches !== true, | ||
| !!(localStorage.darkMode = this.state = 'auto') | ||
| ); | ||
| prefersDarkMode.addListener( | ||
| ({ matches = false }) => | ||
| matches === true && this.toggle(!!matches, true) | ||
| ); | ||
| prefersLightMode.addListener( | ||
| ({ matches = false }) => matches === true && this.toggle(!matches, true) | ||
| ); | ||
| })( | ||
| DarkModeController.prefersDarkMode, | ||
| DarkModeController.prefersLightMode, | ||
| (typeof localStorage === 'object' && localStorage) || undefined | ||
| ); | ||
|
|
||
| Object.preventExtensions(this); | ||
| } | ||
|
|
||
| /** | ||
| * @param {DarkModeState|boolean} [state] | ||
| * @param {boolean} [auto] | ||
| */ | ||
| async toggle(state, auto) { | ||
| const { classList } = this.target; | ||
|
|
||
| if (auto === true) { | ||
| if (state === true) this.prefers = 'dark'; | ||
| else if (state === false) this.prefers = 'light'; | ||
| if (this.state !== 'auto') return; | ||
| } | ||
|
|
||
| state = | ||
| state === 'auto' | ||
| ? ((auto = true), this.prefers !== 'light') | ||
| : state == null | ||
| ? !classList.contains('dark-mode') | ||
| : !!state; | ||
|
|
||
| this.state = localStorage.darkMode = auto | ||
| ? 'auto' | ||
| : state | ||
| ? 'enabled' | ||
| : 'disabled'; | ||
|
|
||
| state | ||
| ? (classList.add('dark-mode'), classList.remove('light-mode')) | ||
| : (classList.add('light-mode'), classList.remove('dark-mode')); | ||
| } | ||
|
|
||
| /** @param {boolean} [auto] */ | ||
| enable(auto) { | ||
| this.toggle(true, auto); | ||
| } | ||
|
|
||
| /** @param {boolean} [auto] */ | ||
| disable(auto) { | ||
| this.toggle(false, auto); | ||
| } | ||
|
|
||
| onPointerDown() { | ||
| clearTimeout(this[DarkModeController.timeout]); | ||
| this[DarkModeController.timeout] = setTimeout(() => { | ||
| this.toggle('auto'); | ||
| this[DarkModeController.resetting] = true; | ||
| // console.log('Reset dark mode!'); | ||
| }, 2000); | ||
| } | ||
|
|
||
| onPointerUp() { | ||
| this[DarkModeController.timeout] = clearTimeout( | ||
| this[DarkModeController.timeout] | ||
| ); | ||
| this[DarkModeController.resetting] === true | ||
| ? (this[DarkModeController.resetting] = false) | ||
| : this.toggle(); | ||
| } | ||
| } | ||
|
|
||
| Object.preventExtensions(DarkModeController); | ||
|
|
||
| /** @typedef {'auto'|'enabled'|'disabled'} DarkModeState */ | ||
| /** @typedef {'light'|'dark'} PrefersColorSchemes */ | ||
|
|
||
| /* eslint-enable */ | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you type this as required instead of optional you can safely remove the existence checks below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alt: Just have the header import the controller. May require structural changes to the controller code to not maintain its own state and instead run off local storage / user preferences. This way, most of those helper methods can exist on the module export as pure functions instead of requiring a new instance to be made each time.