feat: Add PageHead component
This commit is contained in:
30 files changed
+600
-114
No files matched your search
@@ -1,4 +1,7 @@
|
||||
export { castRef } from './castRef/castRef';
|
||||
export { genericMemo } from './genericMemo/genericMemo';
|
||||
export { useResizeObserver } from './useResizeObserver/useResizeObserver';
|
||||
export { useScroll } from './useScroll/useScroll';
|
||||
export { useLocalStorage } from './useLocalStorage/useLocalStorage';
|
||||
|
||||
export type { ReactElementProps } from './types/ReactElementProps';
|
||||
@@ -25,11 +25,11 @@ describe('lib/react/useLocalStorage', () => {
|
||||
it('should update state when setter is called', () => {
|
||||
const manager = createLocalStorage(KEY, DEFAULT_VALUE);
|
||||
const { result } = renderHook(() => useLocalStorage(manager));
|
||||
|
||||
|
||||
act(() => {
|
||||
result.current[1]('new value');
|
||||
});
|
||||
|
||||
|
||||
expect(result.current[0]).toBe('new value');
|
||||
expect(window.localStorage.getItem(KEY)).toBe(JSON.stringify('new value'));
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ export function useLocalStorage<T>(
|
||||
const getSnapshot = useCallback(() => manager.get(), [manager]);
|
||||
|
||||
const value = useSyncExternalStore(subscribe, getSnapshot, () => {
|
||||
// During SSR, we can't access localStorage.
|
||||
// During SSR, we can't access localStorage.
|
||||
// However, createLocalStorage is designed to throw if accessed during SSR.
|
||||
// This hook is intended to be used in client-side components.
|
||||
return manager.get();
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { useResizeObserver } from './useResizeObserver';
|
||||
|
||||
describe('useResizeObserver', () => {
|
||||
it('should return initial dimensions', () => {
|
||||
const ref = React.createRef<HTMLDivElement>();
|
||||
const { result } = renderHook(() => useResizeObserver(ref));
|
||||
expect(result.current).toEqual({ width: 0, height: 0 });
|
||||
});
|
||||
|
||||
// Testing the actual resize observation is tricky in JSDOM.
|
||||
// A more complete test would require a full browser environment.
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/** Result of useResizeObserver */
|
||||
export interface UseResizeObserverResult {
|
||||
/** Observed width */
|
||||
width: number;
|
||||
/** Observed height */
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook that observes the size of an element and returns its dimensions.
|
||||
*
|
||||
* @param ref - A React ref to the element to observe.
|
||||
* @returns The dimensions of the observed element.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const MyComponent = () => {
|
||||
* const ref = useRef<HTMLDivElement>(null);
|
||||
* const { width, height } = useResizeObserver(ref);
|
||||
*
|
||||
* return <div ref={ref}>Width: {width}, Height: {height}</div>;
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export const useResizeObserver = <T extends HTMLElement>(
|
||||
ref: React.RefObject<T | null>
|
||||
): UseResizeObserverResult => {
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const observeTarget = ref.current;
|
||||
if (!observeTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
if (!Array.isArray(entries) || !entries.length) {
|
||||
return;
|
||||
}
|
||||
const entry = entries[0];
|
||||
setDimensions({
|
||||
width: entry.contentRect.width,
|
||||
height: entry.contentRect.height,
|
||||
});
|
||||
});
|
||||
|
||||
resizeObserver.observe(observeTarget);
|
||||
|
||||
return () => {
|
||||
resizeObserver.unobserve(observeTarget);
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
return dimensions;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { vi, describe, it, expect } from 'vitest';
|
||||
|
||||
import { useScroll } from './useScroll';
|
||||
|
||||
describe('useScroll', () => {
|
||||
it('should return initial scroll position', () => {
|
||||
const { result } = renderHook(() => useScroll());
|
||||
expect(result.current).toEqual({ x: 0, y: 0 });
|
||||
});
|
||||
|
||||
it('should update scroll position on scroll', () => {
|
||||
vi.useFakeTimers();
|
||||
renderHook(() => useScroll());
|
||||
|
||||
act(() => {
|
||||
window.scrollX = 100;
|
||||
window.scrollY = 200;
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
});
|
||||
|
||||
// We need to wait for the next animation frame
|
||||
act(() => {
|
||||
vi.runAllTimers();
|
||||
});
|
||||
|
||||
// In JSDOM, window.scrollX/Y might not persist the manual assignment
|
||||
// depending on the environment setup, but we'll try to verify it.
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
/** Result of useScroll */
|
||||
export interface UseScrollResult {
|
||||
/** x scroll position */
|
||||
x: number;
|
||||
/** y scroll position */
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook to track window scroll position.
|
||||
*
|
||||
* @returns An object containing the x and y scroll position.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { x, y } = useScroll();
|
||||
* ```
|
||||
*/
|
||||
export const useScroll = (): UseScrollResult => {
|
||||
const [scroll, setScroll] = useState({ x: 0, y: 0 });
|
||||
const ticking = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (!ticking.current) {
|
||||
window.requestAnimationFrame(() => {
|
||||
setScroll({ x: window.scrollX, y: window.scrollY });
|
||||
ticking.current = false;
|
||||
});
|
||||
ticking.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
return scroll;
|
||||
};
|
||||
@@ -29,11 +29,14 @@ describe('lib/window/localStorage', () => {
|
||||
},
|
||||
dispatchEvent: vi.fn(),
|
||||
});
|
||||
vi.stubGlobal('StorageEvent', class {
|
||||
constructor(type: string, init: any) {
|
||||
Object.assign(this, { type, ...init });
|
||||
vi.stubGlobal(
|
||||
'StorageEvent',
|
||||
class {
|
||||
constructor(type: string, init: any) {
|
||||
Object.assign(this, { type, ...init });
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -51,7 +51,10 @@ export function createLocalStorage<T>(key: string, defaultValue: T): StorageMana
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return JSON.parse(item) as T;
|
||||
} catch (error) {
|
||||
console.error(`[createLocalStorage] Failed to parse key "${key}", falling back to default.`, error);
|
||||
console.error(
|
||||
`[createLocalStorage] Failed to parse key "${key}", falling back to default.`,
|
||||
error
|
||||
);
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user