Skip to content

Commit ae4f7b2

Browse files
committed
main 🧊 rework mutation observer
1 parent e4de62f commit ae4f7b2

File tree

10 files changed

+99
-159
lines changed

10 files changed

+99
-159
lines changed

Diff for: ‎src/hooks/useActiveElement/useActiveElement.ts

+18-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { useEffect, useState } from 'react';
22

3-
import { useMutationObserver } from '../useMutationObserver/useMutationObserver';
4-
53
/**
64
* @name useActiveElement
75
* @description - Hook that returns the active element
@@ -21,29 +19,36 @@ export const useActiveElement = <ActiveElement extends HTMLElement>() => {
2119

2220
window.addEventListener('focus', onActiveElementChange, true);
2321
window.addEventListener('blur', onActiveElementChange, true);
22+
2423
return () => {
25-
window.removeEventListener('focus', onActiveElementChange);
26-
window.removeEventListener('blur', onActiveElementChange);
24+
window.removeEventListener('focus', onActiveElementChange, true);
25+
window.removeEventListener('blur', onActiveElementChange, true);
2726
};
2827
});
2928

30-
useMutationObserver(
31-
document as any,
32-
(mutations) => {
29+
useEffect(() => {
30+
const observer = new MutationObserver((mutations) => {
3331
mutations
3432
.filter((mutation) => mutation.removedNodes.length)
3533
.map((mutation) => Array.from(mutation.removedNodes))
3634
.flat()
3735
.forEach((node) => {
38-
if (node === activeElement)
39-
setActiveElement(document?.activeElement as ActiveElement | null);
36+
setActiveElement((prevActiveElement) => {
37+
if (node === prevActiveElement) return document.activeElement as ActiveElement | null;
38+
return prevActiveElement;
39+
});
4040
});
41-
},
42-
{
41+
});
42+
43+
observer.observe(document, {
4344
childList: true,
4445
subtree: true
45-
}
46-
);
46+
});
47+
48+
return () => {
49+
observer.disconnect();
50+
};
51+
}, []);
4752

4853
return activeElement;
4954
};

Diff for: ‎src/hooks/useClickOutside/useClickOutside.demo.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const Demo = () => {
2121
ref={clickOutsideRef}
2222
className={cn(
2323
'relative flex flex-col items-center justify-center rounded-lg border border-red-500 p-12',
24-
{ 'border-green-500': counter.value < 5 }
24+
{ 'border-green-500': counter.value > 5 }
2525
)}
2626
>
2727
{counter.value <= 5 && 'Click outside'}

Diff for: ‎src/hooks/useClickOutside/useClickOutside.test.ts

-47
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,6 @@ it('Should call callback when clicked outside the ref', () => {
4848
expect(callback).toBeCalledTimes(1);
4949
});
5050

51-
it('Should call callback when clicked outside the function that returns an element', () => {
52-
const callback = vi.fn();
53-
const getElement = () => document.createElement('div');
54-
55-
renderHook(() => useClickOutside(getElement, callback));
56-
57-
expect(callback).not.toBeCalled();
58-
59-
act(() => document.dispatchEvent(new Event('click')));
60-
61-
expect(callback).toBeCalledTimes(1);
62-
});
63-
6451
it('Should not call callback when clicked inside the ref', () => {
6552
const callback = vi.fn();
6653
const ref = { current: document.createElement('div') };
@@ -85,37 +72,3 @@ it('Should not call callback when clicked inside the element', () => {
8572

8673
expect(callback).not.toBeCalled();
8774
});
88-
89-
it('Should not call callback when clicked inside the function that returns an element', () => {
90-
const element = document.createElement('div');
91-
document.body.appendChild(element);
92-
93-
const getElement = () => element;
94-
const callback = vi.fn();
95-
96-
renderHook(() => useClickOutside(getElement, callback));
97-
98-
act(() => element.dispatchEvent(new Event('click')));
99-
100-
expect(callback).not.toBeCalled();
101-
});
102-
103-
it('Should call callback when clicked outside the element (multiple targets)', () => {
104-
const element = document.createElement('div');
105-
document.body.appendChild(element);
106-
107-
const elementForGetElementFunction = document.createElement('div');
108-
document.body.appendChild(elementForGetElementFunction);
109-
const getElement = () => elementForGetElementFunction;
110-
111-
const ref = { current: document.createElement('div') };
112-
document.body.appendChild(ref.current);
113-
114-
const callback = vi.fn();
115-
116-
renderHook(() => useClickOutside([element, ref, getElement], callback));
117-
118-
act(() => document.dispatchEvent(new Event('click')));
119-
120-
expect(callback).toBeCalledTimes(1);
121-
});

Diff for: ‎src/hooks/useClickOutside/useClickOutside.ts

+7-28
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,22 @@ import type { RefObject } from 'react';
22

33
import { useEffect, useRef } from 'react';
44

5-
import { getElement } from '@/utils/helpers';
5+
import { getElement, isTarget } from '@/utils/helpers';
6+
7+
import type { StateRef } from '../useRefState/useRefState';
68

79
import { useRefState } from '../useRefState/useRefState';
810

911
/** The use click outside target element type */
10-
export type UseClickOutsideTarget =
11-
| (() => Element)
12-
| string
13-
| Element
14-
| RefObject<Element | null | undefined>;
12+
export type UseClickOutsideTarget = string | Element | RefObject<Element | null | undefined>;
1513

1614
export interface UseClickOutside {
17-
<Target extends UseClickOutsideTarget | UseClickOutsideTarget[]>(
18-
target: Target,
19-
callback: (event: Event) => void
20-
): void;
15+
<Target extends UseClickOutsideTarget>(target: Target, callback: (event: Event) => void): void;
2116

2217
<Target extends UseClickOutsideTarget>(
2318
callback: (event: Event) => void,
2419
target?: never
25-
): (node: Target) => void;
20+
): StateRef<Target>;
2621
}
2722

2823
/**
@@ -48,10 +43,7 @@ export interface UseClickOutside {
4843
* const ref = useClickOutside<HTMLDivElement>(() => console.log('click outside'));
4944
*/
5045
export const useClickOutside = ((...params: any[]) => {
51-
const target = (typeof params[1] === 'undefined' ? undefined : params[0]) as
52-
| UseClickOutsideTarget
53-
| UseClickOutsideTarget[]
54-
| undefined;
46+
const target = (isTarget(params[0]) ? params[0] : undefined) as UseClickOutsideTarget | undefined;
5547
const callback = (params[1] ? params[1] : params[0]) as (event: Event) => void;
5648

5749
const internalRef = useRefState<Element>();
@@ -61,19 +53,6 @@ export const useClickOutside = ((...params: any[]) => {
6153
useEffect(() => {
6254
if (!target && !internalRef.current) return;
6355
const handler = (event: Event) => {
64-
if (Array.isArray(target)) {
65-
if (!target.length) return;
66-
67-
const isClickedOutsideElements = target.every((target) => {
68-
const element = getElement(target) as Element;
69-
return element && !element.contains(event.target as Node);
70-
});
71-
72-
if (isClickedOutsideElements) internalCallbackRef.current(event);
73-
74-
return;
75-
}
76-
7756
const element = (target ? getElement(target) : internalRef.current) as Element;
7857

7958
if (element && !element.contains(event.target as Node)) {

Diff for: ‎src/hooks/useCssVar/useCssVar.ts

+28-19
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import type { RefObject } from 'react';
22

3-
import { useState } from 'react';
3+
import { useEffect, useState } from 'react';
44

55
import { getElement } from '@/utils/helpers';
66

7-
import { useMount } from '../useMount/useMount';
8-
import { useMutationObserver } from '../useMutationObserver/useMutationObserver';
7+
import { useRefState } from '../useRefState/useRefState';
98

109
/** The css variable target element type */
1110
export type UseCssVarTarget =
@@ -43,7 +42,7 @@ export interface UseCssVar {
4342
* @returns {UseCssVarReturn} The object containing the value of the CSS variable
4443
*
4544
* @example
46-
* const { value, set } = useCssVar('color', 'red');
45+
* const { ref, value, set } = useCssVar('color', 'red');
4746
*
4847
* @overload
4948
* @template Target The target element
@@ -63,9 +62,11 @@ export const useCssVar = ((...params: any[]) => {
6362
const initialValue = (target ? params[2] : params[1]) as string | undefined;
6463

6564
const [value, setValue] = useState(initialValue ?? '');
65+
const internalRef = useRefState<Element>(window.document.documentElement);
6666

6767
const set = (value: string) => {
68-
const element = getElement(target ?? window?.document?.documentElement) as HTMLElement;
68+
const element = (target ? getElement(target) : internalRef.current) as HTMLElement;
69+
if (!element) return;
6970

7071
if (element.style) {
7172
if (!value) {
@@ -79,25 +80,33 @@ export const useCssVar = ((...params: any[]) => {
7980
}
8081
};
8182

82-
const updateCssVar = () => {
83-
const element = getElement(target ?? window?.document?.documentElement) as HTMLElement;
83+
useEffect(() => {
84+
if (initialValue) set(initialValue);
85+
}, []);
86+
87+
useEffect(() => {
88+
if (!target && !internalRef) return;
89+
90+
const element = (target ? getElement(target) : internalRef.current) as Element;
8491
if (!element) return;
8592

86-
const value = window
87-
.getComputedStyle(element as Element)
88-
.getPropertyValue(key)
89-
?.trim();
93+
const updateCssVar = () => {
94+
const value = window
95+
.getComputedStyle(element as Element)
96+
.getPropertyValue(key)
97+
?.trim();
9098

91-
setValue(value ?? initialValue);
92-
};
99+
setValue(value ?? initialValue);
100+
};
93101

94-
useMount(() => {
95-
if (initialValue) set(initialValue);
96-
});
102+
const observer = new MutationObserver(updateCssVar);
103+
104+
observer.observe(element, { attributeFilter: ['style', 'class'] });
97105

98-
useMutationObserver(window, updateCssVar, {
99-
attributeFilter: ['style', 'class']
100-
});
106+
return () => {
107+
observer.disconnect();
108+
};
109+
}, [target, internalRef.current]);
101110

102111
return {
103112
value,

Diff for: ‎src/hooks/useDocumentTitle/useDocumentTitle.ts

+26-20
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { useRef, useState } from 'react';
1+
import { useEffect, useRef, useState } from 'react';
22

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

65
/** The use document title options type */
76
export interface UseDocumentTitleOptions {
@@ -37,24 +36,6 @@ export function useDocumentTitle(
3736
const prevTitleRef = useRef(document.title);
3837
const [title, setTitle] = useState(value ?? document.title);
3938

40-
useMutationObserver(
41-
document.head.querySelector('title')!,
42-
() => {
43-
if (document && document.title !== title) {
44-
setTitle(document.title);
45-
}
46-
},
47-
{ childList: true }
48-
);
49-
50-
useIsomorphicLayoutEffect(() => {
51-
if (options?.restoreOnUnmount) {
52-
return () => {
53-
document.title = prevTitleRef.current;
54-
};
55-
}
56-
}, []);
57-
5839
const set = (value: string) => {
5940
const updatedValue = value.trim();
6041
if (updatedValue.length > 0) document.title = updatedValue;
@@ -65,5 +46,30 @@ export function useDocumentTitle(
6546
set(value);
6647
}, [value]);
6748

49+
useIsomorphicLayoutEffect(() => {
50+
const observer = new MutationObserver(() => {
51+
setTitle((prevTitle) => {
52+
if (document && document.title !== prevTitle) {
53+
return document.title;
54+
}
55+
return prevTitle;
56+
});
57+
});
58+
59+
observer.observe(document.head.querySelector('title')!, { childList: true });
60+
61+
return () => {
62+
observer.disconnect();
63+
};
64+
}, []);
65+
66+
useEffect(() => {
67+
if (options?.restoreOnUnmount) {
68+
return () => {
69+
document.title = prevTitleRef.current;
70+
};
71+
}
72+
}, []);
73+
6874
return [title, set];
6975
}

0 commit comments

Comments
 (0)