-
Notifications
You must be signed in to change notification settings - Fork 74
Expand file tree
/
Copy pathuseColorMode.ts
More file actions
151 lines (131 loc) Β· 5.21 KB
/
useColorMode.ts
File metadata and controls
151 lines (131 loc) Β· 5.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import type { RefObject } from 'react';
import { getElement } from '@/utils/helpers';
import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect';
import { useMutationObserver } from '../useMutationObserver/useMutationObserver';
import type { UseStorageInitialValue, UseStorageOptions } from '../useStorage/useStorage';
import { useStorage } from '../useStorage/useStorage';
export type BasicColorMode = 'light' | 'dark';
export type BasicColorSchema = BasicColorMode | 'auto';
const memoryStorageMap = new Map<BasicColorSchema, string>();
const memoryStorage = {
getItem: (key: BasicColorSchema) => memoryStorageMap.get(key) ?? null,
setItem: (key: BasicColorSchema, value: string) => memoryStorageMap.set(key, value),
removeItem: (key: BasicColorSchema) => memoryStorageMap.delete(key),
length: memoryStorageMap.size,
key: () => null,
clear: () => memoryStorageMap.clear()
};
const CSS_DISABLE_TRANS = '*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}';
export type UseColorModeTarget =
| RefObject<Element | null | undefined>
| (() => Element)
| Element;
/** The use color mode return type */
export type UseColorModeReturn<T extends string = BasicColorMode> = [
/** Current color mode */
string | undefined,
/** Function to set the color mode */
(value: T) => void
];
export interface UseColorModeOptions<
T extends string = BasicColorMode,
Target extends UseColorModeTarget = UseColorModeTarget
>
extends UseStorageOptions<T | BasicColorMode> {
/** Target element applying to */
target?: Target;
/** HTML attribute applying the target element */
attribute?: string;
/** The initial color mode */
initialValue?: UseStorageInitialValue<T | BasicColorMode>;
/** Prefix when adding value to the attribute */
modes?: Partial<Record<T | BasicColorSchema, string>>;
/** A custom handler for handle the updates. When specified, the default behavior will be overridden. */
storageKey?: string | null;
/** Storage object, can be localStorage or sessionStorage */
storage?: Storage;
/** Disable CSS transitions */
disableTransition?: boolean;
}
/**
* @name useColorMode
* @description - Hook that work with color mode
* @category Utilities
*
* @template T The color mode type
* @template Target The target element
* @param {Target} [options.target=document.documentElement] The target element applying to
* @param {string} [options.attribute='class'] HTML attribute applying the target element
* @param {string} [options.initialValue='auto'] The initial color mode
* @param {Record<string, string>} [options.modes={light: 'light', dark: 'dark', auto: ''}] The color modes
* @param {string} [options.storageKey='reactuse-color-scheme'] Prefix when adding value to the attribute
* @param {Storage} [options.storage=localStorage] Storage object
* @param {boolean} [options.disableTransition=true] Disable CSS transitions
* @returns {UseColorModeReturn} An object containing the color mode
*
* @example
* const {mode, setMode} = useColorMode();
*/
export const useColorMode = <
T extends string = BasicColorMode,
Target extends UseColorModeTarget = UseColorModeTarget
> (options?: UseColorModeOptions<T, Target>): UseColorModeReturn<T | BasicColorSchema> => {
const {
target = document.documentElement,
attribute = 'class',
initialValue = 'auto',
storageKey = 'reactuse-color-scheme',
storage,
modes = {},
disableTransition = true
} = options ?? {};
const possibleModes = {
auto: '',
light: 'light',
dark: 'dark',
...(modes || {})
} as Record<BasicColorSchema | T, string>;
const calculatedStorage = !storage && storageKey === null ? memoryStorage : storage;
const { value: mode, set } = useStorage(storageKey ?? '', { initialValue, storage: calculatedStorage });
useMutationObserver(target, () => {
const element = getElement(target) as HTMLElement | null | undefined;
if (!element) {
return;
}
const attributeValue = element.getAttribute(attribute);
if (attributeValue && attributeValue in possibleModes) {
set(possibleModes[attributeValue as BasicColorMode | T]);
} else {
set('light');
}
}, {
attributes: true
});
const setMode = (value: BasicColorSchema | T) => {
if (value === 'auto') {
const media = window?.matchMedia('(prefers-color-scheme: dark)');
value = media?.matches ? 'dark' : 'light';
}
const element = getElement(target) as HTMLElement | null | undefined;
if (element) {
const attributeValue = possibleModes[value];
element.setAttribute(attribute, attributeValue);
}
set(possibleModes[value]);
if (disableTransition) {
const style = window.document.createElement('style');
style.appendChild(document.createTextNode(CSS_DISABLE_TRANS));
window.document.head.appendChild(style);
// Calling getComputedStyle forces the browser to redraw
const _ = window.getComputedStyle(style).opacity;
document.head.removeChild(style!);
}
};
useIsomorphicLayoutEffect(() => {
setMode(mode as T | BasicColorMode);
}, []);
return [
mode,
setMode
];
};