Skip to content

Commit d2cead4

Browse files
committed
main 🧊 add use scroll into view, use state history, use display media
1 parent 1519934 commit d2cead4

File tree

31 files changed

+789
-108
lines changed

31 files changed

+789
-108
lines changed

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,6 @@ generated
215215

216216
# Turbo
217217
.turbo
218+
219+
# Cursor
220+
.cursor

‎src/hooks/useClickOutside/useClickOutside.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,21 +51,21 @@ export const useClickOutside = ((...params: any[]) => {
5151
internalCallbackRef.current = callback;
5252

5353
useEffect(() => {
54-
if (!target && !internalRef.current) return;
55-
const handler = (event: Event) => {
54+
if (!target && !internalRef.state) return;
55+
const onClick = (event: Event) => {
5656
const element = (target ? getElement(target) : internalRef.current) as Element;
5757

5858
if (element && !element.contains(event.target as Node)) {
5959
internalCallbackRef.current(event);
6060
}
6161
};
6262

63-
document.addEventListener('click', handler);
63+
document.addEventListener('click', onClick);
6464

6565
return () => {
66-
document.removeEventListener('click', handler);
66+
document.removeEventListener('click', onClick);
6767
};
68-
}, [internalRef.current, target]);
68+
}, [target, internalRef.state]);
6969

7070
if (target) return;
7171
return internalRef;

‎src/hooks/useConst/useConst.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ it('Should call initializer function', () => {
2222
const { result } = renderHook(() => useConst(init));
2323

2424
expect(result.current).toBe(99);
25-
expect(init).toHaveBeenCalledTimes(1);
25+
expect(init).toHaveBeenCalledOnce();
2626
});

‎src/hooks/useCssVar/useCssVar.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,12 @@ export const useCssVar = ((...params: any[]) => {
8585
}, []);
8686

8787
useEffect(() => {
88-
if (!target && !internalRef) return;
88+
if (!target && !internalRef.state) return;
8989

9090
const element = (target ? getElement(target) : internalRef.current) as Element;
9191
if (!element) return;
9292

93-
const updateCssVar = () => {
93+
const onChange = () => {
9494
const value = window
9595
.getComputedStyle(element as Element)
9696
.getPropertyValue(key)
@@ -99,14 +99,14 @@ export const useCssVar = ((...params: any[]) => {
9999
setValue(value ?? initialValue);
100100
};
101101

102-
const observer = new MutationObserver(updateCssVar);
102+
const observer = new MutationObserver(onChange);
103103

104104
observer.observe(element, { attributeFilter: ['style', 'class'] });
105105

106106
return () => {
107107
observer.disconnect();
108108
};
109-
}, [target, internalRef.current]);
109+
}, [target, internalRef.state]);
110110

111111
return {
112112
value,

‎src/hooks/useDevicePixelRatio/useDevicePixelRatio.test.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,13 @@ it('Should handle media query change', () => {
7979
configurable: true
8080
});
8181

82-
expect(mockMediaQueryListAddEventListener).toHaveBeenCalledTimes(1);
83-
expect(mockMediaQueryListRemoveEventListener).toHaveBeenCalledTimes(0);
82+
expect(mockMediaQueryListAddEventListener).toHaveBeenCalledOnce();
83+
expect(mockMediaQueryListRemoveEventListener).not.toHaveBeenCalled();
8484

8585
act(() => trigger.callback(`(resolution: 1dppx)`));
8686

8787
expect(mockMediaQueryListAddEventListener).toHaveBeenCalledTimes(2);
88-
expect(mockMediaQueryListRemoveEventListener).toHaveBeenCalledTimes(1);
88+
expect(mockMediaQueryListRemoveEventListener).toHaveBeenCalledOnce();
8989
expect(result.current.ratio).toEqual(3);
9090
});
9191

@@ -94,5 +94,5 @@ it('Should disconnect on onmount', () => {
9494

9595
unmount();
9696

97-
expect(mockMediaQueryListRemoveEventListener).toHaveBeenCalledTimes(1);
97+
expect(mockMediaQueryListRemoveEventListener).toHaveBeenCalledOnce();
9898
});

‎src/hooks/useDidUpdate/useDidUpdate.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ it('Should call effect on subsequent updates when dependencies change', () => {
2020
expect(effect).not.toHaveBeenCalled();
2121

2222
rerender({ deps: [true] });
23-
expect(effect).toHaveBeenCalledTimes(1);
23+
expect(effect).toHaveBeenCalledOnce();
2424
});
2525

2626
it('Should call effect on rerender when dependencies empty', () => {
@@ -30,7 +30,7 @@ it('Should call effect on rerender when dependencies empty', () => {
3030
expect(effect).not.toHaveBeenCalled();
3131

3232
rerender();
33-
expect(effect).toHaveBeenCalledTimes(1);
33+
expect(effect).toHaveBeenCalledOnce();
3434

3535
rerender();
3636
expect(effect).toHaveBeenCalledTimes(2);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useDisplayMedia } from './useDisplayMedia';
2+
3+
const Demo = () => {
4+
const { sharing, supported, start, stop, ref } = useDisplayMedia();
5+
6+
return (
7+
<div className="flex flex-col gap-4">
8+
<div className="flex gap-4 justify-center items-center">
9+
<button
10+
disabled={!supported}
11+
type="button"
12+
onClick={sharing ? stop : start}
13+
>
14+
{sharing ? 'Stop Sharing' : 'Start Sharing'}
15+
</button>
16+
</div>
17+
18+
<video
19+
muted
20+
playsInline
21+
ref={ref}
22+
className="w-full max-w-2xl border rounded"
23+
autoPlay
24+
/>
25+
</div>
26+
);
27+
};
28+
29+
export default Demo;
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { act, renderHook, waitFor } from '@testing-library/react';
2+
import { afterEach, beforeEach, expect, vi } from 'vitest';
3+
4+
import { renderHookServer } from '@/tests';
5+
import { getElement } from '@/utils/helpers';
6+
7+
import type { StateRef } from '../useRefState/useRefState';
8+
import type { UseDisplayMediaReturn } from './useDisplayMedia';
9+
10+
import { useDisplayMedia } from './useDisplayMedia';
11+
12+
const mockGetDisplayMedia = vi.fn();
13+
const mockTrack = {
14+
stop: vi.fn(),
15+
onended: vi.fn()
16+
};
17+
18+
beforeEach(() => {
19+
Object.assign(navigator, {
20+
mediaDevices: {
21+
getDisplayMedia: mockGetDisplayMedia
22+
}
23+
});
24+
25+
mockGetDisplayMedia.mockResolvedValue({
26+
getTracks: () => [mockTrack]
27+
});
28+
});
29+
30+
afterEach(() => {
31+
vi.clearAllMocks();
32+
});
33+
34+
const targets = [
35+
undefined,
36+
'#target',
37+
document.getElementById('target') as HTMLVideoElement,
38+
{ current: document.getElementById('target') as HTMLVideoElement }
39+
];
40+
41+
targets.forEach((target) => {
42+
beforeEach(mockGetDisplayMedia.mockClear);
43+
44+
it('Should use display media', () => {
45+
const { result } = renderHook(() => {
46+
if (target)
47+
return useDisplayMedia(target) as {
48+
ref: StateRef<HTMLVideoElement>;
49+
} & UseDisplayMediaReturn;
50+
return useDisplayMedia<HTMLVideoElement>();
51+
});
52+
53+
expect(result.current.sharing).toBe(false);
54+
expect(result.current.stream).toBeNull();
55+
expect(result.current.supported).toBe(true);
56+
expect(result.current.start).toBeTypeOf('function');
57+
expect(result.current.stop).toBeTypeOf('function');
58+
if (!target) expect(result.current.ref).toBeTypeOf('function');
59+
});
60+
61+
it('Should use display media on server', () => {
62+
const { result } = renderHookServer(() => {
63+
if (target)
64+
return useDisplayMedia(target) as {
65+
ref: StateRef<HTMLVideoElement>;
66+
} & UseDisplayMediaReturn;
67+
return useDisplayMedia<HTMLVideoElement>();
68+
});
69+
70+
expect(result.current.sharing).toBe(false);
71+
expect(result.current.stream).toBeNull();
72+
expect(result.current.supported).toBe(false);
73+
expect(result.current.start).toBeTypeOf('function');
74+
expect(result.current.stop).toBeTypeOf('function');
75+
if (!target) expect(result.current.ref).toBeTypeOf('function');
76+
});
77+
78+
it('Should use display media for unsupported', () => {
79+
Object.assign(navigator, {
80+
mediaDevices: undefined
81+
});
82+
83+
const { result } = renderHook(() => {
84+
if (target)
85+
return useDisplayMedia(target) as {
86+
ref: StateRef<HTMLVideoElement>;
87+
} & UseDisplayMediaReturn;
88+
return useDisplayMedia<HTMLVideoElement>();
89+
});
90+
91+
expect(result.current.sharing).toBe(false);
92+
expect(result.current.stream).toBeNull();
93+
expect(result.current.supported).toBe(false);
94+
expect(result.current.start).toBeTypeOf('function');
95+
expect(result.current.stop).toBeTypeOf('function');
96+
if (!target) expect(result.current.ref).toBeTypeOf('function');
97+
});
98+
99+
it('Should be able to start and stop sharing', async () => {
100+
const { result } = renderHook(() => {
101+
if (target)
102+
return useDisplayMedia(target) as {
103+
ref: StateRef<HTMLVideoElement>;
104+
} & UseDisplayMediaReturn;
105+
return useDisplayMedia<HTMLVideoElement>();
106+
});
107+
108+
if (!target)
109+
act(() => result.current.ref(document.getElementById('target')! as HTMLVideoElement));
110+
111+
await act(result.current.start);
112+
113+
const element = (target ? getElement(target) : result.current.ref.current) as HTMLVideoElement;
114+
expect(element.srcObject).toBeTruthy();
115+
expect(result.current.sharing).toBe(true);
116+
expect(result.current.stream).toBeTruthy();
117+
118+
await act(result.current.stop);
119+
120+
expect(mockTrack.stop).toHaveBeenCalled();
121+
expect(result.current.sharing).toBe(false);
122+
expect(result.current.stream).toBeNull();
123+
});
124+
125+
it('Should start immediately when immediate option is true', async () => {
126+
const { result } = renderHook(() => {
127+
if (target)
128+
return useDisplayMedia(target, { enabled: true }) as {
129+
ref: StateRef<HTMLVideoElement>;
130+
} & UseDisplayMediaReturn;
131+
return useDisplayMedia<HTMLVideoElement>({ enabled: true });
132+
});
133+
134+
if (!target)
135+
act(() => result.current.ref(document.getElementById('target')! as HTMLVideoElement));
136+
137+
await waitFor(() => expect(mockGetDisplayMedia).toHaveBeenCalled());
138+
expect(result.current.sharing).toBe(true);
139+
});
140+
141+
it('Should accept boolean audio and video constraints', async () => {
142+
const { result } = renderHook(() => {
143+
if (target)
144+
return useDisplayMedia(target, { audio: false, video: false }) as {
145+
ref: StateRef<HTMLVideoElement>;
146+
} & UseDisplayMediaReturn;
147+
return useDisplayMedia<HTMLVideoElement>({ audio: false, video: false });
148+
});
149+
150+
if (!target)
151+
act(() => result.current.ref(document.getElementById('target')! as HTMLVideoElement));
152+
153+
await act(result.current.start);
154+
155+
expect(mockGetDisplayMedia).toHaveBeenCalledWith({
156+
audio: false,
157+
video: false
158+
});
159+
});
160+
});

0 commit comments

Comments
 (0)