Skip to content

Commit 4402676

Browse files
committed
main 🧊 add use css var, use mutation observer
1 parent 244c5b2 commit 4402676

File tree

10 files changed

+311
-46
lines changed

10 files changed

+311
-46
lines changed

‎src/hooks/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export { useBrowserLanguage } from './useBrowserLanguage/useBrowserLanguage';
66
export { useClickOutside } from './useClickOutside/useClickOutside';
77
export { useClipboard } from './useClipboard/useClipboard';
88
export { useCounter } from './useCounter/useCounter';
9+
export { useCssVar } from './useCssVar/useCssVar';
910
export { useDebounceCallback } from './useDebounceCallback/useDebounceCallback';
1011
export { useDebounceValue } from './useDebounceValue/useDebounceValue';
1112
export { useDefault } from './useDefault/useDefault';
@@ -49,6 +50,7 @@ export { useMemory } from './useMemory/useMemory';
4950
export { useMount } from './useMount/useMount';
5051
export { useMouse } from './useMouse/useMouse';
5152
export { useMutation } from './useMutation/useMutation';
53+
export { useMutationObserver } from './useMutationObserver/useMutationObserver';
5254
export { useNetwork } from './useNetwork/useNetwork';
5355
export { useOffsetPagination } from './useOffsetPagination/useOffsetPagination';
5456
export { useOnline } from './useOnline/useOnline';
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useCssVar } from './useCssVar';
2+
3+
const Demo = () => {
4+
const key = '--color';
5+
const colorVar = useCssVar(key, '#7fa998');
6+
const style = {
7+
color: 'var(--color)'
8+
};
9+
10+
const switchColor = () => {
11+
if (colorVar.value === '#df8543') colorVar.set('#7fa998');
12+
else colorVar.set('#df8543');
13+
};
14+
15+
return (
16+
<>
17+
<p style={style}>Sample text, {colorVar.value}</p>
18+
<button type='button' onClick={switchColor}>
19+
Change Color
20+
</button>
21+
</>
22+
);
23+
};
24+
25+
export default Demo;

‎src/hooks/useCssVar/useCssVar.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type { RefObject } from 'react';
2+
import { useState } from 'react';
3+
4+
import { getElement } from '@/utils/helpers';
5+
6+
import { useMount } from '../useMount/useMount';
7+
import { useMutationObserver } from '../useMutationObserver/useMutationObserver';
8+
9+
/** The css variable target element type */
10+
export type UseCssVarTarget = RefObject<Element | null | undefined> | (() => Element) | Element;
11+
12+
/** The css variable return type */
13+
export interface UseCssVarReturn {
14+
/** The value of the CSS variable */
15+
value: string;
16+
/** Set the value of the CSS variable */
17+
set: (value: string) => void;
18+
}
19+
20+
export interface UseCssVar {
21+
<Target extends UseCssVarTarget>(
22+
target: Target,
23+
key: string,
24+
initialValue?: string
25+
): UseCssVarReturn;
26+
27+
(key: string, initialValue?: string): UseCssVarReturn;
28+
}
29+
30+
/**
31+
* @name useCssVar
32+
* @description - Hook that returns the value of a CSS variable
33+
* @category Utilities
34+
*
35+
* @overload
36+
* @param {string} key The CSS variable key
37+
* @param {string} initialValue The initial value of the CSS variable
38+
* @returns {UseCssVarReturn} The object containing the value of the CSS variable
39+
*
40+
* @example
41+
* const { value, set } = useCssVar('color', 'red');
42+
*
43+
* @overload
44+
* @template Target The target element
45+
* @param {Target} target The target element
46+
* @param {string} key The CSS variable key
47+
* @param {string} initialValue The initial value of the CSS variable
48+
* @returns {UseCssVarReturn} The object containing the value of the CSS variable
49+
*
50+
* @example
51+
* const { value, set } = useCssVar(ref, 'color', 'red');
52+
*/
53+
export const useCssVar = ((...params: any[]) => {
54+
const target = (typeof params[0] === 'object' ? params[0] : undefined) as
55+
| UseCssVarTarget
56+
| undefined;
57+
const key = (target ? params[1] : params[0]) as string;
58+
const initialValue = (target ? params[2] : params[1]) as string | undefined;
59+
60+
const [value, setValue] = useState(initialValue ?? '');
61+
62+
const set = (value: string) => {
63+
const element = getElement(target ?? window?.document?.documentElement) as HTMLElement;
64+
65+
if (element.style) {
66+
if (!value) {
67+
element.style.removeProperty(key);
68+
setValue(value);
69+
return;
70+
}
71+
72+
element.style.setProperty(key, value);
73+
setValue(value);
74+
}
75+
};
76+
77+
const updateCssVar = () => {
78+
const element = getElement(target ?? window?.document?.documentElement) as HTMLElement;
79+
if (!element) return;
80+
81+
const value = window
82+
.getComputedStyle(element as Element)
83+
.getPropertyValue(key)
84+
?.trim();
85+
86+
setValue(value ?? initialValue);
87+
};
88+
89+
useMount(() => {
90+
if (initialValue) set(initialValue);
91+
});
92+
93+
useMutationObserver(window, updateCssVar, {
94+
attributeFilter: ['style', 'class']
95+
});
96+
97+
return {
98+
value,
99+
set
100+
};
101+
}) as UseCssVar;

‎src/hooks/useDocumentTitle/useDocumentTitle.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useRef, useState } from 'react';
22

33
import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect/useIsomorphicLayoutEffect';
4-
import { useMutationObserver } from '../useMutationObserver';
4+
import { useMutationObserver } from '../useMutationObserver/useMutationObserver';
55

66
/** The use document title options type */
77
export interface UseDocumentTitleOptions {
@@ -38,13 +38,13 @@ export function useDocumentTitle(
3838
const [title, setTitle] = useState(value ?? document.title);
3939

4040
useMutationObserver(
41+
document.head.querySelector('title')!,
4142
() => {
4243
if (document && document.title !== title) {
4344
setTitle(document.title);
4445
}
4546
},
46-
{ childList: true },
47-
document.head.querySelector('title')
47+
{ childList: true }
4848
);
4949

5050
useIsomorphicLayoutEffect(() => {

‎src/hooks/useFullscreen/useFullscreen.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export const useFullscreen = ((...params: any[]) => {
8181
| undefined;
8282
const options = (target ? params[1] : params[0]) as UseFullScreenOptions | undefined;
8383

84-
const internalRef = useRef<Element>(null);
84+
const internalRef = useRef<Element>();
8585
const [value, setValue] = useState(options?.initialValue ?? false);
8686

8787
const onChange = () => {

‎src/hooks/useHotkeys/useHotkeys.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export type UseHotkeysKey = { key: string; code: string; alias: string };
4141
* @param {UseHotkeysHotkeys} hotkeys The key or keys to listen for
4242
* @param {(event: KeyboardEvent) => void} callback The callback function to be called when the hotkey is pressed
4343
* @param {UseEventListenerTarget} [options.target=window] The target to attach the event listeners to
44-
* @param {boolean} [options.enabled=true] Enable or disable the event listeners
44+
* @param {boolean} [options.enabled=true] The enable or disable the event listeners
4545
* @param {boolean} [options.preventDefault=true] Whether to prevent the default behavior of the event
4646
* @param {Record<string, string>} [options.aliasMap] Alias map for hotkeys
4747
* @returns {useKeysPressedReturns} Array of strings with keys that were press

‎src/hooks/useMutationObserver.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useRef, useState } from 'react';
2+
3+
import { useMutationObserver } from './useMutationObserver';
4+
5+
export const Demo = () => {
6+
const [observed, setObserved] = useState(false);
7+
const buttonRef = useRef<HTMLButtonElement>(null);
8+
9+
useMutationObserver(
10+
buttonRef,
11+
(mutations) => {
12+
for (const mutation of mutations) {
13+
if (mutation.type === 'attributes') {
14+
setObserved(true);
15+
}
16+
}
17+
},
18+
{ attributes: true }
19+
);
20+
21+
const addAttribute = () => {
22+
if (!buttonRef.current) return;
23+
24+
buttonRef.current.setAttribute('data-mut', 'hello world');
25+
};
26+
27+
return (
28+
<div>
29+
<div>{observed ? 'Observed attribute change to node' : 'No changes observed yet'}</div>
30+
31+
<button ref={buttonRef} type='button' onClick={addAttribute} disabled={observed}>
32+
{observed ? 'Added Attribute To Node' : 'Add Attribute To Node'}
33+
</button>
34+
</div>
35+
);
36+
};
37+
38+
export default Demo;
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import type { RefObject } from 'react';
2+
import { useEffect, useRef, useState } from 'react';
3+
4+
import { getElement } from '@/utils/helpers';
5+
6+
/** The mutation observer target element type */
7+
export type UseMutationObserverTarget =
8+
| RefObject<Element | null | undefined>
9+
| (() => Element)
10+
| Element
11+
| Window
12+
| Document;
13+
14+
/** The mutation observer return type */
15+
export interface UseMutationObserverReturn {
16+
/** The mutation observer entries */
17+
stop: () => void;
18+
/** The mutation observer instance */
19+
observer: MutationObserver;
20+
}
21+
22+
/** The mutation observer options type */
23+
export interface UseMutationObserverOptions extends MutationObserverInit {
24+
/** The enabled state of the mutation observer */
25+
enabled?: boolean;
26+
}
27+
28+
export type UseResizeObserver = {
29+
<Target extends UseMutationObserverTarget | UseMutationObserverTarget[]>(
30+
target: Target,
31+
callback: MutationCallback,
32+
options?: UseMutationObserverOptions
33+
): UseMutationObserverReturn;
34+
35+
<Target extends UseMutationObserverTarget | UseMutationObserverTarget[]>(
36+
callback: MutationCallback,
37+
options?: UseMutationObserverOptions,
38+
target?: never
39+
): UseMutationObserverReturn & { ref: (node: Target) => void };
40+
};
41+
42+
/**
43+
* @name useMutationObserver
44+
* @description - Hook that gives you mutation observer state
45+
* @category Browser
46+
*
47+
* @overload
48+
* @template Target The target element
49+
* @param {MutationCallback} callback The callback to execute when mutation is detected
50+
* @param {boolean} [options.enabled=true] The enabled state of the mutation observer
51+
* @param {boolean} [options.attributes] Set to true if mutations to target's attributes are to be observed
52+
* @param {boolean} [options.characterData] Set to true if mutations to target's data are to be observed
53+
* @param {boolean} [options.childList] Set to true if mutations to target's children are to be observed
54+
* @param {boolean} [options.subtree] Set to true if mutations to not just target, but also target's descendants are to be observed
55+
* @param {boolean} [options.characterDataOldValue] Set to true if characterData is set to true or omitted and target's data before the mutation needs to be recorded
56+
* @param {boolean} [options.attributeOldValue] Set to a list of attribute local names (without namespace) if not all attribute mutations need to be observed and attributes is true or omitted
57+
* @param {string[]} [options.attributeFilter] Set to a list of attribute local names (without namespace) if not all attribute mutations need to be observed and attributes is true or omitted
58+
* @returns {UseMutationObserverReturn & { ref: (node: Target) => void }} An object containing the mutation observer state
59+
*
60+
* @example
61+
* const { ref, observer, stop } = useMutationObserver(() => console.log('callback'))
62+
*
63+
* @overload
64+
* @template Target The target element
65+
* @param {Target} target The target element to observe
66+
* @param {MutationCallback} callback The callback to execute when mutation is detected
67+
* @param {boolean} [options.enabled=true] The enabled state of the mutation observer
68+
* @param {boolean} [options.attributes] Set to true if mutations to target's attributes are to be observed
69+
* @param {boolean} [options.characterData] Set to true if mutations to target's data are to be observed
70+
* @param {boolean} [options.childList] Set to true if mutations to target's children are to be observed
71+
* @param {boolean} [options.subtree] Set to true if mutations to not just target, but also target's descendants are to be observed
72+
* @param {boolean} [options.characterDataOldValue] Set to true if characterData is set to true or omitted and target's data before the mutation needs to be recorded
73+
* @param {boolean} [options.attributeOldValue] Set to a list of attribute local names (without namespace) if not all attribute mutations need to be observed and attributes is true or omitted
74+
* @param {string[]} [options.attributeFilter] Set to a list of attribute local names (without namespace) if not all attribute mutations need to be observed and attributes is true or omitted
75+
* @returns {UseMutationObserverReturn} An object containing the mutation observer state
76+
*
77+
* @example
78+
* const { observer, stop } = useMutationObserver(ref, () => console.log('callback'))
79+
*/
80+
export const useMutationObserver = ((...params: any[]) => {
81+
const target = (
82+
typeof params[0] === 'object' && !('current' in params[0]) ? undefined : params[0]
83+
) as UseMutationObserverTarget | UseMutationObserverTarget[] | undefined;
84+
const callback = (target ? params[1] : params[0]) as MutationCallback;
85+
const options = (target ? params[2] : params[1]) as UseMutationObserverOptions | undefined;
86+
87+
const [observer, setObserver] = useState<MutationObserver>();
88+
const enabled = options?.enabled ?? true;
89+
90+
const [internalRef, setInternalRef] = useState<Element>();
91+
const internalCallbackRef = useRef<MutationCallback>(callback);
92+
internalCallbackRef.current = callback;
93+
94+
useEffect(() => {
95+
if (!enabled && !target && !internalRef) return;
96+
97+
if (Array.isArray(target)) {
98+
if (!target.length) return;
99+
const observer = new MutationObserver(internalCallbackRef.current);
100+
setObserver(observer);
101+
target.forEach((target) => {
102+
const element = getElement(target);
103+
if (!element) return;
104+
observer.observe(element as Element, options);
105+
});
106+
107+
return () => {
108+
observer.disconnect();
109+
};
110+
}
111+
112+
const element = target ? getElement(target) : internalRef;
113+
if (!element) return;
114+
115+
const observer = new MutationObserver(internalCallbackRef.current);
116+
setObserver(observer);
117+
observer.observe(element as Element, options);
118+
119+
return () => {
120+
observer.disconnect();
121+
};
122+
}, [internalRef, target, ...Object.values(options ?? {})]);
123+
124+
const stop = () => observer?.disconnect();
125+
126+
if (target) return { stop, observer };
127+
return {
128+
ref: setInternalRef,
129+
stop,
130+
observer
131+
};
132+
}) as UseResizeObserver;

0 commit comments

Comments
 (0)