Skip to content

Commit

Permalink
main 🧊 add use scroll into view, use state history, use display media
Browse files Browse the repository at this point in the history
  • Loading branch information
debabin committed Feb 19, 2025
1 parent 1519934 commit d2cead4
Show file tree
Hide file tree
Showing 31 changed files with 789 additions and 108 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,6 @@ generated

# Turbo
.turbo

# Cursor
.cursor
10 changes: 5 additions & 5 deletions src/hooks/useClickOutside/useClickOutside.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,21 @@ export const useClickOutside = ((...params: any[]) => {
internalCallbackRef.current = callback;

useEffect(() => {
if (!target && !internalRef.current) return;
const handler = (event: Event) => {
if (!target && !internalRef.state) return;
const onClick = (event: Event) => {
const element = (target ? getElement(target) : internalRef.current) as Element;

if (element && !element.contains(event.target as Node)) {
internalCallbackRef.current(event);
}
};

document.addEventListener('click', handler);
document.addEventListener('click', onClick);

return () => {
document.removeEventListener('click', handler);
document.removeEventListener('click', onClick);
};
}, [internalRef.current, target]);
}, [target, internalRef.state]);

if (target) return;
return internalRef;
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useConst/useConst.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ it('Should call initializer function', () => {
const { result } = renderHook(() => useConst(init));

expect(result.current).toBe(99);
expect(init).toHaveBeenCalledTimes(1);
expect(init).toHaveBeenCalledOnce();
});
8 changes: 4 additions & 4 deletions src/hooks/useCssVar/useCssVar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ export const useCssVar = ((...params: any[]) => {
}, []);

useEffect(() => {
if (!target && !internalRef) return;
if (!target && !internalRef.state) return;

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

const updateCssVar = () => {
const onChange = () => {
const value = window
.getComputedStyle(element as Element)
.getPropertyValue(key)
Expand All @@ -99,14 +99,14 @@ export const useCssVar = ((...params: any[]) => {
setValue(value ?? initialValue);
};

const observer = new MutationObserver(updateCssVar);
const observer = new MutationObserver(onChange);

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

return () => {
observer.disconnect();
};
}, [target, internalRef.current]);
}, [target, internalRef.state]);

return {
value,
Expand Down
8 changes: 4 additions & 4 deletions src/hooks/useDevicePixelRatio/useDevicePixelRatio.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ it('Should handle media query change', () => {
configurable: true
});

expect(mockMediaQueryListAddEventListener).toHaveBeenCalledTimes(1);
expect(mockMediaQueryListRemoveEventListener).toHaveBeenCalledTimes(0);
expect(mockMediaQueryListAddEventListener).toHaveBeenCalledOnce();
expect(mockMediaQueryListRemoveEventListener).not.toHaveBeenCalled();

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

expect(mockMediaQueryListAddEventListener).toHaveBeenCalledTimes(2);
expect(mockMediaQueryListRemoveEventListener).toHaveBeenCalledTimes(1);
expect(mockMediaQueryListRemoveEventListener).toHaveBeenCalledOnce();
expect(result.current.ratio).toEqual(3);
});

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

unmount();

expect(mockMediaQueryListRemoveEventListener).toHaveBeenCalledTimes(1);
expect(mockMediaQueryListRemoveEventListener).toHaveBeenCalledOnce();
});
4 changes: 2 additions & 2 deletions src/hooks/useDidUpdate/useDidUpdate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ it('Should call effect on subsequent updates when dependencies change', () => {
expect(effect).not.toHaveBeenCalled();

rerender({ deps: [true] });
expect(effect).toHaveBeenCalledTimes(1);
expect(effect).toHaveBeenCalledOnce();
});

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

rerender();
expect(effect).toHaveBeenCalledTimes(1);
expect(effect).toHaveBeenCalledOnce();

rerender();
expect(effect).toHaveBeenCalledTimes(2);
Expand Down
29 changes: 29 additions & 0 deletions src/hooks/useDisplayMedia/useDisplayMedia.demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useDisplayMedia } from './useDisplayMedia';

const Demo = () => {
const { sharing, supported, start, stop, ref } = useDisplayMedia();

return (
<div className="flex flex-col gap-4">
<div className="flex gap-4 justify-center items-center">
<button
disabled={!supported}
type="button"
onClick={sharing ? stop : start}
>
{sharing ? 'Stop Sharing' : 'Start Sharing'}
</button>
</div>

<video
muted
playsInline
ref={ref}
className="w-full max-w-2xl border rounded"
autoPlay
/>
</div>
);
};

export default Demo;
160 changes: 160 additions & 0 deletions src/hooks/useDisplayMedia/useDisplayMedia.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { act, renderHook, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, expect, vi } from 'vitest';

import { renderHookServer } from '@/tests';
import { getElement } from '@/utils/helpers';

import type { StateRef } from '../useRefState/useRefState';
import type { UseDisplayMediaReturn } from './useDisplayMedia';

import { useDisplayMedia } from './useDisplayMedia';

const mockGetDisplayMedia = vi.fn();
const mockTrack = {
stop: vi.fn(),
onended: vi.fn()
};

beforeEach(() => {
Object.assign(navigator, {
mediaDevices: {
getDisplayMedia: mockGetDisplayMedia
}
});

mockGetDisplayMedia.mockResolvedValue({
getTracks: () => [mockTrack]
});
});

afterEach(() => {
vi.clearAllMocks();
});

const targets = [
undefined,
'#target',
document.getElementById('target') as HTMLVideoElement,
{ current: document.getElementById('target') as HTMLVideoElement }
];

targets.forEach((target) => {
beforeEach(mockGetDisplayMedia.mockClear);

it('Should use display media', () => {
const { result } = renderHook(() => {
if (target)
return useDisplayMedia(target) as {
ref: StateRef<HTMLVideoElement>;
} & UseDisplayMediaReturn;
return useDisplayMedia<HTMLVideoElement>();
});

expect(result.current.sharing).toBe(false);
expect(result.current.stream).toBeNull();
expect(result.current.supported).toBe(true);
expect(result.current.start).toBeTypeOf('function');
expect(result.current.stop).toBeTypeOf('function');
if (!target) expect(result.current.ref).toBeTypeOf('function');
});

it('Should use display media on server', () => {
const { result } = renderHookServer(() => {
if (target)
return useDisplayMedia(target) as {
ref: StateRef<HTMLVideoElement>;
} & UseDisplayMediaReturn;
return useDisplayMedia<HTMLVideoElement>();
});

expect(result.current.sharing).toBe(false);
expect(result.current.stream).toBeNull();
expect(result.current.supported).toBe(false);
expect(result.current.start).toBeTypeOf('function');
expect(result.current.stop).toBeTypeOf('function');
if (!target) expect(result.current.ref).toBeTypeOf('function');
});

it('Should use display media for unsupported', () => {
Object.assign(navigator, {
mediaDevices: undefined
});

const { result } = renderHook(() => {
if (target)
return useDisplayMedia(target) as {
ref: StateRef<HTMLVideoElement>;
} & UseDisplayMediaReturn;
return useDisplayMedia<HTMLVideoElement>();
});

expect(result.current.sharing).toBe(false);
expect(result.current.stream).toBeNull();
expect(result.current.supported).toBe(false);
expect(result.current.start).toBeTypeOf('function');
expect(result.current.stop).toBeTypeOf('function');
if (!target) expect(result.current.ref).toBeTypeOf('function');
});

it('Should be able to start and stop sharing', async () => {
const { result } = renderHook(() => {
if (target)
return useDisplayMedia(target) as {
ref: StateRef<HTMLVideoElement>;
} & UseDisplayMediaReturn;
return useDisplayMedia<HTMLVideoElement>();
});

if (!target)
act(() => result.current.ref(document.getElementById('target')! as HTMLVideoElement));

await act(result.current.start);

const element = (target ? getElement(target) : result.current.ref.current) as HTMLVideoElement;
expect(element.srcObject).toBeTruthy();
expect(result.current.sharing).toBe(true);
expect(result.current.stream).toBeTruthy();

await act(result.current.stop);

expect(mockTrack.stop).toHaveBeenCalled();
expect(result.current.sharing).toBe(false);
expect(result.current.stream).toBeNull();
});

it('Should start immediately when immediate option is true', async () => {
const { result } = renderHook(() => {
if (target)
return useDisplayMedia(target, { enabled: true }) as {
ref: StateRef<HTMLVideoElement>;
} & UseDisplayMediaReturn;
return useDisplayMedia<HTMLVideoElement>({ enabled: true });
});

if (!target)
act(() => result.current.ref(document.getElementById('target')! as HTMLVideoElement));

await waitFor(() => expect(mockGetDisplayMedia).toHaveBeenCalled());
expect(result.current.sharing).toBe(true);
});

it('Should accept boolean audio and video constraints', async () => {
const { result } = renderHook(() => {
if (target)
return useDisplayMedia(target, { audio: false, video: false }) as {
ref: StateRef<HTMLVideoElement>;
} & UseDisplayMediaReturn;
return useDisplayMedia<HTMLVideoElement>({ audio: false, video: false });
});

if (!target)
act(() => result.current.ref(document.getElementById('target')! as HTMLVideoElement));

await act(result.current.start);

expect(mockGetDisplayMedia).toHaveBeenCalledWith({
audio: false,
video: false
});
});
});
Loading

0 comments on commit d2cead4

Please sign in to comment.