mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(store): initial release (#279)
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { animationFrame } from './animation-frame';
|
||||
|
||||
describe('animationFrame', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('calls the callback on next animation frame', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
animationFrame(callback);
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('passes timestamp to callback', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
animationFrame(callback);
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(expect.any(Number));
|
||||
});
|
||||
|
||||
it('returns a cleanup function', () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
const cancel = animationFrame(callback);
|
||||
|
||||
expect(cancel).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
it('cancel prevents callback from being called', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
const cancel = animationFrame(callback);
|
||||
cancel();
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cancel can be called multiple times safely', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
const cancel = animationFrame(callback);
|
||||
cancel();
|
||||
cancel();
|
||||
cancel();
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows multiple independent animation frames', async () => {
|
||||
const callback1 = vi.fn();
|
||||
const callback2 = vi.fn();
|
||||
const callback3 = vi.fn();
|
||||
|
||||
animationFrame(callback1);
|
||||
animationFrame(callback2);
|
||||
const cancel3 = animationFrame(callback3);
|
||||
|
||||
cancel3();
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback1).toHaveBeenCalledOnce();
|
||||
expect(callback2).toHaveBeenCalledOnce();
|
||||
expect(callback3).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Request an animation frame and return a cleanup function to cancel it.
|
||||
*
|
||||
* @param callback - The callback to invoke on the next animation frame
|
||||
* @returns A cleanup function that cancels the animation frame request
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const cancel = animationFrame((time) => {
|
||||
* console.log('Frame at', time);
|
||||
* });
|
||||
*
|
||||
* // Later, cancel if needed
|
||||
* cancel();
|
||||
* ```
|
||||
*/
|
||||
export function animationFrame(callback: FrameRequestCallback): () => void {
|
||||
const id = requestAnimationFrame(callback);
|
||||
return () => cancelAnimationFrame(id);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { onEvent } from './event';
|
||||
|
||||
describe('onEvent', () => {
|
||||
it('returns a promise', () => {
|
||||
const target = new EventTarget();
|
||||
const promise = onEvent(target, 'click');
|
||||
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
|
||||
// Dispatch to resolve
|
||||
target.dispatchEvent(new Event('click'));
|
||||
});
|
||||
|
||||
it('resolves with the event when it fires', async () => {
|
||||
const target = new EventTarget();
|
||||
const event = new Event('click');
|
||||
|
||||
const promise = onEvent(target, 'click');
|
||||
target.dispatchEvent(event);
|
||||
|
||||
await expect(promise).resolves.toBe(event);
|
||||
});
|
||||
|
||||
it('only listens for the first occurrence (once)', async () => {
|
||||
const target = new EventTarget();
|
||||
|
||||
const promise = onEvent(target, 'click');
|
||||
|
||||
target.dispatchEvent(new Event('click'));
|
||||
target.dispatchEvent(new Event('click'));
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBeInstanceOf(Event);
|
||||
});
|
||||
|
||||
it('rejects when signal is already aborted', async () => {
|
||||
const target = new EventTarget();
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
|
||||
const promise = onEvent(target, 'click', { signal: controller.signal });
|
||||
|
||||
await expect(promise).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('rejects with AbortError when signal is aborted', async () => {
|
||||
const target = new EventTarget();
|
||||
const controller = new AbortController();
|
||||
|
||||
const promise = onEvent(target, 'click', { signal: controller.signal });
|
||||
controller.abort();
|
||||
|
||||
await expect(promise).rejects.toMatchObject({
|
||||
name: 'AbortError',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects with custom abort reason', async () => {
|
||||
const target = new EventTarget();
|
||||
const controller = new AbortController();
|
||||
const reason = new Error('Custom reason');
|
||||
|
||||
const promise = onEvent(target, 'click', { signal: controller.signal });
|
||||
controller.abort(reason);
|
||||
|
||||
await expect(promise).rejects.toBe(reason);
|
||||
});
|
||||
|
||||
it('cleans up abort listener when event fires', async () => {
|
||||
const target = new EventTarget();
|
||||
const controller = new AbortController();
|
||||
|
||||
const promise = onEvent(target, 'click', { signal: controller.signal });
|
||||
target.dispatchEvent(new Event('click'));
|
||||
|
||||
await promise;
|
||||
|
||||
// Aborting after resolution should not cause issues
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
it('passes options to addEventListener', async () => {
|
||||
const target = new EventTarget();
|
||||
const addSpy = vi.spyOn(target, 'addEventListener');
|
||||
|
||||
const promise = onEvent(target, 'click', { passive: true, capture: true });
|
||||
target.dispatchEvent(new Event('click'));
|
||||
|
||||
await promise;
|
||||
|
||||
expect(addSpy).toHaveBeenCalledWith(
|
||||
'click',
|
||||
expect.any(Function),
|
||||
expect.objectContaining({
|
||||
passive: true,
|
||||
capture: true,
|
||||
once: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('works with HTMLMediaElement events', async () => {
|
||||
const video = document.createElement('video');
|
||||
|
||||
const promise = onEvent(video, 'play');
|
||||
video.dispatchEvent(new Event('play'));
|
||||
|
||||
const event = await promise;
|
||||
expect(event.type).toBe('play');
|
||||
});
|
||||
|
||||
it('works with Window events', async () => {
|
||||
const promise = onEvent(window, 'resize');
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
||||
const event = await promise;
|
||||
expect(event.type).toBe('resize');
|
||||
});
|
||||
|
||||
it('works with Document events', async () => {
|
||||
const promise = onEvent(document, 'visibilitychange');
|
||||
document.dispatchEvent(new Event('visibilitychange'));
|
||||
|
||||
const event = await promise;
|
||||
expect(event.type).toBe('visibilitychange');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
export interface OnEventOptions extends AddEventListenerOptions {
|
||||
/**
|
||||
* An AbortSignal to cancel waiting for the event.
|
||||
*
|
||||
* If aborted, the returned promise will reject with an `AbortError`.
|
||||
*/
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for an event to occur on a target.
|
||||
*
|
||||
* @param target - The event target (HTMLMediaElement)
|
||||
* @param type - The event type to wait for
|
||||
* @param options - Optional event options including AbortSignal
|
||||
* @returns A promise that resolves with the event
|
||||
*/
|
||||
export function onEvent<K extends keyof HTMLMediaElementEventMap>(
|
||||
target: HTMLMediaElement,
|
||||
type: K,
|
||||
options?: OnEventOptions,
|
||||
): Promise<HTMLMediaElementEventMap[K]>;
|
||||
|
||||
/**
|
||||
* Wait for an event to occur on a target.
|
||||
*
|
||||
* @param target - The event target (HTMLElement)
|
||||
* @param type - The event type to wait for
|
||||
* @param options - Optional event options including AbortSignal
|
||||
* @returns A promise that resolves with the event
|
||||
*/
|
||||
export function onEvent<K extends keyof HTMLElementEventMap>(
|
||||
target: HTMLElement,
|
||||
type: K,
|
||||
options?: OnEventOptions,
|
||||
): Promise<HTMLElementEventMap[K]>;
|
||||
|
||||
/**
|
||||
* Wait for an event to occur on a target.
|
||||
*
|
||||
* @param target - The event target (Window)
|
||||
* @param type - The event type to wait for
|
||||
* @param options - Optional event options including AbortSignal
|
||||
* @returns A promise that resolves with the event
|
||||
*/
|
||||
export function onEvent<K extends keyof WindowEventMap>(
|
||||
target: Window,
|
||||
type: K,
|
||||
options?: OnEventOptions,
|
||||
): Promise<WindowEventMap[K]>;
|
||||
|
||||
/**
|
||||
* Wait for an event to occur on a target.
|
||||
*
|
||||
* @param target - The event target (Document)
|
||||
* @param type - The event type to wait for
|
||||
* @param options - Optional event options including AbortSignal
|
||||
* @returns A promise that resolves with the event
|
||||
*/
|
||||
export function onEvent<K extends keyof DocumentEventMap>(
|
||||
target: Document,
|
||||
type: K,
|
||||
options?: OnEventOptions,
|
||||
): Promise<DocumentEventMap[K]>;
|
||||
|
||||
/**
|
||||
* Wait for an event to occur on a target.
|
||||
*
|
||||
* @param target - The event target
|
||||
* @param type - The event type to wait for
|
||||
* @param options - Optional event options including AbortSignal
|
||||
* @returns A promise that resolves with the event
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Wait for video to be seeked
|
||||
* const event = await onEvent(video, 'seeked');
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // With AbortSignal for cancellation
|
||||
* const controller = new AbortController();
|
||||
*
|
||||
* try {
|
||||
* const event = await onEvent(video, 'seeked', { signal: controller.signal });
|
||||
* } catch (e) {
|
||||
* if (e.name === 'AbortError') {
|
||||
* console.log('Cancelled waiting for event');
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* // Cancel from elsewhere
|
||||
* controller.abort();
|
||||
* ```
|
||||
*/
|
||||
export function onEvent(target: EventTarget, type: string, options?: OnEventOptions): Promise<Event>;
|
||||
|
||||
export function onEvent(target: EventTarget, type: string, options?: OnEventOptions): Promise<Event> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const handleAbort = () => {
|
||||
reject(options?.signal?.reason ?? new DOMException('Aborted', 'AbortError'));
|
||||
};
|
||||
|
||||
// If already aborted, reject immediately
|
||||
if (options?.signal?.aborted) {
|
||||
handleAbort();
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for abort
|
||||
options?.signal?.addEventListener('abort', handleAbort, { once: true });
|
||||
|
||||
// Listen for the event
|
||||
target.addEventListener(
|
||||
type,
|
||||
(event) => {
|
||||
options?.signal?.removeEventListener('abort', handleAbort);
|
||||
resolve(event);
|
||||
},
|
||||
{ ...options, once: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { idleCallback } from './idle-callback';
|
||||
|
||||
describe('idleCallback', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('calls the callback when idle', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
idleCallback(callback);
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('passes deadline object to callback', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
idleCallback(callback);
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
didTimeout: expect.any(Boolean),
|
||||
timeRemaining: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns a cleanup function', () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
const cancel = idleCallback(callback);
|
||||
|
||||
expect(cancel).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
it('cancel prevents callback from being called', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
const cancel = idleCallback(callback);
|
||||
cancel();
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cancel can be called multiple times safely', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
const cancel = idleCallback(callback);
|
||||
cancel();
|
||||
cancel();
|
||||
cancel();
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows multiple independent idle callbacks', async () => {
|
||||
const callback1 = vi.fn();
|
||||
const callback2 = vi.fn();
|
||||
const callback3 = vi.fn();
|
||||
|
||||
idleCallback(callback1);
|
||||
idleCallback(callback2);
|
||||
const cancel3 = idleCallback(callback3);
|
||||
|
||||
cancel3();
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback1).toHaveBeenCalledOnce();
|
||||
expect(callback2).toHaveBeenCalledOnce();
|
||||
expect(callback3).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('accepts options parameter', async () => {
|
||||
const callback = vi.fn();
|
||||
|
||||
idleCallback(callback, { timeout: 1000 });
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { supportsIdleCallback } from './supports';
|
||||
|
||||
/**
|
||||
* Request an idle callback and return a cleanup function to cancel it.
|
||||
*
|
||||
* Falls back to `setTimeout` with 1ms delay in environments that don't
|
||||
* support `requestIdleCallback` (e.g., Safari).
|
||||
*
|
||||
* @param callback - The callback to invoke when the browser is idle
|
||||
* @param options - Optional idle callback options (timeout, etc.)
|
||||
* @returns A cleanup function that cancels the idle callback request
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const cancel = idleCallback((deadline) => {
|
||||
* console.log('Time remaining:', deadline.timeRemaining());
|
||||
* });
|
||||
*
|
||||
* // Later, cancel if needed
|
||||
* cancel();
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // With timeout option
|
||||
* const cancel = idleCallback(doWork, { timeout: 1000 });
|
||||
* ```
|
||||
*/
|
||||
export function idleCallback(callback: IdleRequestCallback, options?: IdleRequestOptions): () => void {
|
||||
if (supportsIdleCallback()) {
|
||||
const id = requestIdleCallback(callback, options);
|
||||
return () => cancelIdleCallback(id);
|
||||
}
|
||||
|
||||
// Fallback for Safari and other browsers without requestIdleCallback
|
||||
const id = setTimeout(() => {
|
||||
callback({
|
||||
didTimeout: false,
|
||||
timeRemaining: () => 50, // Approximate idle time
|
||||
});
|
||||
}, 1);
|
||||
|
||||
return () => clearTimeout(id);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { animationFrame } from './animation-frame';
|
||||
export { onEvent, type OnEventOptions } from './event';
|
||||
export { idleCallback } from './idle-callback';
|
||||
export { listen } from './listen';
|
||||
export { supportsAnimationFrame, supportsIdleCallback } from './supports';
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { listen } from './listen';
|
||||
|
||||
describe('listen', () => {
|
||||
it('adds an event listener', () => {
|
||||
const target = new EventTarget();
|
||||
const handler = vi.fn();
|
||||
|
||||
listen(target, 'click', handler);
|
||||
target.dispatchEvent(new Event('click'));
|
||||
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('returns a cleanup function', () => {
|
||||
const target = new EventTarget();
|
||||
const handler = vi.fn();
|
||||
|
||||
const cleanup = listen(target, 'click', handler);
|
||||
|
||||
expect(cleanup).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
it('cleanup removes the listener', () => {
|
||||
const target = new EventTarget();
|
||||
const handler = vi.fn();
|
||||
|
||||
const cleanup = listen(target, 'click', handler);
|
||||
cleanup();
|
||||
target.dispatchEvent(new Event('click'));
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes options to addEventListener', () => {
|
||||
const target = new EventTarget();
|
||||
const handler = vi.fn();
|
||||
|
||||
listen(target, 'click', handler, { once: true });
|
||||
|
||||
target.dispatchEvent(new Event('click'));
|
||||
target.dispatchEvent(new Event('click'));
|
||||
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('passes capture option correctly', () => {
|
||||
const parent = document.createElement('div');
|
||||
const child = document.createElement('span');
|
||||
parent.appendChild(child);
|
||||
|
||||
const order: string[] = [];
|
||||
|
||||
listen(parent, 'click', () => order.push('parent-capture'), { capture: true });
|
||||
listen(child, 'click', () => order.push('child'));
|
||||
listen(parent, 'click', () => order.push('parent-bubble'));
|
||||
|
||||
child.dispatchEvent(new Event('click', { bubbles: true }));
|
||||
|
||||
expect(order).toEqual(['parent-capture', 'child', 'parent-bubble']);
|
||||
});
|
||||
|
||||
it('cleanup works with options', () => {
|
||||
const target = new EventTarget();
|
||||
const handler = vi.fn();
|
||||
|
||||
const cleanup = listen(target, 'click', handler, { passive: true });
|
||||
cleanup();
|
||||
target.dispatchEvent(new Event('click'));
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('works with AbortSignal in options', () => {
|
||||
const target = new EventTarget();
|
||||
const handler = vi.fn();
|
||||
const controller = new AbortController();
|
||||
|
||||
listen(target, 'click', handler, { signal: controller.signal });
|
||||
|
||||
target.dispatchEvent(new Event('click'));
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
|
||||
controller.abort();
|
||||
target.dispatchEvent(new Event('click'));
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('provides correct event type for typed targets', () => {
|
||||
const video = document.createElement('video');
|
||||
const handler = vi.fn((event: Event) => {
|
||||
expect(event.type).toBe('play');
|
||||
});
|
||||
|
||||
listen(video, 'play', handler);
|
||||
video.dispatchEvent(new Event('play'));
|
||||
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('works with Window', () => {
|
||||
const handler = vi.fn();
|
||||
const cleanup = listen(window, 'resize', handler);
|
||||
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('works with Document', () => {
|
||||
const handler = vi.fn();
|
||||
const cleanup = listen(document, 'visibilitychange', handler);
|
||||
|
||||
document.dispatchEvent(new Event('visibilitychange'));
|
||||
expect(handler).toHaveBeenCalledOnce();
|
||||
|
||||
cleanup();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Add an event listener and return a cleanup function to remove it.
|
||||
*
|
||||
* @param target - The event target (HTMLMediaElement)
|
||||
* @param type - The event type
|
||||
* @param listener - The event listener
|
||||
* @param options - Optional event listener options
|
||||
* @returns A cleanup function that removes the event listener
|
||||
*/
|
||||
export function listen<K extends keyof HTMLMediaElementEventMap>(
|
||||
target: HTMLMediaElement,
|
||||
type: K,
|
||||
listener: (event: HTMLMediaElementEventMap[K]) => void,
|
||||
options?: AddEventListenerOptions,
|
||||
): () => void;
|
||||
|
||||
/**
|
||||
* Add an event listener and return a cleanup function to remove it.
|
||||
*
|
||||
* @param target - The event target (HTMLElement)
|
||||
* @param type - The event type
|
||||
* @param listener - The event listener
|
||||
* @param options - Optional event listener options
|
||||
* @returns A cleanup function that removes the event listener
|
||||
*/
|
||||
export function listen<K extends keyof HTMLElementEventMap>(
|
||||
target: HTMLElement,
|
||||
type: K,
|
||||
listener: (event: HTMLElementEventMap[K]) => void,
|
||||
options?: AddEventListenerOptions,
|
||||
): () => void;
|
||||
|
||||
/**
|
||||
* Add an event listener and return a cleanup function to remove it.
|
||||
*
|
||||
* @param target - The event target (Window)
|
||||
* @param type - The event type
|
||||
* @param listener - The event listener
|
||||
* @param options - Optional event listener options
|
||||
* @returns A cleanup function that removes the event listener
|
||||
*/
|
||||
export function listen<K extends keyof WindowEventMap>(
|
||||
target: Window,
|
||||
type: K,
|
||||
listener: (event: WindowEventMap[K]) => void,
|
||||
options?: AddEventListenerOptions,
|
||||
): () => void;
|
||||
|
||||
/**
|
||||
* Add an event listener and return a cleanup function to remove it.
|
||||
*
|
||||
* @param target - The event target (Document)
|
||||
* @param type - The event type
|
||||
* @param listener - The event listener
|
||||
* @param options - Optional event listener options
|
||||
* @returns A cleanup function that removes the event listener
|
||||
*/
|
||||
export function listen<K extends keyof DocumentEventMap>(
|
||||
target: Document,
|
||||
type: K,
|
||||
listener: (event: DocumentEventMap[K]) => void,
|
||||
options?: AddEventListenerOptions,
|
||||
): () => void;
|
||||
|
||||
/**
|
||||
* Add an event listener and return a cleanup function to remove it.
|
||||
*
|
||||
* @param target - The event target
|
||||
* @param type - The event type
|
||||
* @param listener - The event listener
|
||||
* @param options - Optional event listener options
|
||||
* @returns A cleanup function that removes the event listener
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const cleanup = listen(video, 'play', () => console.log('playing'));
|
||||
*
|
||||
* // Later, remove the listener
|
||||
* cleanup();
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // With options
|
||||
* const cleanup = listen(video, 'play', handler, { once: true, passive: true });
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // With AbortSignal (native browser support)
|
||||
* const controller = new AbortController();
|
||||
* listen(video, 'play', handler, { signal: controller.signal });
|
||||
*
|
||||
* // Later, abort to remove the listener
|
||||
* controller.abort();
|
||||
* ```
|
||||
*/
|
||||
export function listen(
|
||||
target: EventTarget,
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: AddEventListenerOptions,
|
||||
): () => void;
|
||||
|
||||
export function listen(
|
||||
target: EventTarget,
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: AddEventListenerOptions,
|
||||
): () => void {
|
||||
target.addEventListener(type, listener, options);
|
||||
return () => target.removeEventListener(type, listener, options);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { supportsAnimationFrame, supportsIdleCallback } from './supports';
|
||||
|
||||
describe('supports', () => {
|
||||
describe('supportsAnimationFrame', () => {
|
||||
it('returns a boolean', () => {
|
||||
const result = supportsAnimationFrame();
|
||||
expect(typeof result).toBe('boolean');
|
||||
});
|
||||
|
||||
it('returns true in browser environment', () => {
|
||||
expect(supportsAnimationFrame()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('supportsIdleCallback', () => {
|
||||
it('returns a boolean', () => {
|
||||
const result = supportsIdleCallback();
|
||||
// Note: requestIdleCallback may or may not be available in jsdom
|
||||
// depending on the version, so we just check it returns a boolean
|
||||
expect(typeof result).toBe('boolean');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Check if `requestIdleCallback` is supported.
|
||||
*
|
||||
* @returns `true` if `requestIdleCallback` is available
|
||||
*/
|
||||
export function supportsIdleCallback(): boolean {
|
||||
return typeof requestIdleCallback === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if `requestAnimationFrame` is supported.
|
||||
*
|
||||
* @returns `true` if `requestAnimationFrame` is available
|
||||
*/
|
||||
export function supportsAnimationFrame(): boolean {
|
||||
return typeof requestAnimationFrame === 'function';
|
||||
}
|
||||
Reference in New Issue
Block a user